Merge branch '2.0' into 2.0-aop

This commit is contained in:
黄朝晖 2020-05-25 14:26:18 +08:00 committed by GitHub
commit 31db7269bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
147 changed files with 9843 additions and 355 deletions

View File

@ -62,11 +62,13 @@ return PhpCsFixer\Config::create()
'multiline_whitespace_before_semicolons' => [
'strategy' => 'no_multi_line',
],
'constant_case' => [
'case' => 'lower',
],
'class_attributes_separation' => true,
'combine_consecutive_unsets' => true,
'declare_strict_types' => true,
'linebreak_after_opening_tag' => true,
'lowercase_constants' => true,
'lowercase_static_reference' => true,
'no_useless_else' => true,
'no_unused_imports' => true,

View File

@ -5,11 +5,11 @@ sudo: required
matrix:
include:
- php: 7.2
env: SW_VERSION="4.5.0"
env: SW_VERSION="4.5.1"
- php: 7.3
env: SW_VERSION="4.5.0"
env: SW_VERSION="4.5.1"
- php: 7.4
env: SW_VERSION="4.5.0"
env: SW_VERSION="4.5.1"
- php: 7.2
env: SW_VERSION="4.4.18"
- php: 7.3
@ -47,5 +47,18 @@ script:
src/cache \
src/command \
src/config
src/di \
src/json-rpc \
src/tracer \
src/metric \
src/redis \
src/nats \
src/db \
src/retry \
src/grpc-client \
src/nsq \
src/filesystem \
src/socketio-server \
src/load-balancer
- composer test -- --exclude-group NonCoroutine
- vendor/bin/phpunit --group NonCoroutine

View File

@ -35,7 +35,8 @@ VALUES
(2,'https://avatars2.githubusercontent.com/u/44228082?s=200&v=4',2,'user','2018-01-01 00:00:00','2018-01-01 00:00:00'),
(3,'https://avatars2.githubusercontent.com/u/44228082?s=200&v=4',1,'book','2018-01-01 00:00:00','2018-01-01 00:00:00'),
(4,'https://avatars2.githubusercontent.com/u/44228082?s=200&v=4',2,'book','2018-01-01 00:00:00','2018-01-01 00:00:00'),
(5,'https://avatars2.githubusercontent.com/u/44228082?s=200&v=4',3,'book','2018-01-01 00:00:00','2018-01-01 00:00:00');
(5,'https://avatars2.githubusercontent.com/u/44228082?s=200&v=4',3,'book','2018-01-01 00:00:00','2018-01-01 00:00:00'),
(6,'https://avatars2.githubusercontent.com/u/44228082?s=200&v=4',0,'','2018-01-01 00:00:00','2018-01-01 00:00:00');
DROP TABLE IF EXISTS `role`;

View File

@ -1,8 +1,55 @@
# v1.1.30 - TBD
# v1.1.33 - TBD
# v1.1.32 - 2020-05-21
## Fixed
- [#1734](https://github.com/hyperf/hyperf/pull/1734) Fixed the bug that the morph association is empty and cannot be queried.
- [#1739](https://github.com/hyperf/hyperf/pull/1739) Fixed the wrong bitwise operator in oss hook.
- [#1743](https://github.com/hyperf/hyperf/pull/1743) Fixed the wrong `refId` for `grafana.json`.
- [#1748](https://github.com/hyperf/hyperf/pull/1748) Fixed `concurrent.limit` does not works when using another pool.
- [#1750](https://github.com/hyperf/hyperf/pull/1750) Fixed the incorrent number of current connections when close failed.
- [#1754](https://github.com/hyperf/hyperf/pull/1754) Fixed the wrong start info for base server.
- [#1764](https://github.com/hyperf/hyperf/pull/1764) Fixed datetime validate failed when the value is null.
- [#1769](https://github.com/hyperf/hyperf/pull/1769) Fixed a notice when client initiate disconnects in `socketio-server`.
## Added
- [#1616](https://github.com/hyperf/hyperf/pull/1616) Added ORM methods `morphWith` and `whereHasMorph`.
- [#1724](https://github.com/hyperf/hyperf/pull/1724) Added `Model::orWhereHasMorph` ,`Model::whereDoesntHaveMorph` and `Model::orWhereDoesntHaveMorph`.
- [#1741](https://github.com/hyperf/hyperf/pull/1741) Added `Hyperf\Command\Command::choiceMultiple(): array` method, because the return type of `choice` method is `string`, so the methed cannot handle the multiple selections, even though setted `$multiple` argument.
- [#1742](https://github.com/hyperf/hyperf/pull/1742) Added Custom Casts for model.
- Added interface `Castable`, `CastsAttributes` and `CastsInboundAttributes`.
- Added `Model\Builder::withCasts`.
- Added `Model::loadMorph`, `Model::loadMorphCount` and `Model::syncAttributes`.
# v1.1.31 - 2020-05-14
## Added
- [#1723](https://github.com/hyperf/hyperf/pull/1723) Added filp/whoops integration in hyperf/exception-handler component.
- [#1730](https://github.com/hyperf/hyperf/pull/1730) Added shortcut `-R` of `--refresh-fillable` for command `gen:model`.
## Fixed
- [#1696](https://github.com/hyperf/hyperf/pull/1696) Fixed `Context::copy` does not works when use keys.
- [#1708](https://github.com/hyperf/hyperf/pull/1708) [#1718](https://github.com/hyperf/hyperf/pull/1718) Fixed a series of issues for `hyperf/socketio-server`.
## Optimized
- [#1710](https://github.com/hyperf/hyperf/pull/1710) Don't set process title in Darwin OS.
# v1.1.30 - 2020-05-07
## Added
- [#1616](https://github.com/hyperf/hyperf/pull/1616) Added `morphWith` and `whereHasMorph` for hyperf/database component.
- [#1651](https://github.com/hyperf/hyperf/pull/1651) Added socket.io-server component.
- [#1666](https://github.com/hyperf/hyperf/pull/1666) [#1669](https://github.com/hyperf/hyperf/pull/1669) Added support for AMQP RPC mode.
## Fixed
- [#1682](https://github.com/hyperf/hyperf/pull/1682) Fixed the connection pool does not works in JSONRPC pool transporter.
- [#1683](https://github.com/hyperf/hyperf/pull/1683) Fixed JSONRPC client connection reset failed, when the connection was closed in context.
## Optimized

View File

@ -85,6 +85,7 @@ Hyperf 还提供了 `基于 PSR-11 的依赖注入容器`、`注解`、`AOP 面
# 性能
### 阿里云 8 核 16G
命令: `wrk -c 1024 -t 8 http://127.0.0.1:9501/`
```bash
Running 10s test @ http://127.0.0.1:9501/
@ -97,6 +98,10 @@ Requests/sec: 103921.49
Transfer/sec: 18.83MB
```
# Hyperf 生态
- 🧬 [Nano](https://github.com/hyperf/nano) 是一款零配置、无骨架、极小化的 Hyperf 发行版,通过 Nano 可以让您仅仅通过 1 个 PHP 文件即可快速搭建一个 Hyperf 应用。
# 开源协议
Hyperf 是一个基于 [MIT 协议](https://github.com/hyperf/hyperf/blob/master/LICENSE) 开源的软件。

View File

@ -11,7 +11,7 @@ English | [中文](./README-CN.md)
</p>
# Introduction
s
Hyperf is an extremely performant and flexible PHP CLI framework based on `Swoole 4.4+`, powered by the state-of-the-art coroutine server and a large number of battle-tested components. Aside from the decisive benchmark outmatching against PHP-FPM frameworks, Hyperf also distinct itself by its focus on flexibility and composability. Hyperf ships with an AOP-enabling dependency injector to ensure components and classes are pluggable and meta programmable. All of its core components strictly follow the PSR standards and thus can be used in other frameworks.
Hyperf's architecture is built upon the combination of `Coroutine`, `Dependency injection`, `Events`, `Annotation`, `AOP (aspect-oriented programming)`. Core components provided by Hyperf can be used out of the box in coroutine context. The set includes but not limited to: `MySQL coroutine client`, `Redis coroutine client`, `WebSocket server and client`, `JSON RPC server and client`, `gRPC server and client`, `Zipkin/Jaeger (OpenTracing) client`, `Guzzle HTTP client`, `Elasticsearch client`, `Consul client`, `ETCD client`, `AMQP component`, `Apollo configuration center`, `Aliyun ACM`, `ETCD configuration center`, `Token bucket algorithm-based limiter`, `Universal connection pool`, `Circuit breaker`, `Swagger`, `Swoole Tracker`, `Snowflake`, `Simply Redis MQ`, `RabbitMQ`, `NSQ`, `Nats`, `Seconds level crontab`, `Custom Processes`, etc. Be assured Hyperf is still a PHP framework. You will also find familiar packages such as `Middleware`, `Event Manager`, `Coroutine optimized Eloquent ORM` (And Model Cache!), `Translation`, `Validation`, `View engine (Blade/Smarty/Twig/Plates/ThinkTemplate)` and more at your command.
@ -73,11 +73,12 @@ Support this project with your organization or company. Your logo will show up h
# Performance
### Aliyun 8 cores 16G ram
command: `wrk -c 1024 -t 8 http://127.0.0.1:9501/`
```bash
Running 10s test @ http://127.0.0.1:9501/
8 threads and 1024 connections
Thread Stats Avg Stdev Max +/- Stdev
Thread Stats Avg Stdev Max +/- Stdevs
Latency 10.08ms 6.82ms 56.66ms 70.19%
Req/Sec 13.17k 5.94k 33.06k 84.12%
1049478 requests in 10.10s, 190.16MB read
@ -85,6 +86,10 @@ Requests/sec: 103921.49
Transfer/sec: 18.83MB
```
# The Hyperf Ecosystem
- 🧬 [Nano](https://github.com/hyperf/nano) is a zero-config, no skeleton, minimal Hyperf distribution that allows you to quickly build a Hyperf application with just a single PHP file.
# License
The Hyperf framework is open-source software licensed under the MIT license.

View File

@ -39,13 +39,14 @@
"elasticsearch/elasticsearch": "^7.0",
"endclothing/prometheus_client_php": "^0.9.1",
"fig/http-message-util": "^1.1.2",
"filp/whoops": "^2.7",
"friendsofphp/php-cs-fixer": "^2.14",
"google/protobuf": "^3.6.1",
"grpc/grpc": "^1.15",
"guzzlehttp/guzzle": "^6.3",
"influxdb/influxdb-php": "^1.15.0",
"ircmaxell/random-lib": "^1.2",
"jcchavezs/zipkin-opentracing": "^0.1.4",
"jcchavezs/zipkin-opentracing": "^0.1.5",
"jean85/pretty-package-versions": "^1.2",
"jonahgeorge/jaeger-client-php": "^0.4.4",
"laminas/laminas-mime": "^2.7",
@ -55,6 +56,7 @@
"league/plates": "^3.3",
"malukenho/docheader": "^0.1.6",
"markrogoyski/math-php": "^0.49.0",
"mix/redis-subscribe": "^2.1",
"mockery/mockery": "^1.0",
"monolog/monolog": "^2.0",
"nesbot/carbon": "^2.0",
@ -124,8 +126,8 @@
"hyperf/server": "self.version",
"hyperf/service-governance": "self.version",
"hyperf/session": "self.version",
"hyperf/socketio-server": "self.version",
"hyperf/swagger": "self.version",
"hyperf/swoole-enterprise": "self.version",
"hyperf/task": "self.version",
"hyperf/tracer": "self.version",
"hyperf/translation": "self.version",
@ -143,10 +145,10 @@
"files": [
"src/config/src/Functions.php",
"src/di/src/Functions.php",
"src/filesystem/src/Adapter/AliyunOssHook.php",
"src/nats/src/Functions.php",
"src/translation/src/Functions.php",
"src/utils/src/Functions.php",
"src/filesystem/src/Adapter/AliyunOssHook.php"
"src/utils/src/Functions.php"
],
"psr-4": {
"Hyperf\\Amqp\\": "src/amqp/src/",
@ -205,10 +207,10 @@
"Hyperf\\ServiceGovernance\\": "src/service-governance/src/",
"Hyperf\\Session\\": "src/session/src/",
"Hyperf\\Snowflake\\": "src/snowflake/src/",
"Hyperf\\SocketIOServer\\": "src/socketio-server/src/",
"Hyperf\\Socket\\": "src/socket/src/",
"Hyperf\\SuperGlobals\\": "src/super-globals/src/",
"Hyperf\\Swagger\\": "src/swagger/src/",
"Hyperf\\SwooleEnterprise\\": "src/swoole-enterprise/src/",
"Hyperf\\SwooleTracker\\": "src/swoole-tracker/src/",
"Hyperf\\Task\\": "src/task/src/",
"Hyperf\\Testing\\": "src/testing/src/",
@ -274,6 +276,7 @@
"HyperfTest\\ServiceGovernance\\": "src/service-governance/tests/",
"HyperfTest\\Session\\": "src/session/tests/",
"HyperfTest\\Snowflake\\": "src/snowflake/tests/",
"HyperfTest\\SocketIOServer\\": "src/socketio-server/tests/",
"HyperfTest\\Socket\\": "src/socket/tests/",
"HyperfTest\\SuperGlobals\\": "src/super-globals/tests/",
"HyperfTest\\Task\\": "src/task/tests/",
@ -340,10 +343,10 @@
"Hyperf\\ServiceGovernance\\ConfigProvider",
"Hyperf\\Session\\ConfigProvider",
"Hyperf\\Snowflake\\ConfigProvider",
"Hyperf\\SocketIOServer\\ConfigProvider",
"Hyperf\\Socket\\ConfigProvider",
"Hyperf\\SuperGlobals\\ConfigProvider",
"Hyperf\\Swagger\\ConfigProvider",
"Hyperf\\SwooleEnterprise\\ConfigProvider",
"Hyperf\\SwooleTracker\\ConfigProvider",
"Hyperf\\Task\\ConfigProvider",
"Hyperf\\Tracer\\ConfigProvider",

View File

@ -1,24 +0,0 @@
# 介绍
Hyperf 是基于 `Swoole 4.4+` 实现的高性能、高灵活性的 PHP 协程框架,内置协程服务器及大量常用的组件,性能较传统基于 `PHP-FPM` 的框架有质的提升,提供超高性能的同时,也保持着极其灵活的可扩展性,标准组件均基于 [PSR 标准](https://www.php-fig.org/psr) 实现,基于强大的依赖注入设计,保证了绝大部分组件或类都是 `可替换``可复用` 的。
框架组件库除了常见的协程版的 `MySQL 客户端`、`Redis 客户端`,还为您准备了协程版的 `Eloquent ORM`、`WebSocket 服务端及客户端`、`JSON RPC 服务端及客户端`、`GRPC 服务端及客户端`、`Zipkin/Jaeger (OpenTracing) 客户端`、`Guzzle HTTP 客户端`、`Elasticsearch 客户端`、`Consul 客户端`、`ETCD 客户端`、`AMQP 组件`、`Apollo 配置中心`、`阿里云 ACM 应用配置管理`、`ETCD 配置中心`、`基于令牌桶算法的限流器`、`通用连接池`、`熔断器`、`Swagger 文档生成`、`Swoole Tracker`、`视图引擎`、`Snowflake 全局 ID 生成器` 等组件,省去了自己实现对应协程版本的麻烦。
Hyperf 还提供了 `基于 PSR-11 的依赖注入容器`、`注解`、`AOP 面向切面编程`、`基于 PSR-15 的中间件`、`自定义进程`、`基于 PSR-14 的事件管理器`、`Redis/RabbitMQ 消息队列`、`自动模型缓存`、`基于 PSR-16 的缓存`、`Crontab 秒级定时任务`、`国际化`、`Validation 表单验证器` 等非常便捷的功能,满足丰富的技术场景和业务场景,开箱即用。
# 框架初衷
尽管现在基于 PHP 语言开发的框架处于一个百家争鸣的时代,但仍旧未能看到一个优雅的设计与超高性能的共存的完美框架,亦没有看到一个真正为 PHP 微服务铺路的框架,此为 Hyperf 及其团队成员的初衷,我们将持续投入并为此付出努力,也欢迎你加入我们参与开源建设。
# 设计理念
`Hyperspeed + Flexibility = Hyperf`,从名字上我们就将 `超高速``灵活性` 作为 Hyperf 的基因。
- 对于超高速,我们基于 Swoole 协程并在框架设计上进行大量的优化以确保超高性能的输出。
- 对于灵活性,我们基于 Hyperf 强大的依赖注入组件,组件均基于 [PSR 标准](https://www.php-fig.org/psr) 的契约和由 Hyperf 定义的契约实现,达到框架内的绝大部分的组件或类都是可替换的。
基于以上的特点Hyperf 将存在丰富的可能性,如实现 Web 服务网关服务分布式中间件微服务架构游戏服务器物联网IOT等。
# 生产可用
我们为组件进行了大量的单元测试以保证逻辑的正确,同时维护了高质量的文档,在 Hyperf 正式对外开放之前,便已经过了严酷的生产环境的考验,我们才正式的对外开放该项目,至今,已有很多的大型/中小型互联网公司在生产环境使用 Hyperf。

251
doc/en/nano.md Normal file
View File

@ -0,0 +1,251 @@
`hyperf/nano` is a package that scales Hyperf down to a single file.
## Example
```php
<?php
// index.php
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create('0.0.0.0', 9051);
$app->get('/', function () {
$user = $this->request->input('user', 'nano');
$method = $this->request->getMethod();
return [
'message' => "hello {$user}",
'method' => $method,
];
});
$app->run();
```
Run
```bash
php index.php start
```
That's all you need.
## Feature
* No skeleton.
* Fast startup.
* Zero config.
* Closure style.
* Support all Hyperf features except annotations.
* Compatible with all Hyperf components.
## More Examples
### Routing
$app inherits all methods from hyperf router.
```php
<?php
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->addGroup('/nano', function () use ($app) {
$app->addRoute(['GET', 'POST'], '/{id:\d+}', function($id) {
return '/nano/'.$id;
});
$app->put('/{name:.+}', function($name) {
return '/nano/'.$name;
});
});
$app->run();
```
### DI Container
```php
<?php
use Hyperf\Nano\ContainerProxy;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
class Foo {
public function bar() {
return 'bar';
}
}
$app = AppFactory::create();
$app->getContainer()->set(Foo::class, new Foo());
$app->get('/', function () {
/** @var ContainerProxy $this */
$foo = $this->get(Foo::class);
return $foo->bar();
});
$app->run();
```
> As a convention, $this is bind to ContainerProxy in all closures managed by nano, including middleware, exception handler and more.
### Middleware
```php
<?php
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->get('/', function () {
return $this->request->getAttribute('key');
});
$app->addMiddleware(function ($request, $handler) {
$request = $request->withAttribute('key', 'value');
return $handler->handle($request);
});
$app->run();
```
> In addition to closure, all $app->addXXX() methods also accept class name as argument. You can pass any corresponding hyperf classes.
### ExceptionHandler
```php
<?php
use Hyperf\HttpMessage\Stream\SwooleStream;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->get('/', function () {
throw new \Exception();
});
$app->addExceptionHandler(function ($throwable, $response) {
return $response->withStatus('418')
->withBody(new SwooleStream('I\'m a teapot'));
});
$app->run();
```
### Custom Command
```php
<?php
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->addCommand('echo', function(){
$this->get(StdoutLoggerInterface::class)->info('A new command called echo!');
});
$app->run();
```
To run this command, execute
```bash
php index.php echo
```
### Event Listener
```php
<?php
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Framework\Event\BootApplication;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->addListener(BootApplication::class, function($event){
$this->get(StdoutLoggerInterface::class)->info('App started');
});
$app->run();
```
### Custom Process
```php
<?php
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->addProcess(function(){
while (true) {
sleep(1);
$this->get(StdoutLoggerInterface::class)->info('Processing...');
}
});
$app->run();
```
### Crontab
```php
<?php
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->addCrontab('* * * * * *', function(){
$this->get(StdoutLoggerInterface::class)->info('execute every second!');
});
$app->run();
```
### Use Hyperf Component.
```php
<?php
use Hyperf\DB\DB;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->config([
'db.default' => [
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', 3306),
'database' => env('DB_DATABASE', 'hyperf'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
]
]);
$app->get('/', function(){
return DB::query('SELECT * FROM `user` WHERE gender = ?;', [1]);
});
$app->run();
```

View File

@ -112,14 +112,14 @@ ENTRYPOINT ["sh", ".build/entrypoint.sh"]
composer require hyperf/swoole-dashboard dev-master
```
然后将以下 `Middleware` 写到 `middleware.php` 中。
然后将以下 `Middleware` 写到 `config/autoload/middlewares.php` 配置文件中。
```php
<?php
return [
'http' => [
Hyperf\SwooleDashboard\Middleware\HttpServerMiddleware::class
Hyperf\SwooleDashboard\Middlewasre\HttpServerMiddleware::class
],
];

View File

@ -28,6 +28,7 @@
window.$docsify = {
name: 'Hyperf',
repo: 'hyperf/hyperf',
homepage: './zh-cn/README.md',
loadSidebar: 'summary.md',
loadNavbar: true,
fallbackLanguages: ['zh-cn', 'en'],
@ -35,8 +36,8 @@
themeColor: '#3F51B5',
logo: '/logo.png',
auto2top: true,
autoHeader: false,
subMaxLevel: 4,
topMargin: 20,
search: {
depth: 6,
noData: {

View File

@ -18,7 +18,7 @@ composer require hyperf/async-queue
|:----------------:|:---------:|:-------------------------------------------:|:---------------------------------------:|
| driver | string | Hyperf\AsyncQueue\Driver\RedisDriver::class | 无 |
| channel | string | queue | 队列前缀 |
| redis.pool | string | default | redis连接池 |
| redis.pool | string | default | redis 连接池 |
| timeout | int | 2 | pop 消息的超时时间 |
| retry_seconds | int,array | 5 | 失败后重新尝试间隔 |
| handle_timeout | int | 10 | 消息处理超时时间 |

View File

@ -1,5 +1,60 @@
# 版本更新记录
# v1.1.32 - 2020-05-21
## 修复
- [#1734](https://github.com/hyperf/hyperf/pull/1734) 修复模型多态查询,关联关系为空时,也会查询 SQL 的问题;
- [#1739](https://github.com/hyperf/hyperf/pull/1739) 修复 `hyperf/filesystem` 组件 OSS HOOK 位运算错误,导致 resource 判断不准确的问题;
- [#1743](https://github.com/hyperf/hyperf/pull/1743) 修复 `grafana.json` 中错误的`refId` 字段值;
- [#1748](https://github.com/hyperf/hyperf/pull/1748) 修复 `hyperf/amqp` 组件在使用其他连接池时,对应的 `concurrent.limit` 配置不生效的问题;
- [#1750](https://github.com/hyperf/hyperf/pull/1750) 修复连接池组件,在连接关闭失败时会导致计数有误的问题;
- [#1754](https://github.com/hyperf/hyperf/pull/1754) 修复 BASE Server 服务,启动提示没有考虑 UDP 服务的情况;
- [#1764](https://github.com/hyperf/hyperf/pull/1764) 修复当时间值为 null 时datatime 验证器执行失败的 BUG
- [#1769](https://github.com/hyperf/hyperf/pull/1769) 修复 `hyperf/socketio-server` 组件中,客户端初始化断开连接操作时会报 Notice 的错误的问题;
## 新增
- [#1724](https://github.com/hyperf/hyperf/pull/1724) 新增模型方法 `Model::orWhereHasMorph` ,`Model::whereDoesntHaveMorph` and `Model::orWhereDoesntHaveMorph`
- [#1741](https://github.com/hyperf/hyperf/pull/1741) 新增 `Hyperf\Command\Command::choiceMultiple(): array` 方法,因为 `choice` 方法的返回类型为 `string所以就算设置了 `$multiple` 参数也无法处理多个选择的情况;
- [#1742](https://github.com/hyperf/hyperf/pull/1742) 新增模型 自定义类型转换器 功能;
- 新增 interface `Castable`, `CastsAttributes``CastsInboundAttributes`
- 新增方法 `Model\Builder::withCasts`
- 新增方法 `Model::loadMorph`, `Model::loadMorphCount``Model::syncAttributes`
# v1.1.31 - 2020-05-14
## 新增
- [#1723](https://github.com/hyperf/hyperf/pull/1723) 异常处理器集成了 filp/whoops 。
- [#1730](https://github.com/hyperf/hyperf/pull/1730) 为命令 `gen:model` 可选项 `--refresh-fillable` 添加简写 `-R`
## 修复
- [#1696](https://github.com/hyperf/hyperf/pull/1696) 修复方法 `Context::copy` 传入字段 `keys` 后无法正常使用的BUG。
- [#1708](https://github.com/hyperf/hyperf/pull/1708) [#1718](https://github.com/hyperf/hyperf/pull/1718) 修复 `hyperf/socketio-server` 组件内存溢出等BUG。
## 优化
- [#1710](https://github.com/hyperf/hyperf/pull/1710) MAC系统下不再使用 `cli_set_process_title` 方法设置进程名。
# v1.1.30 - 2020-05-07
## 新增
- [#1616](https://github.com/hyperf/hyperf/pull/1616) 新增 ORM 方法 `morphWith``whereHasMorph`
- [#1651](https://github.com/hyperf/hyperf/pull/1651) 新增 `socket.io-server` 组件。
- [#1666](https://github.com/hyperf/hyperf/pull/1666) [#1669](https://github.com/hyperf/hyperf/pull/1669) 新增 AMQP RPC 客户端。
## 修复
- [#1682](https://github.com/hyperf/hyperf/pull/1682) 修复 `RpcPoolTransporter` 的连接池配置不生效的 BUG。
- [#1683](https://github.com/hyperf/hyperf/pull/1683) 修复 `RpcConnection` 连接失败后,相同协程内无法正常重置连接的 BUG。
## 优化
- [#1670](https://github.com/hyperf/hyperf/pull/1670) 优化掉 `Cache 组件` 一条无意义的删除指令。
# v1.1.28 - 2020-04-30
## 新增
@ -26,16 +81,16 @@
## 新增
- [#1575](https://github.com/hyperf/hyperf/pull/1575) 为脚本 `gen:model` 生成的模型,自动添加 `relation` `scope``attributes` 的变量注释。
- [#1586](https://github.com/hyperf/hyperf/pull/1586) 添加 `symfony/event-dispatcher` 组件小于 `4.3` 时的 `conflict` 配置。用于解决用户使用了 `4.3` 以下版本时,导致 `SymfonyDispatcher` 实现冲突的BUG。
- [#1586](https://github.com/hyperf/hyperf/pull/1586) 添加 `symfony/event-dispatcher` 组件小于 `4.3` 时的 `conflict` 配置。用于解决用户使用了 `4.3` 以下版本时,导致 `SymfonyDispatcher` 实现冲突的 BUG。
- [#1597](https://github.com/hyperf/hyperf/pull/1597) 为 `AMQP` 消费者,添加最大消费次数 `maxConsumption`
- [#1603](https://github.com/hyperf/hyperf/pull/1603) 为 `WebSocket` 服务添加基于 `fd` 存储的 `Context`
## 修复
- [#1553](https://github.com/hyperf/hyperf/pull/1553) 修复 `jsonrpc` 服务,发布了相同名字不同协议到 `consul`客户端无法正常工作的BUG。
- [#1589](https://github.com/hyperf/hyperf/pull/1589) 修复了文件锁在协程下可能会造成死锁的BUG。
- [#1607](https://github.com/hyperf/hyperf/pull/1607) 修复了重写后的 `go` 方法,返回值与 `swoole` 原生方法不符的BUG。
- [#1624](https://github.com/hyperf/hyperf/pull/1624) 修复当路由 `Handler` 是匿名函数时,脚本 `describe:routes` 执行失败的BUG。
- [#1553](https://github.com/hyperf/hyperf/pull/1553) 修复 `jsonrpc` 服务,发布了相同名字不同协议到 `consul` 后,客户端无法正常工作的 BUG。
- [#1589](https://github.com/hyperf/hyperf/pull/1589) 修复了文件锁在协程下可能会造成死锁的 BUG。
- [#1607](https://github.com/hyperf/hyperf/pull/1607) 修复了重写后的 `go` 方法,返回值与 `swoole` 原生方法不符的 BUG。
- [#1624](https://github.com/hyperf/hyperf/pull/1624) 修复当路由 `Handler` 是匿名函数时,脚本 `describe:routes` 执行失败的 BUG。
# v1.1.26 - 2020-04-16
@ -47,9 +102,9 @@
- [#1563](https://github.com/hyperf/hyperf/pull/1563) 修复服务关停后,定时器的 `onOneServer` 配置不会被重置。
- [#1565](https://github.com/hyperf/hyperf/pull/1565) 当 `DB` 组件重连 `Mysql` 时,重置事务等级为 0。
- [#1572](https://github.com/hyperf/hyperf/pull/1572) 修复 `Hyperf\GrpcServer\CoreMiddleware`自定义类的父类找不到时报错的BUG。
- [#1577](https://github.com/hyperf/hyperf/pull/1577) 修复 `describe:routes` 脚本 `server` 配置不生效的BUG。
- [#1579](https://github.com/hyperf/hyperf/pull/1579) 修复 `migrate:refresh` 脚本 `step` 参数不为 `int` 时会报错的BUG。
- [#1572](https://github.com/hyperf/hyperf/pull/1572) 修复 `Hyperf\GrpcServer\CoreMiddleware` 中,自定义类的父类找不到时报错的 BUG。
- [#1577](https://github.com/hyperf/hyperf/pull/1577) 修复 `describe:routes` 脚本 `server` 配置不生效的 BUG。
- [#1579](https://github.com/hyperf/hyperf/pull/1579) 修复 `migrate:refresh` 脚本 `step` 参数不为 `int` 时会报错的 BUG。
## 变更

555
doc/zh-cn/db/mutators.md Normal file
View File

@ -0,0 +1,555 @@
# 修改器
> 本文档大量借鉴于 [LearnKu](https://learnku.com) 十分感谢 LearnKu 对 PHP 社区做出的贡献。
当你在模型实例中获取或设置某些属性值的时候,访问器和修改器允许你对模型属性值进行格式化。
## 访问器 & 修改器
### 定义一个访问器
若要定义一个访问器, 则需在模型上创建一个 `getFooAttribute` 方法,要访问的 `Foo` 字段需使用「驼峰式」命名。 在这个示例中,我们将为 `first_name` 属性定义一个访问器。当模型尝试获取 `first_name` 属性时,将自动调用此访问器:
```php
<?php
namespace App;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 获取用户的姓名.
*
* @param string $value
* @return string
*/
public function getFirstNameAttribute($value)
{
return ucfirst($value);
}
}
```
如你所见,字段的原始值被传递到访问器中,允许你对它进行处理并返回结果。如果想获取被修改后的值,你可以在模型实例上访问 `first_name` 属性:
```php
$user = App\User::find(1);
$firstName = $user->first_name;
```
当然,你也可以通过已有的属性值,使用访问器返回新的计算值:
```php
namespace App;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 获取用户的姓名.
*
* @return string
*/
public function getFullNameAttribute()
{
return "{$this->first_name} {$this->last_name}";
}
}
```
### 定义一个修改器
若要定义一个修改器,则需在模型上面定义 `setFooAttribute` 方法。要访问的 `Foo` 字段使用「驼峰式」命名。让我们再来定义一个 `first_name` 属性的修改器。当我们尝试在模式上在设置 `first_name` 属性值时,该修改器将被自动调用:
```php
<?php
namespace App;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 设置用户的姓名.
*
* @param string $value
* @return void
*/
public function setFirstNameAttribute($value)
{
$this->attributes['first_name'] = strtolower($value);
}
}
```
修改器会获取属性已经被设置的值,允许你修改并且将其值设置到模型内部的 `$attributes` 属性上。举个例子,如果我们尝试将 `first_name` 属性的值设置为 `Sally`
```php
$user = App\User::find(1);
$user->first_name = 'Sally';
```
在这个例子中,`setFirstNameAttribute` 方法在调用的时候接受 `Sally` 这个值作为参数。接着修改器会应用 `strtolower` 函数并将处理的结果设置到内部的 `$attributes` 数组。
## 日期转化器
默认情况下,模型会将 `created_at``updated_at` 字段转换为 `Carbon` 实例,它继承了 `PHP` 原生的 `DateTime` 类并提供了各种有用的方法。你可以通过设置模型的 `$dates` 属性来添加其他日期属性:
```php
<?php
namespace App;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 应该转换为日期格式的属性.
*
* @var array
*/
protected $dates = [
'seen_at',
];
}
```
> Tip: 你可以通过将模型的公有属性 $timestamps 值设置为 false 来禁用默认的 created_at 和 updated_at 时间戳。
当某个字段是日期格式时,你可以将值设置为一个 `UNIX` 时间戳,日期时间 `(Y-m-d)` 字符串,或者 `DateTime` / `Carbon` 实例。日期值会被正确格式化并保存到你的数据库中:
就如上面所说,当获取到的属性包含在 `$dates` 属性中时,都会自动转换为 `Carbon` 实例,允许你在属性上使用任意的 `Carbon` 方法:
```php
$user = App\User::find(1);
return $user->deleted_at->getTimestamp();
```
### 时间格式
时间戳都将以 `Y-m-d H:i:s` 形式格式化。如果你需要自定义时间戳格式,可在模型中设置 `$dateFormat` 属性。这个属性决定了日期属性将以何种形式保存在数据库中,以及当模型序列化成数组或 `JSON` 时的格式:
```php
<?php
namespace App;
use Hyperf\DbConnection\Model\Model;
class Flight extends Model
{
/**
* 这个属性应该被转化为原生类型.
*
* @var string
*/
protected $dateFormat = 'U';
}
```
## 属性类型转换
模型中的 `$casts` 属性提供了一个便利的方法来将属性转换为常见的数据类型。`$casts` 属性应是一个数组,且数组的键是那些需要被转换的属性名称,值则是你希望转换的数据类型。
支持转换的数据类型有:`integer`, `real`, `float`, `double`, `decimal:<digits>`, `string`, `boolean`, `object`, `array`, `collection`, `date`, `datetime``timestamp`。 当需要转换为 `decimal` 类型时,你需要定义小数位的个数,如: `decimal:2`
示例, 让我们把以整数( `0``1` )形式存储在数据库中的 `is_admin` 属性转成布尔值:
```php
<?php
namespace App;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'is_admin' => 'boolean',
];
}
```
现在当你访问 `is_admin` 属性时,虽然保存在数据库里的值是一个整数类型,但是返回值总是会被转换成布尔值类型:
```php
$user = App\User::find(1);
if ($user->is_admin) {
//
}
```
### 自定义类型转换
模型内置了多种常用的类型转换。但是,用户偶尔会需要将数据转换成自定义类型。现在,该需求可以通过定义一个实现 `CastsAttributes` 接口的类来完成
实现了该接口的类必须事先定义一个 `get` 和 `set` 方法。 `get` 方法负责将从数据库中获取的原始数据转换成对应的类型,而 `set` 方法则是将数据转换成对应的数据库类型以便存入数据库中。举个例子,下面我们将内置的 `json` 类型转换以自定义类型转换的形式重新实现一遍:
```php
<?php
namespace App\Casts;
use Hyperf\Contract\CastsAttributes;
class Json implements CastsAttributes
{
/**
* 将取出的数据进行转换
*/
public function get($model, $key, $value, $attributes)
{
return json_decode($value, true);
}
/**
* 转换成将要进行存储的值
*/
public function set($model, $key, $value, $attributes)
{
return json_encode($value);
}
}
```
定义好自定义类型转换后,可以使用其类名称将其附加到模型属性:
```php
<?php
namespace App;
use App\Casts\Json;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 应进行类型转换的属性
*
* @var array
*/
protected $casts = [
'options' => Json::class,
];
}
```
#### 值对象类型转换
你不仅可以将数据转换成原生的数据类型,还可以将数据转换成对象。两种自定义类型转换的定义方式非常类似。但是将数据转换成对象的自定义转换类中的 `set` 方法需要返回键值对数组,用于设置原始、可存储的值到对应的模型中。
举个例子,定义一个自定义类型转换类用于将多个模型属性值转换成单个 `Address` 值对象,假设 `Address` 对象有两个公有属性 `lineOne``lineTwo`
```php
<?php
namespace App\Casts;
use App\Address;
use Hyperf\Contract\CastsAttributes;
class AddressCaster implements CastsAttributes
{
/**
* 将取出的数据进行转换
* @return \App\Address
*/
public function get($model, $key, $value, $attributes)
{
return new Address(
$attributes['address_line_one'],
$attributes['address_line_two']
);
}
/**
* 转换成将要进行存储的值
*/
public function set($model, $key, $value, $attributes)
{
return [
'address_line_one' => $value->lineOne,
'address_line_two' => $value->lineTwo,
];
}
}
```
进行值对象类型转换后,任何对值对象的数据变更将会自动在模型保存前同步回模型当中:
```php
<?php
$user = App\User::find(1);
$user->address->lineOne = 'Updated Address Value';
$user->address->lineTwo = '#10000';
$user->save();
var_dump($user->getAttributes());
//[
// 'address_line_one' => 'Updated Address Value',
// 'address_line_two' => '#10000'
//];
```
**这里的实现与 Laravel 不同,如果出现以下用法,请需要格外注意**
```php
$user = App\User::find(1);
var_dump($user->getAttributes());
//[
// 'address_line_one' => 'Address Value',
// 'address_line_two' => '#10000'
//];
$user->address->lineOne = 'Updated Address Value';
$user->address->lineTwo = '#20000';
// 直接修改 address 的字段后,是无法立马再 attributes 中生效的,但可以直接通过 $user->address 拿到修改后的数据。
var_dump($user->getAttributes());
//[
// 'address_line_one' => 'Address Value',
// 'address_line_two' => '#10000'
//];
// 当我们保存数据或者删除数据后attributes 便会改成修改后的数据。
$user->save();
var_dump($user->getAttributes());
//[
// 'address_line_one' => 'Updated Address Value',
// 'address_line_two' => '#20000'
//];
```
如果修改 `address` 后,不想要保存,也不想通过 `address->lineOne` 获取 `address_line_one` 的数据,还可以使用以下 方法
```php
$user = App\User::find(1);
$user->address->lineOne = 'Updated Address Value';
$user->syncAttributes();
var_dump($user->getAttributes());
```
当然,如果您仍然需要修改对应的 `value` 后,同步修改 `attributes` 的功能,可以尝试使用以下方式。首先,我们实现一个 `UserInfo` 并继承 `CastsValue`
```php
namespace App\Caster;
use Hyperf\Database\Model\CastsValue;
/**
* @property string $name
* @property int $gender
*/
class UserInfo extends CastsValue
{
}
```
然后实现对应的 `UserInfoCaster`
```php
<?php
declare(strict_types=1);
namespace App\Caster;
use Hyperf\Contract\CastsAttributes;
use Hyperf\Utils\Arr;
class UserInfoCaster implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes)
{
return new UserInfo($model, Arr::only($attributes, ['name', 'gender']));
}
public function set($model, string $key, $value, array $attributes)
{
return [
'name' => $value->name,
'gender' => $value->gender,
];
}
}
```
当我们再使用以下方式修改 UserInfo 时,便可以同步修改到 attributes 的数据。
```php
/** @var User $user */
$user = User::query()->find(100);
$user->userInfo->name = 'John1';
var_dump($user->getAttributes()); // ['name' => 'John1']
```
#### 入站类型转换
有时候你可能只需要对写入模型的属性值进行类型转换而不需要对从模型中获取的属性值进行任何处理。一个典型入站类型转换的例子就是「hashing」。入站类型转换类需要实现 `CastsInboundAttributes` 接口,只需要实现 `set` 方法。
```php
<?php
namespace App\Casts;
use Hyperf\Contract\CastsInboundAttributes;
class Hash implements CastsInboundAttributes
{
/**
* 哈希算法
*
* @var string
*/
protected $algorithm;
/**
* 创建一个新的类型转换类实例
*/
public function __construct($algorithm = 'md5')
{
$this->algorithm = $algorithm;
}
/**
* 转换成将要进行存储的值
*/
public function set($model, $key, $value, $attributes)
{
return hash($this->algorithm, $value);
}
}
```
#### 类型转换参数
当将自定义类型转换附加到模型时,可以指定传入的类型转换参数。传入类型转换参数需使用 `:` 将参数与类名分隔,多个参数之间使用逗号分隔。这些参数将会传递到类型转换类的构造函数中:
```php
<?php
namespace App;
use App\Casts\Json;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 应进行类型转换的属性
*
* @var array
*/
protected $casts = [
'secret' => Hash::class.':sha256',
];
}
```
### 数组 & `JSON` 转换
当你在数据库存储序列化的 `JSON` 的数据时,`array` 类型的转换非常有用。比如:如果你的数据库具有被序列化为 `JSON``JSON``TEXT` 字段类型,并且在模型中加入了 `array` 类型转换,那么当你访问的时候就会自动被转换为 `PHP` 数组:
```php
<?php
namespace App;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 应进行类型转换的属性
*
* @var array
*/
protected $casts = [
'options' => 'array',
];
}
```
一旦定义了转换,你访问 `options` 属性时他会自动从 `JSON` 类型反序列化为 `PHP` 数组。当你设置了 `options` 属性的值时,给定的数组也会自动序列化为 `JSON` 类型存储:
```php
$user = App\User::find(1);
$options = $user->options;
$options['key'] = 'value';
$user->options = $options;
$user->save();
```
### Date 类型转换
当使用 `date``datetime` 属性时,可以指定日期的格式。 这种格式会被用在模型序列化为数组或者 `JSON`
```php
<?php
namespace App;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 应进行类型转换的属性
*
* @var array
*/
protected $casts = [
'created_at' => 'datetime:Y-m-d',
];
}
```
### 查询时类型转换
有时候需要在查询执行过程中对特定属性进行类型转换,例如需要从数据库表中获取数据的时候。举个例子,请参考以下查询:
```php
use App\Post;
use App\User;
$users = User::select([
'users.*',
'last_posted_at' => Post::selectRaw('MAX(created_at)')
->whereColumn('user_id', 'users.id')
])->get();
```
在该查询获取到的结果集中,`last_posted_at` 属性将会是一个字符串。假如我们在执行查询时进行 `date` 类型转换将更方便。你可以通过使用 `withCasts` 方法来完成上述操作:
```php
$users = User::select([
'users.*',
'last_posted_at' => Post::selectRaw('MAX(created_at)')
->whereColumn('user_id', 'users.id')
])->withCasts([
'last_posted_at' => 'date'
])->get();
```

View File

@ -106,6 +106,33 @@ class IndexController extends Controller
```
在上面这个例子,我们先假设 `FooException` 是存在的一个异常,以及假设已经完成了该处理器的配置,那么当业务抛出一个没有被捕获处理的异常时,就会根据配置的顺序依次传递,整一个处理流程可以理解为一个管道,若前一个异常处理器调用 `$this->stopPropagation()` 则不再往后传递,若最后一个配置的异常处理器仍不对该异常进行捕获处理,那么就会交由 Hyperf 的默认异常处理器处理了。
## 集成 Whoops
框架提供了 Whoops 集成。
首先安装 Whoops
```php
composer require --dev filp/whoops
```
然后配置 Whoops 专用异常处理器。
```php
// config/autoload/exceptions.php
return [
'handler' => [
'http' => [
\Hyperf\ExceptionHandler\Handler\WhoopsExceptionHandler::class,
],
],
];
```
效果如图:
![whoops](/imgs/whoops.png)
## Error 监听器
框架提供了 `error_reporting()` 错误级别的监听器 `Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler`

View File

@ -130,7 +130,7 @@ return [
1. S3 存储请确认安装 `hyperf/guzzle` 组件以提供协程化支持。阿里云、七牛云存储请[开启 Curl Hook](/zh-cn/coroutine?id=swoole-runtime-hook-level)来使用协程。因 Curl Hook 的参数支持性问题,请使用 Swoole 4.4.13 以上版本。
2. minIO, ceph radosgw 等私有对象存储方案均支持 S3 协议,可以使用 S3 适配器。
3. 使用Local驱动时根目录是配置好的地址而不是操作系统的根目录。例如Local驱动 `root` 设置为 `/var/www`, 则本地磁盘上的 `/var/www/public/file.txt` 通过 flysystem API 访问时应使用 `/public/file.txt``public/file.txt`
3. 使用 Local 驱动时根目录是配置好的地址而不是操作系统的根目录。例如Local 驱动 `root` 设置为 `/var/www`, 则本地磁盘上的 `/var/www/public/file.txt` 通过 flysystem API 访问时应使用 `/public/file.txt``public/file.txt`
4. 以阿里云 OSS 为例1 核 1 进程读操作性能对比:
```bash
@ -239,7 +239,7 @@ return [
'accessKey' => env('QINIU_ACCESS_KEY'),
'secretKey' => env('QINIU_SECRET_KEY'),
'bucket' => env('QINIU_BUCKET'),
'domain' => env('QINBIU_DOMAIN'),
'domain' => env('QINIU_DOMAIN'),
],
],
];

BIN
doc/zh-cn/imgs/whoops.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 KiB

260
doc/zh-cn/nano.md Normal file
View File

@ -0,0 +1,260 @@
通过 `hyperf/nano` 可以在无骨架、零配置的情况下快速搭建 Hyperf 应用。
## 安装
```php
composer install hyperf/nano
```
## 快速开始
```php
<?php
// index.php
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create('0.0.0.0', 9051);
$app->get('/', function () {
$user = $this->request->input('user', 'nano');
$method = $this->request->getMethod();
return [
'message' => "hello {$user}",
'method' => $method,
];
});
$app->run();
```
启动:
```bash
php index.php start
```
简洁如此。
## 特性
* 无骨架
* 零配置
* 快速启动
* 闭包风格
* 支持注解外的全部 Hyperf 功能
* 兼容全部 Hyperf 组件
* Phar 友好
## 更多示例
### 路由
$app 集成了 Hyperf 路由器的所有方法。
```php
<?php
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->addGroup('/nano', function () use ($app) {
$app->addRoute(['GET', 'POST'], '/{id:\d+}', function($id) {
return '/nano/'.$id;
});
$app->put('/{name:.+}', function($name) {
return '/nano/'.$name;
});
});
$app->run();
```
### DI 容器
```php
<?php
use Hyperf\Nano\ContainerProxy;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
class Foo {
public function bar() {
return 'bar';
}
}
$app = AppFactory::create();
$app->getContainer()->set(Foo::class, new Foo());
$app->get('/', function () {
/** @var ContainerProxy $this */
$foo = $this->get(Foo::class);
return $foo->bar();
});
$app->run();
```
> 所有 $app 管理的闭包回调中,$this 都被绑定到了 `Hyperf\Nano\ContainerProxy` 上。
### 中间件
```php
<?php
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->get('/', function () {
return $this->request->getAttribute('key');
});
$app->addMiddleware(function ($request, $handler) {
$request = $request->withAttribute('key', 'value');
return $handler->handle($request);
});
$app->run();
```
> 除了闭包之外,所有 $app->addXXX() 方法还接受类名作为参数。可以传入对应的 Hyperf 类。
### 异常处理
```php
<?php
use Hyperf\HttpMessage\Stream\SwooleStream;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->get('/', function () {
throw new \Exception();
});
$app->addExceptionHandler(function ($throwable, $response) {
return $response->withStatus('418')
->withBody(new SwooleStream('I\'m a teapot'));
});
$app->run();
```
### 命令行
```php
<?php
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->addCommand('echo', function(){
$this->get(StdoutLoggerInterface::class)->info('A new command called echo!');
});
$app->run();
```
执行
```bash
php index.php echo
```
### 事件监听
```php
<?php
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Framework\Event\BootApplication;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->addListener(BootApplication::class, function($event){
$this->get(StdoutLoggerInterface::class)->info('App started');
});
$app->run();
```
### 自定义进程
```php
<?php
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->addProcess(function(){
while (true) {
sleep(1);
$this->get(StdoutLoggerInterface::class)->info('Processing...');
}
});
$app->run();
```
### 定时任务
```php
<?php
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->addCrontab('* * * * * *', function(){
$this->get(StdoutLoggerInterface::class)->info('execute every second!');
});
$app->run();
```
### 使用 Hyperf 组件.
```php
<?php
use Hyperf\DB\DB;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->config([
'db.default' => [
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', 3306),
'database' => env('DB_DATABASE', 'hyperf'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
]
]);
$app->get('/', function(){
return DB::query('SELECT * FROM `user` WHERE gender = ?;', [1]);
});
$app->run();
```

View File

@ -21,6 +21,7 @@ namespace App\Controller;
use Hyperf\HttpServer\Annotation\AutoController;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\Paginator\Paginator;
use Hyperf\Utils\Collection;
/**
* @AutoController()
@ -29,15 +30,20 @@ class UserController
{
public function index(RequestInterface $request)
{
$currentPage = $request->input('page', 1);
$perPage = $request->input('per_page', 2);
$users = [
$currentPage = (int) $request->input('page', 1);
$perPage = (int) $request->input('per_page', 2);
// 这里根据 $currentPage 和 $perPage 进行数据查询,以下使用 Collection 代替
$collection = new Collection([
['id' => 1, 'name' => 'Tom'],
['id' => 2, 'name' => 'Sam'],
['id' => 3, 'name' => 'Tim'],
['id' => 4, 'name' => 'Joe'],
];
return new Paginator($users, (int) $perPage, (int) $currentPage);
]);
$users = array_values($collection->forPage($currentPage, $perPage)->toArray());
return new Paginator($users, $perPage, $currentPage);
}
}
```

View File

@ -49,7 +49,6 @@ AbstractProvider::setGuzzleOptions([
<?php
! defined('SWOOLE_HOOK_FLAGS') && define('SWOOLE_HOOK_FLAGS', SWOOLE_HOOK_ALL | SWOOLE_HOOK_CURL);
```
## 如何使用 EasyWeChat
@ -78,6 +77,20 @@ $xml = $this->request->getBody()->getContents();
$app['request'] = new Request($get,$post,[],$cookie,$files,$server,$xml);
// Do something...
```
3. 服务器配置
如果需要使用微信公众平台的服务器配置功能,可以使用以下代码。
> 以下 `$response``Symfony\Component\HttpFoundation\Response` 并非 `Hyperf\HttpMessage\Server\Response`
> 所以只需将 `Body` 内容直接返回,即可通过微信验证。
```php
$response = $app->server->serve();
return $response->getBody()->getContents();
```
## 如何替换缓存
@ -92,5 +105,4 @@ use EasyWeChat\Factory;
$app = Factory::miniProgram([]);
$app['cache'] = ApplicationContext::getContainer()->get(CacheInterface::class);
```

View File

@ -14,7 +14,7 @@ Session 组件的配置储存于 `config/autoload/session.php` 文件中,如
## 配置 Session 中间件
在使用 Session 之前,您需要将 `Hyperf\Session\Middleware\SessionMiddleware` 中间件配置为 HTTP Server 的全局中间件,这样组件才能介入到请求流程进行对应的处理,`config/autoload/middleware.php` 配置文件示例如下:
在使用 Session 之前,您需要将 `Hyperf\Session\Middleware\SessionMiddleware` 中间件配置为 HTTP Server 的全局中间件,这样组件才能介入到请求流程进行对应的处理,`config/autoload/middlewares.php` 配置文件示例如下:
```php
<?php

View File

@ -66,7 +66,7 @@ return [
```
框架中使用 `Snowfalke` 十分简单,只需要从 `DI` 中取出 `IdGeneratorInterface` 对象即可。
框架中使用 `Snowflake` 十分简单,只需要从 `DI` 中取出 `IdGeneratorInterface` 对象即可。
```php
<?php

View File

@ -0,0 +1,290 @@
Socket.io是一款非常流行的应用层实时通讯协议和框架可以轻松实现应答、分组、广播。hyperf/socketio-server支持了Socket.io的WebSocket传输协议。
## 安装
```bash
composer require hyperf/socketio-server
```
hyperf/socketio-server 是基于WebSocket实现的请确保服务端已经添加了WebSocket服务配置。
```php
[
'name' => 'socket-io',
'type' => Server::SERVER_WEBSOCKET,
'host' => '0.0.0.0',
'port' => 9502,
'sock_type' => SWOOLE_SOCK_TCP,
'callbacks' => [
SwooleEvent::ON_HAND_SHAKE => [Hyperf\WebSocketServer\Server::class, 'onHandShake'],
SwooleEvent::ON_MESSAGE => [Hyperf\WebSocketServer\Server::class, 'onMessage'],
SwooleEvent::ON_CLOSE => [Hyperf\WebSocketServer\Server::class, 'onClose'],
],
],
```
## 快速开始
### 服务端
```php
<?php
declare(strict_types=1);
namespace App\Controller;
use Hyperf\SocketIOServer\Annotation\Event;
use Hyperf\SocketIOServer\Annotation\SocketIONamespace;
use Hyperf\SocketIOServer\BaseNamespace;
use Hyperf\SocketIOServer\Socket;
use Hyperf\Utils\Codec\Json;
/**
* @SocketIONamespace("/")
*/
class WebSocketController extends BaseNamespace
{
/**
* @Event("event")
* @param string $data
*/
public function onEvent(Socket $socket, $data)
{
// 应答
return 'Event Received: ' . $data;
}
/**
* @Event("join-room")
* @param string $data
*/
public function onJoinRoom(Socket $socket, $data)
{
// 将当前用户加入房间
$socket->join($data);
// 向房间内其他用户推送(不含当前用户)
$socket->to($data)->emit('event', $socket->getSid() . "has joined {$data}");
// 向房间内所有人广播(含当前用户)
$this->emit('event', 'There are ' . count($socket->getAdapter()->clients($data)) . " players in {$data}");
}
/**
* @Event("say")
* @param string $data
*/
public function onSay(Socket $socket, $data)
{
$data = Json::decode($data);
$socket->to($data['room'])->emit('event', $socket->getSid() . " say: {$data['message']}");
}
}
```
> 每个 socket 会自动加入以自己 `sid` 命名的房间(`$socket->getSid()`),发送私聊信息就推送到对应 `sid` 即可。
> 框架会自动触发 `connect``disconnect` 两个事件。
### 客户端
由于服务端只实现了WebSocket通讯所以客户端要加上 `{transports:["websocket"]}`
```html
<script src="https://cdn.bootcss.com/socket.io/2.3.0/socket.io.js"></script>
<script>
var socket = io('ws://127.0.0.1:9502', { transports: ["websocket"] });
socket.on('connect', data => {
socket.emit('event', 'hello, hyperf', console.log);
socket.emit('join-room', 'room1', console.log);
setInterval(function () {
socket.emit('say', '{"room":"room1", "message":"Hello Hyperf."}');
}, 1000);
});
socket.on('event', console.log);
</script>
```
## API 清单
```php
<?php
function onConnect(\Hyperf\SocketIOServer\Socket $socket){
// sending to the client
$socket->emit('hello', 'can you hear me?', 1, 2, 'abc');
// sending to all clients except sender
$socket->broadcast->emit('broadcast', 'hello friends!');
// sending to all clients in 'game' room except sender
$socket->to('game')->emit('nice game', "let's play a game");
// sending to all clients in 'game1' and/or in 'game2' room, except sender
$socket->to('game1')->to('game2')->emit('nice game', "let's play a game (too)");
// WARNING: `$socket->to($socket->getSid())->emit()` will NOT work, as it will send to everyone in the room
// named `$socket->getSid()` but the sender. Please use the classic `$socket->emit()` instead.
// sending with acknowledgement
$reply = $socket->emit('question', 'do you think so?')->reply();
// sending without compression
$socket->compress(false)->emit('uncompressed', "that's rough");
$io = \Hyperf\Utils\ApplicationContext::getContainer()->get(\Hyperf\SocketIOServer\SocketIO::class);
// sending to all clients in 'game' room, including sender
$io->in('game')->emit('big-announcement', 'the game will start soon');
// sending to all clients in namespace 'myNamespace', including sender
$io->of('/myNamespace')->emit('bigger-announcement', 'the tournament will start soon');
// sending to a specific room in a specific namespace, including sender
$io->of('/myNamespace')->to('room')->emit('event', 'message');
// sending to individual socketid (private message)
$io->to('socketId')->emit('hey', 'I just met you');
// sending to all clients on this node (when using multiple nodes)
$io->local->emit('hi', 'my lovely babies');
// sending to all connected clients
$io->emit('an event sent to all connected clients');
};
```
## 进阶教程
### 设置 Socket.io 命名空间
Socket.io 通过自定义命名空间实现多路复用。(注意:不是 PHP 的命名空间)
1. 可以通过 `@SocketIONamespace("/xxx")` 将控制器映射为 xxx 的命名空间,
2. 也可通过
```php
<?php
use Hyperf\SocketIOServer\Collector\SocketIORouter;
use App\Controller\WebSocketController;
SocketIORouter::addNamespace('/xxx' , WebSocketController::class);
```
在路由中添加。
### 开启 Session
安装并配置好 hyperf/session 组件及其对应中间件,再通过 `SessionAspect` 切入 SocketIO 来使用 Session 。
```php
<?php
// config/autoload/aspect.php
return [
\Hyperf\SocketIOServer\Aspect\SessionAspect::class,
];
```
> swoole 4.4.17 及以下版本只能读取 http 创建好的Cookie4.4.18 及以上版本可以在WebSocket握手时创建Cookie
### 调整房间适配器
默认的房间功能通过 Redis 适配器实现,可以适应多进程乃至分布式场景。
1. 可以替换为内存适配器,只适用于单 worker 场景。
```php
<?php
// config/autoload/dependencies.php
return [
\Hyperf\SocketIOServer\Room\AdapterInterface::class => \Hyperf\SocketIOServer\Room\MemoryAdapter::class,
];
```
2. 可以替换为空适配器,不需要房间功能时可以降低消耗。
```php
<?php
// config/autoload/dependencies.php
return [
\Hyperf\SocketIOServer\Room\AdapterInterface::class => \Hyperf\SocketIOServer\Room\NullAdapter::class,
];
```
### 调整 SocketID (`sid`)
默认 SocketID 使用 `ServerID#FD` 的格式,可以适应分布式场景。
1. 可以替换为直接使用 Fd 。
```php
<?php
// config/autoload/dependencies.php
return [
\Hyperf\SocketIOServer\SidProvider\SidProviderInterface::class => \Hyperf\SocketIOServer\SidProvider\LocalSidProvider::class,
];
```
2. 也可以替换为 SessionID 。
```php
<?php
// config/autoload/dependencies.php
return [
\Hyperf\SocketIOServer\SidProvider\SidProviderInterface::class => \Hyperf\SocketIOServer\SidProvider\SessionSidProvider::class,
];
```
### 其他事件分发方法
1. 可以手动注册事件,不使用注解。
```php
<?php
declare(strict_types=1);
namespace App\Controller;
use Hyperf\SocketIOServer\BaseNamespace;
use Hyperf\SocketIOServer\SidProvider\SidProviderInterface;
use Hyperf\SocketIOServer\Socket;
use Hyperf\WebSocketServer\Sender;
class WebSocketController extends BaseNamespace
{
public function __construct(Sender $sender, SidProviderInterface $sidProvider) {
parent::__construct($sender,$sidProvider);
$this->on('event', [$this, 'echo']);
}
public function echo(Socket $socket, $data)
{
$socket->emit('event', $data);
}
}
```
2. 可以在控制器上添加 `@Event()` 注解,以方法名作为事件名来分发。此时应注意其他公有方法可能会和事件名冲突。
```php
<?php
declare(strict_types=1);
namespace App\Controller;
use Hyperf\SocketIOServer\Annotation\SocketIONamespace;
use Hyperf\SocketIOServer\Annotation\Event;
use Hyperf\SocketIOServer\BaseNamespace;
use Hyperf\SocketIOServer\Socket;
/**
* @SocketIONamespace("/")
* @Event()
*/
class WebSocketController extends BaseNamespace
{
public function echo(Socket $socket, $data)
{
$socket->emit('event', $data);
}
}
```

View File

@ -52,6 +52,7 @@
* [模型缓存](zh-cn/db/model-cache.md)
* [数据库迁移](zh-cn/db/migration.md)
* [极简 DB 组件](zh-cn/db/db.md)
* [修改器](zh-cn/db/mutators.md)
* 微服务
@ -83,6 +84,7 @@
* [ETCD 协程客户端](zh-cn/etcd.md)
* [WebSocket 服务](zh-cn/websocket-server.md)
* [WebSocket 协程客户端](zh-cn/websocket-client.md)
* [Socket.io 服务](zh-cn/socketio-server.md)
* [自定义进程](zh-cn/process.md)
* [开发者工具](zh-cn/devtool.md)
* [辅助类](zh-cn/utils.md)

View File

@ -209,3 +209,88 @@ class DemoConsumer extends ConsumerMessage
| \Hyperf\Amqp\Result::NACK | 消息沒有被正確消費掉,以 `basic_nack` 方法來響應 |
| \Hyperf\Amqp\Result::REQUEUE | 消息沒有被正確消費掉,以 `basic_reject` 方法來響應,並使消息重新入列 |
| \Hyperf\Amqp\Result::DROP | 消息沒有被正確消費掉,以 `basic_reject` 方法來響應 |
## RPC 遠程過程調用
除了典型的消息隊列場景,我們還可以通過 AMQP 來實現 RPC 遠程過程調用,本組件也為這個實現提供了對應的支持。
### 創建消費者
RPC 使用的消費者,與典型消息隊列場景的消費者實現基本無差,唯一的區別是需要通過調用 `reply` 方法返回數據給生產者。
```php
<?php
declare(strict_types=1);
namespace App\Amqp\Consumer;
use Hyperf\Amqp\Annotation\Consumer;
use Hyperf\Amqp\Message\ConsumerMessage;
use Hyperf\Amqp\Result;
use PhpAmqpLib\Message\AMQPMessage;
/**
* @Consumer(exchange="hyperf", routingKey="hyperf", queue="rpc.reply", name="ReplyConsumer", nums=1, enable=true)
*/
class ReplyConsumer extends ConsumerMessage
{
public function consumeMessage($data, AMQPMessage $message): string
{
$data['message'] .= 'Reply:' . $data['message'];
$this->reply($data, $message);
return Result::ACK;
}
}
```
### 發起 RPC 調用
作為生成者發起一次 RPC 遠程過程調用也非常的簡單,只需通過依賴注入容器獲得 `Hyperf\Amqp\RpcClient` 對象並調用其中的 `call` 方法即可,返回的結果是消費者 reply 的數據,如下所示:
```php
<?php
use Hyperf\Amqp\Message\DynamicRpcMessage;
use Hyperf\Amqp\RpcClient;
use Hyperf\Utils\ApplicationContext;
$rpcClient = ApplicationContext::getContainer()->get(RpcClient::class);
// 在 DynamicRpcMessage 上設置與 Consumer 一致的 Exchange 和 RoutingKey
$result = $rpcClient->call(new DynamicRpcMessage('hyperf', 'hyperf', ['message' => 'Hello Hyperf']));
// $result:
// array(1) {
// ["message"]=>
// string(18) "Reply:Hello Hyperf"
// }
```
### 抽象 RpcMessage
上面的 RPC 調用過程是直接通過 `Hyperf\Amqp\Message\DynamicRpcMessage` 類來完成 Exchange 和 RoutingKey 的定義,並傳遞消息數據,在生產項目的設計上,我們可以對 RpcMessage 進行一層抽象,以統一 Exchange 和 RoutingKey 的定義。
我們可以創建對應的 RpcMessage 類如 `App\Amqp\FooRpcMessage` 如下:
```php
<?php
use Hyperf\Amqp\Message\RpcMessage;
class FooRpcMessage extends RpcMessage
{
protected $exchange = 'hyperf';
protected $routingKey = 'hyperf';
public function __construct($data)
{
// 要傳遞數據
$this->payload = $data;
}
}
```
這樣我們進行 RPC 調用時,只需直接傳遞 `FooRpcMessage` 實例到 `call` 方法即可,無需每次調用時都去定義 Exchange 和 RoutingKey。

View File

@ -18,7 +18,7 @@ composer require hyperf/async-queue
|:----------------:|:---------:|:-------------------------------------------:|:---------------------------------------:|
| driver | string | Hyperf\AsyncQueue\Driver\RedisDriver::class | 無 |
| channel | string | queue | 隊列前綴 |
| redis.pool | string | default | redis連接池 |
| redis.pool | string | default | redis 連接池 |
| timeout | int | 2 | pop 消息的超時時間 |
| retry_seconds | int,array | 5 | 失敗後重新嘗試間隔 |
| handle_timeout | int | 10 | 消息處理超時時間 |

View File

@ -1,5 +1,59 @@
# 版本更新記錄
# v1.1.32 - 2020-05-21
## 修復
- [#1734](https://github.com/hyperf/hyperf/pull/1734) 修復模型多態查詢,關聯關係為空時,也會查詢 SQL 的問題;
- [#1739](https://github.com/hyperf/hyperf/pull/1739) 修復 `hyperf/filesystem` 組件 OSS HOOK 位運算錯誤,導致 resource 判斷不準確的問題;
- [#1743](https://github.com/hyperf/hyperf/pull/1743) 修復 `grafana.json` 中錯誤的`refId` 字段值;
- [#1748](https://github.com/hyperf/hyperf/pull/1748) 修復 `hyperf/amqp` 組件在使用其他連接池時,對應的 `concurrent.limit` 配置不生效的問題;
- [#1750](https://github.com/hyperf/hyperf/pull/1750) 修復連接池組件,在連接關閉失敗時會導致計數有誤的問題;
- [#1754](https://github.com/hyperf/hyperf/pull/1754) 修復 BASE Server 服務,啟動提示沒有考慮 UDP 服務的情況;
- [#1764](https://github.com/hyperf/hyperf/pull/1764) 修復當時間值為 null 時datatime 驗證器執行失敗的 BUG
- [#1769](https://github.com/hyperf/hyperf/pull/1769) 修復 `hyperf/socketio-server` 組件中,客户端初始化斷開連接操作時會報 Notice 的錯誤的問題;
## 新增
- [#1724](https://github.com/hyperf/hyperf/pull/1724) 新增模型方法 `Model::orWhereHasMorph` ,`Model::whereDoesntHaveMorph` and `Model::orWhereDoesntHaveMorph`
- [#1742](https://github.com/hyperf/hyperf/pull/1742) 新增模型 自定義類型轉換器 功能;
- 新增 interface `Castable`, `CastsAttributes``CastsInboundAttributes`
- 新增方法 `Model\Builder::withCasts`
- 新增方法 `Model::loadMorph`, `Model::loadMorphCount``Model::syncAttributes`
# v1.1.31 - 2020-05-14
## 新增
- [#1723](https://github.com/hyperf/hyperf/pull/1723) 異常處理器集成了 filp/whoops 。
- [#1730](https://github.com/hyperf/hyperf/pull/1730) 為命令 `gen:model` 可選項 `--refresh-fillable` 添加簡寫 `-R`
## 修復
- [#1696](https://github.com/hyperf/hyperf/pull/1696) 修復方法 `Context::copy` 傳入字段 `keys` 後無法正常使用的BUG。
- [#1708](https://github.com/hyperf/hyperf/pull/1708) [#1718](https://github.com/hyperf/hyperf/pull/1718) 修復 `hyperf/socketio-server` 組件內存溢出等BUG。
## 優化
- [#1710](https://github.com/hyperf/hyperf/pull/1710) MAC系統下不再使用 `cli_set_process_title` 方法設置進程名。
# v1.1.30 - 2020-05-07
## 新增
- [#1616](https://github.com/hyperf/hyperf/pull/1616) 新增 ORM 方法 `morphWith``whereHasMorph`
- [#1651](https://github.com/hyperf/hyperf/pull/1651) 新增 `socket.io-server` 組件。
- [#1666](https://github.com/hyperf/hyperf/pull/1666) [#1669](https://github.com/hyperf/hyperf/pull/1669) 新增 AMQP RPC 客户端。
## 修復
- [#1682](https://github.com/hyperf/hyperf/pull/1682) 修復 `RpcPoolTransporter` 的連接池配置不生效的 BUG。
- [#1683](https://github.com/hyperf/hyperf/pull/1683) 修復 `RpcConnection` 連接失敗後,相同協程內無法正常重置連接的 BUG。
## 優化
- [#1670](https://github.com/hyperf/hyperf/pull/1670) 優化掉 `Cache 組件` 一條無意義的刪除指令。
# v1.1.28 - 2020-04-30
## 新增
@ -26,16 +80,16 @@
## 新增
- [#1575](https://github.com/hyperf/hyperf/pull/1575) 為腳本 `gen:model` 生成的模型,自動添加 `relation` `scope``attributes` 的變量註釋。
- [#1586](https://github.com/hyperf/hyperf/pull/1586) 添加 `symfony/event-dispatcher` 組件小於 `4.3` 時的 `conflict` 配置。用於解決用户使用了 `4.3` 以下版本時,導致 `SymfonyDispatcher` 實現衝突的BUG。
- [#1586](https://github.com/hyperf/hyperf/pull/1586) 添加 `symfony/event-dispatcher` 組件小於 `4.3` 時的 `conflict` 配置。用於解決用户使用了 `4.3` 以下版本時,導致 `SymfonyDispatcher` 實現衝突的 BUG。
- [#1597](https://github.com/hyperf/hyperf/pull/1597) 為 `AMQP` 消費者,添加最大消費次數 `maxConsumption`
- [#1603](https://github.com/hyperf/hyperf/pull/1603) 為 `WebSocket` 服務添加基於 `fd` 存儲的 `Context`
## 修復
- [#1553](https://github.com/hyperf/hyperf/pull/1553) 修復 `jsonrpc` 服務,發佈了相同名字不同協議到 `consul`客户端無法正常工作的BUG。
- [#1589](https://github.com/hyperf/hyperf/pull/1589) 修復了文件鎖在協程下可能會造成死鎖的BUG。
- [#1607](https://github.com/hyperf/hyperf/pull/1607) 修復了重寫後的 `go` 方法,返回值與 `swoole` 原生方法不符的BUG。
- [#1624](https://github.com/hyperf/hyperf/pull/1624) 修復當路由 `Handler` 是匿名函數時,腳本 `describe:routes` 執行失敗的BUG。
- [#1553](https://github.com/hyperf/hyperf/pull/1553) 修復 `jsonrpc` 服務,發佈了相同名字不同協議到 `consul` 後,客户端無法正常工作的 BUG。
- [#1589](https://github.com/hyperf/hyperf/pull/1589) 修復了文件鎖在協程下可能會造成死鎖的 BUG。
- [#1607](https://github.com/hyperf/hyperf/pull/1607) 修復了重寫後的 `go` 方法,返回值與 `swoole` 原生方法不符的 BUG。
- [#1624](https://github.com/hyperf/hyperf/pull/1624) 修復當路由 `Handler` 是匿名函數時,腳本 `describe:routes` 執行失敗的 BUG。
# v1.1.26 - 2020-04-16
@ -47,9 +101,9 @@
- [#1563](https://github.com/hyperf/hyperf/pull/1563) 修復服務關停後,定時器的 `onOneServer` 配置不會被重置。
- [#1565](https://github.com/hyperf/hyperf/pull/1565) 當 `DB` 組件重連 `Mysql` 時,重置事務等級為 0。
- [#1572](https://github.com/hyperf/hyperf/pull/1572) 修復 `Hyperf\GrpcServer\CoreMiddleware`自定義類的父類找不到時報錯的BUG。
- [#1577](https://github.com/hyperf/hyperf/pull/1577) 修復 `describe:routes` 腳本 `server` 配置不生效的BUG。
- [#1579](https://github.com/hyperf/hyperf/pull/1579) 修復 `migrate:refresh` 腳本 `step` 參數不為 `int` 時會報錯的BUG。
- [#1572](https://github.com/hyperf/hyperf/pull/1572) 修復 `Hyperf\GrpcServer\CoreMiddleware` 中,自定義類的父類找不到時報錯的 BUG。
- [#1577](https://github.com/hyperf/hyperf/pull/1577) 修復 `describe:routes` 腳本 `server` 配置不生效的 BUG。
- [#1579](https://github.com/hyperf/hyperf/pull/1579) 修復 `migrate:refresh` 腳本 `step` 參數不為 `int` 時會報錯的 BUG。
## 變更

555
doc/zh-hk/db/mutators.md Normal file
View File

@ -0,0 +1,555 @@
# 修改器
> 本文檔大量借鑑於 [LearnKu](https://learnku.com) 十分感謝 LearnKu 對 PHP 社區做出的貢獻。
當你在模型實例中獲取或設置某些屬性值的時候,訪問器和修改器允許你對模型屬性值進行格式化。
## 訪問器 & 修改器
### 定義一個訪問器
若要定義一個訪問器, 則需在模型上創建一個 `getFooAttribute` 方法,要訪問的 `Foo` 字段需使用「駝峯式」命名。 在這個示例中,我們將為 `first_name` 屬性定義一個訪問器。當模型嘗試獲取 `first_name` 屬性時,將自動調用此訪問器:
```php
<?php
namespace App;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 獲取用户的姓名.
*
* @param string $value
* @return string
*/
public function getFirstNameAttribute($value)
{
return ucfirst($value);
}
}
```
如你所見,字段的原始值被傳遞到訪問器中,允許你對它進行處理並返回結果。如果想獲取被修改後的值,你可以在模型實例上訪問 `first_name` 屬性:
```php
$user = App\User::find(1);
$firstName = $user->first_name;
```
當然,你也可以通過已有的屬性值,使用訪問器返回新的計算值:
```php
namespace App;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 獲取用户的姓名.
*
* @return string
*/
public function getFullNameAttribute()
{
return "{$this->first_name} {$this->last_name}";
}
}
```
### 定義一個修改器
若要定義一個修改器,則需在模型上面定義 `setFooAttribute` 方法。要訪問的 `Foo` 字段使用「駝峯式」命名。讓我們再來定義一個 `first_name` 屬性的修改器。當我們嘗試在模式上在設置 `first_name` 屬性值時,該修改器將被自動調用:
```php
<?php
namespace App;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 設置用户的姓名.
*
* @param string $value
* @return void
*/
public function setFirstNameAttribute($value)
{
$this->attributes['first_name'] = strtolower($value);
}
}
```
修改器會獲取屬性已經被設置的值,允許你修改並且將其值設置到模型內部的 `$attributes` 屬性上。舉個例子,如果我們嘗試將 `first_name` 屬性的值設置為 `Sally`
```php
$user = App\User::find(1);
$user->first_name = 'Sally';
```
在這個例子中,`setFirstNameAttribute` 方法在調用的時候接受 `Sally` 這個值作為參數。接着修改器會應用 `strtolower` 函數並將處理的結果設置到內部的 `$attributes` 數組。
## 日期轉化器
默認情況下,模型會將 `created_at``updated_at` 字段轉換為 `Carbon` 實例,它繼承了 `PHP` 原生的 `DateTime` 類並提供了各種有用的方法。你可以通過設置模型的 `$dates` 屬性來添加其他日期屬性:
```php
<?php
namespace App;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 應該轉換為日期格式的屬性.
*
* @var array
*/
protected $dates = [
'seen_at',
];
}
```
> Tip: 你可以通過將模型的公有屬性 $timestamps 值設置為 false 來禁用默認的 created_at 和 updated_at 時間戳。
當某個字段是日期格式時,你可以將值設置為一個 `UNIX` 時間戳,日期時間 `(Y-m-d)` 字符串,或者 `DateTime` / `Carbon` 實例。日期值會被正確格式化並保存到你的數據庫中:
就如上面所説,當獲取到的屬性包含在 `$dates` 屬性中時,都會自動轉換為 `Carbon` 實例,允許你在屬性上使用任意的 `Carbon` 方法:
```php
$user = App\User::find(1);
return $user->deleted_at->getTimestamp();
```
### 時間格式
時間戳都將以 `Y-m-d H:i:s` 形式格式化。如果你需要自定義時間戳格式,可在模型中設置 `$dateFormat` 屬性。這個屬性決定了日期屬性將以何種形式保存在數據庫中,以及當模型序列化成數組或 `JSON` 時的格式:
```php
<?php
namespace App;
use Hyperf\DbConnection\Model\Model;
class Flight extends Model
{
/**
* 這個屬性應該被轉化為原生類型.
*
* @var string
*/
protected $dateFormat = 'U';
}
```
## 屬性類型轉換
模型中的 `$casts` 屬性提供了一個便利的方法來將屬性轉換為常見的數據類型。`$casts` 屬性應是一個數組,且數組的鍵是那些需要被轉換的屬性名稱,值則是你希望轉換的數據類型。
支持轉換的數據類型有:`integer`, `real`, `float`, `double`, `decimal:<digits>`, `string`, `boolean`, `object`, `array`, `collection`, `date`, `datetime``timestamp`。 當需要轉換為 `decimal` 類型時,你需要定義小數位的個數,如: `decimal:2`
示例, 讓我們把以整數( `0``1` )形式存儲在數據庫中的 `is_admin` 屬性轉成布爾值:
```php
<?php
namespace App;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'is_admin' => 'boolean',
];
}
```
現在當你訪問 `is_admin` 屬性時,雖然保存在數據庫裏的值是一個整數類型,但是返回值總是會被轉換成布爾值類型:
```php
$user = App\User::find(1);
if ($user->is_admin) {
//
}
```
### 自定義類型轉換
模型內置了多種常用的類型轉換。但是,用户偶爾會需要將數據轉換成自定義類型。現在,該需求可以通過定義一個實現 `CastsAttributes` 接口的類來完成
實現了該接口的類必須事先定義一個 `get` 和 `set` 方法。 `get` 方法負責將從數據庫中獲取的原始數據轉換成對應的類型,而 `set` 方法則是將數據轉換成對應的數據庫類型以便存入數據庫中。舉個例子,下面我們將內置的 `json` 類型轉換以自定義類型轉換的形式重新實現一遍:
```php
<?php
namespace App\Casts;
use Hyperf\Contract\CastsAttributes;
class Json implements CastsAttributes
{
/**
* 將取出的數據進行轉換
*/
public function get($model, $key, $value, $attributes)
{
return json_decode($value, true);
}
/**
* 轉換成將要進行存儲的值
*/
public function set($model, $key, $value, $attributes)
{
return json_encode($value);
}
}
```
定義好自定義類型轉換後,可以使用其類名稱將其附加到模型屬性:
```php
<?php
namespace App;
use App\Casts\Json;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 應進行類型轉換的屬性
*
* @var array
*/
protected $casts = [
'options' => Json::class,
];
}
```
#### 值對象類型轉換
你不僅可以將數據轉換成原生的數據類型,還可以將數據轉換成對象。兩種自定義類型轉換的定義方式非常類似。但是將數據轉換成對象的自定義轉換類中的 `set` 方法需要返回鍵值對數組,用於設置原始、可存儲的值到對應的模型中。
舉個例子,定義一個自定義類型轉換類用於將多個模型屬性值轉換成單個 `Address` 值對象,假設 `Address` 對象有兩個公有屬性 `lineOne``lineTwo`
```php
<?php
namespace App\Casts;
use App\Address;
use Hyperf\Contract\CastsAttributes;
class AddressCaster implements CastsAttributes
{
/**
* 將取出的數據進行轉換
* @return \App\Address
*/
public function get($model, $key, $value, $attributes)
{
return new Address(
$attributes['address_line_one'],
$attributes['address_line_two']
);
}
/**
* 轉換成將要進行存儲的值
*/
public function set($model, $key, $value, $attributes)
{
return [
'address_line_one' => $value->lineOne,
'address_line_two' => $value->lineTwo,
];
}
}
```
進行值對象類型轉換後,任何對值對象的數據變更將會自動在模型保存前同步回模型當中:
```php
<?php
$user = App\User::find(1);
$user->address->lineOne = 'Updated Address Value';
$user->address->lineTwo = '#10000';
$user->save();
var_dump($user->getAttributes());
//[
// 'address_line_one' => 'Updated Address Value',
// 'address_line_two' => '#10000'
//];
```
**這裏的實現與 Laravel 不同,如果出現以下用法,請需要格外注意**
```php
$user = App\User::find(1);
var_dump($user->getAttributes());
//[
// 'address_line_one' => 'Address Value',
// 'address_line_two' => '#10000'
//];
$user->address->lineOne = 'Updated Address Value';
$user->address->lineTwo = '#20000';
// 直接修改 address 的字段後,是無法立馬再 attributes 中生效的,但可以直接通過 $user->address 拿到修改後的數據。
var_dump($user->getAttributes());
//[
// 'address_line_one' => 'Address Value',
// 'address_line_two' => '#10000'
//];
// 當我們保存數據或者刪除數據後attributes 便會改成修改後的數據。
$user->save();
var_dump($user->getAttributes());
//[
// 'address_line_one' => 'Updated Address Value',
// 'address_line_two' => '#20000'
//];
```
如果修改 `address` 後,不想要保存,也不想通過 `address->lineOne` 獲取 `address_line_one` 的數據,還可以使用以下 方法
```php
$user = App\User::find(1);
$user->address->lineOne = 'Updated Address Value';
$user->syncAttributes();
var_dump($user->getAttributes());
```
當然,如果您仍然需要修改對應的 `value` 後,同步修改 `attributes` 的功能,可以嘗試使用以下方式。首先,我們實現一個 `UserInfo` 並繼承 `CastsValue`
```php
namespace App\Caster;
use Hyperf\Database\Model\CastsValue;
/**
* @property string $name
* @property int $gender
*/
class UserInfo extends CastsValue
{
}
```
然後實現對應的 `UserInfoCaster`
```php
<?php
declare(strict_types=1);
namespace App\Caster;
use Hyperf\Contract\CastsAttributes;
use Hyperf\Utils\Arr;
class UserInfoCaster implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes)
{
return new UserInfo($model, Arr::only($attributes, ['name', 'gender']));
}
public function set($model, string $key, $value, array $attributes)
{
return [
'name' => $value->name,
'gender' => $value->gender,
];
}
}
```
當我們再使用以下方式修改 UserInfo 時,便可以同步修改到 attributes 的數據。
```php
/** @var User $user */
$user = User::query()->find(100);
$user->userInfo->name = 'John1';
var_dump($user->getAttributes()); // ['name' => 'John1']
```
#### 入站類型轉換
有時候你可能只需要對寫入模型的屬性值進行類型轉換而不需要對從模型中獲取的屬性值進行任何處理。一個典型入站類型轉換的例子就是「hashing」。入站類型轉換類需要實現 `CastsInboundAttributes` 接口,只需要實現 `set` 方法。
```php
<?php
namespace App\Casts;
use Hyperf\Contract\CastsInboundAttributes;
class Hash implements CastsInboundAttributes
{
/**
* 哈希算法
*
* @var string
*/
protected $algorithm;
/**
* 創建一個新的類型轉換類實例
*/
public function __construct($algorithm = 'md5')
{
$this->algorithm = $algorithm;
}
/**
* 轉換成將要進行存儲的值
*/
public function set($model, $key, $value, $attributes)
{
return hash($this->algorithm, $value);
}
}
```
#### 類型轉換參數
當將自定義類型轉換附加到模型時,可以指定傳入的類型轉換參數。傳入類型轉換參數需使用 `:` 將參數與類名分隔,多個參數之間使用逗號分隔。這些參數將會傳遞到類型轉換類的構造函數中:
```php
<?php
namespace App;
use App\Casts\Json;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 應進行類型轉換的屬性
*
* @var array
*/
protected $casts = [
'secret' => Hash::class.':sha256',
];
}
```
### 數組 & `JSON` 轉換
當你在數據庫存儲序列化的 `JSON` 的數據時,`array` 類型的轉換非常有用。比如:如果你的數據庫具有被序列化為 `JSON``JSON``TEXT` 字段類型,並且在模型中加入了 `array` 類型轉換,那麼當你訪問的時候就會自動被轉換為 `PHP` 數組:
```php
<?php
namespace App;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 應進行類型轉換的屬性
*
* @var array
*/
protected $casts = [
'options' => 'array',
];
}
```
一旦定義了轉換,你訪問 `options` 屬性時他會自動從 `JSON` 類型反序列化為 `PHP` 數組。當你設置了 `options` 屬性的值時,給定的數組也會自動序列化為 `JSON` 類型存儲:
```php
$user = App\User::find(1);
$options = $user->options;
$options['key'] = 'value';
$user->options = $options;
$user->save();
```
### Date 類型轉換
當使用 `date``datetime` 屬性時,可以指定日期的格式。 這種格式會被用在模型序列化為數組或者 `JSON`
```php
<?php
namespace App;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 應進行類型轉換的屬性
*
* @var array
*/
protected $casts = [
'created_at' => 'datetime:Y-m-d',
];
}
```
### 查詢時類型轉換
有時候需要在查詢執行過程中對特定屬性進行類型轉換,例如需要從數據庫表中獲取數據的時候。舉個例子,請參考以下查詢:
```php
use App\Post;
use App\User;
$users = User::select([
'users.*',
'last_posted_at' => Post::selectRaw('MAX(created_at)')
->whereColumn('user_id', 'users.id')
])->get();
```
在該查詢獲取到的結果集中,`last_posted_at` 屬性將會是一個字符串。假如我們在執行查詢時進行 `date` 類型轉換將更方便。你可以通過使用 `withCasts` 方法來完成上述操作:
```php
$users = User::select([
'users.*',
'last_posted_at' => Post::selectRaw('MAX(created_at)')
->whereColumn('user_id', 'users.id')
])->withCasts([
'last_posted_at' => 'date'
])->get();
```

View File

@ -212,7 +212,6 @@ return $this->belongsToMany('App\Role')->wherePivot('approved', 1);
return $this->belongsToMany('App\Role')->wherePivotIn('priority', [1, 2]);
```
## 預加載
當以屬性方式訪問 `Hyperf` 關聯時,關聯數據「懶加載」。這着直到第一次訪問屬性時關聯數據才會被真實加載。不過 `Hyperf` 能在查詢父模型時「預先載入」子關聯。預加載可以緩解 N + 1 查詢問題。為了説明 N + 1 查詢問題,考慮 `User` 模型關聯到 `Role` 的情形:
@ -264,3 +263,249 @@ SELECT * FROM `user`;
SELECT * FROM `role` WHERE id in (1, 2, 3, ...);
```
## 多態關聯
多態關聯允許目標模型藉助關聯關係,關聯多個模型。
### 一對一(多態)
#### 表結構
一對一多態關聯與簡單的一對一關聯類似;不過,目標模型能夠在一個關聯上從屬於多個模型。
例如Book 和 User 可能共享一個關聯到 Image 模型的關係。使用一對一多態關聯允許使用一個唯一圖片列表同時用於 Book 和 User。讓我們先看看錶結構
```
book
id - integer
title - string
user
id - integer
name - string
image
id - integer
url - string
imageable_id - integer
imageable_type - string
```
image 表中的 imageable_id 字段會根據 imageable_type 的不同代表不同含義默認情況下imageable_type 直接是相關模型類名。
#### 模型示例
```php
<?php
namespace App\Model;
class Image extends Model
{
public function imageable()
{
return $this->morphTo();
}
}
class Book extends Model
{
public function image()
{
return $this->morphOne(Image::class, 'imageable');
}
}
class User extends Model
{
public function image()
{
return $this->morphOne(Image::class, 'imageable');
}
}
```
#### 獲取關聯
按照上述定義模型後,我們就可以通過模型關係獲取對應的模型。
比如,我們獲取某用户的圖片。
```php
use App\Model\User;
$user = User::find(1);
$image = $user->image;
```
或者我們獲取某個圖片對應用户或書本。`imageable` 會根據 `imageable_type` 獲取對應的 `User` 或者 `Book`
```php
use App\Model\Image;
$image = Image::find(1);
$imageable = $image->imageable;
```
### 一對多(多態)
#### 模型示例
```php
<?php
namespace App\Model;
class Image extends Model
{
public function imageable()
{
return $this->morphTo();
}
}
class Book extends Model
{
public function images()
{
return $this->morphMany(Image::class, 'imageable');
}
}
class User extends Model
{
public function images()
{
return $this->morphMany(Image::class, 'imageable');
}
}
```
#### 獲取關聯
獲取用户所有的圖片
```php
use App\Model\User;
$user = User::query()->find(1);
foreach ($user->images as $image) {
// ...
}
```
### 自定義多態映射
默認情況下,框架要求 `type` 必須存儲對應模型類名,比如上述 `imageable_type` 必須是對應的 `User::class``Book::class`,但顯然在實際應用中,這是十分不方便的。所以我們可以自定義映射關係,來解耦數據庫與應用內部結構。
```php
use App\Model;
use Hyperf\Database\Model\Relations\Relation;
Relation::morphMap([
'user' => Model\User::class,
'book' => Model\Book::class,
]);
```
因為 `Relation::morphMap` 修改後會常駐內存,所以我們可以在項目啟動時,就創建好對應的關係映射。我們可以創建以下監聽器:
```php
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Listener;
use App\Model;
use Hyperf\Database\Model\Relations\Relation;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Framework\Event\BootApplication;
/**
* @Listener
*/
class MorphMapRelationListener implements ListenerInterface
{
public function listen(): array
{
return [
BootApplication::class,
];
}
public function process(object $event)
{
Relation::morphMap([
'user' => Model\User::class,
'book' => Model\Book::class,
]);
}
}
```
### 嵌套預加載 `morphTo` 關聯
如果你希望加載一個 `morphTo` 關係,以及該關係可能返回的各種實體的嵌套關係,可以將 `with` 方法與 `morphTo` 關係的 `morphWith` 方法結合使用。
比如我們打算預加載 image 的 book.user 的關係。
```php
use App\Model\Book;
use App\Model\Image;
use Hyperf\Database\Model\Relations\MorphTo;
$images = Image::query()->with([
'imageable' => function (MorphTo $morphTo) {
$morphTo->morphWith([
Book::class => ['user'],
]);
},
])->get();
```
對應的SQL查詢如下
```sql
// 查詢所有圖片
select * from `images`;
// 查詢圖片對應的用户列表
select * from `user` where `user`.`id` in (1, 2);
// 查詢圖片對應的書本列表
select * from `book` where `book`.`id` in (1, 2, 3);
// 查詢書本列表對應的用户列表
select * from `user` where `user`.`id` in (1, 2);
```
### 多態關聯查詢
要查詢 `MorphTo` 關聯的存在,可以使用 `whereHasMorph` 方法及其相應的方法:
以下示例會查詢,書本或用户 `ID` 為 1 的圖片列表。
```php
use App\Model\Book;
use App\Model\Image;
use App\Model\User;
use Hyperf\Database\Model\Builder;
$images = Image::query()->whereHasMorph(
'imageable',
[
User::class,
Book::class,
],
function (Builder $query) {
$query->where('imageable_id', 1);
}
)->get();
```

View File

@ -106,6 +106,33 @@ class IndexController extends Controller
```
在上面這個例子,我們先假設 `FooException` 是存在的一個異常,以及假設已經完成了該處理器的配置,那麼當業務拋出一個沒有被捕獲處理的異常時,就會根據配置的順序依次傳遞,整一個處理流程可以理解為一個管道,若前一個異常處理器調用 `$this->stopPropagation()` 則不再往後傳遞,若最後一個配置的異常處理器仍不對該異常進行捕獲處理,那麼就會交由 Hyperf 的默認異常處理器處理了。
## 集成 Whoops
框架提供了 Whoops 集成。
首先安裝 Whoops
```php
composer require --dev filp/whoops
```
然後配置 Whoops 專用異常處理器。
```php
// config/autoload/exceptions.php
return [
'handler' => [
'http' => [
\Hyperf\ExceptionHandler\Handler\WhoopsExceptionHandler::class,
],
],
];
```
效果如圖:
![whoops](/imgs/whoops.png)
## Error 監聽器
框架提供了 `error_reporting()` 錯誤級別的監聽器 `Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler`

View File

@ -130,7 +130,7 @@ return [
1. S3 存儲請確認安裝 `hyperf/guzzle` 組件以提供協程化支持。阿里雲、七牛雲存儲請[開啟 Curl Hook](/zh-cn/coroutine?id=swoole-runtime-hook-level)來使用協程。因 Curl Hook 的參數支持性問題,請使用 Swoole 4.4.13 以上版本。
2. minIO, ceph radosgw 等私有對象存儲方案均支持 S3 協議,可以使用 S3 適配器。
3. 使用Local驅動時根目錄是配置好的地址而不是操作系統的根目錄。例如Local驅動 `root` 設置為 `/var/www`, 則本地磁盤上的 `/var/www/public/file.txt` 通過 flysystem API 訪問時應使用 `/public/file.txt``public/file.txt`
3. 使用 Local 驅動時根目錄是配置好的地址而不是操作系統的根目錄。例如Local 驅動 `root` 設置為 `/var/www`, 則本地磁盤上的 `/var/www/public/file.txt` 通過 flysystem API 訪問時應使用 `/public/file.txt``public/file.txt`
4. 以阿里雲 OSS 為例1 核 1 進程讀操作性能對比:
```bash
@ -239,7 +239,7 @@ return [
'accessKey' => env('QINIU_ACCESS_KEY'),
'secretKey' => env('QINIU_SECRET_KEY'),
'bucket' => env('QINIU_BUCKET'),
'domain' => env('QINBIU_DOMAIN'),
'domain' => env('QINIU_DOMAIN'),
],
],
];

BIN
doc/zh-hk/imgs/whoops.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 KiB

260
doc/zh-hk/nano.md Normal file
View File

@ -0,0 +1,260 @@
通過 `hyperf/nano` 可以在無骨架、零配置的情況下快速搭建 Hyperf 應用。
## 安裝
```php
composer install hyperf/nano
```
## 快速開始
```php
<?php
// index.php
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create('0.0.0.0', 9051);
$app->get('/', function () {
$user = $this->request->input('user', 'nano');
$method = $this->request->getMethod();
return [
'message' => "hello {$user}",
'method' => $method,
];
});
$app->run();
```
啟動:
```bash
php index.php start
```
簡潔如此。
## 特性
* 無骨架
* 零配置
* 快速啟動
* 閉包風格
* 支持註解外的全部 Hyperf 功能
* 兼容全部 Hyperf 組件
* Phar 友好
## 更多示例
### 路由
$app 集成了 Hyperf 路由器的所有方法。
```php
<?php
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->addGroup('/nano', function () use ($app) {
$app->addRoute(['GET', 'POST'], '/{id:\d+}', function($id) {
return '/nano/'.$id;
});
$app->put('/{name:.+}', function($name) {
return '/nano/'.$name;
});
});
$app->run();
```
### DI 容器
```php
<?php
use Hyperf\Nano\ContainerProxy;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
class Foo {
public function bar() {
return 'bar';
}
}
$app = AppFactory::create();
$app->getContainer()->set(Foo::class, new Foo());
$app->get('/', function () {
/** @var ContainerProxy $this */
$foo = $this->get(Foo::class);
return $foo->bar();
});
$app->run();
```
> 所有 $app 管理的閉包回調中,$this 都被綁定到了 `Hyperf\Nano\ContainerProxy` 上。
### 中間件
```php
<?php
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->get('/', function () {
return $this->request->getAttribute('key');
});
$app->addMiddleware(function ($request, $handler) {
$request = $request->withAttribute('key', 'value');
return $handler->handle($request);
});
$app->run();
```
> 除了閉包之外,所有 $app->addXXX() 方法還接受類名作為參數。可以傳入對應的 Hyperf 類。
### 異常處理
```php
<?php
use Hyperf\HttpMessage\Stream\SwooleStream;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->get('/', function () {
throw new \Exception();
});
$app->addExceptionHandler(function ($throwable, $response) {
return $response->withStatus('418')
->withBody(new SwooleStream('I\'m a teapot'));
});
$app->run();
```
### 命令行
```php
<?php
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->addCommand('echo', function(){
$this->get(StdoutLoggerInterface::class)->info('A new command called echo!');
});
$app->run();
```
執行
```bash
php index.php echo
```
### 事件監聽
```php
<?php
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Framework\Event\BootApplication;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->addListener(BootApplication::class, function($event){
$this->get(StdoutLoggerInterface::class)->info('App started');
});
$app->run();
```
### 自定義進程
```php
<?php
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->addProcess(function(){
while (true) {
sleep(1);
$this->get(StdoutLoggerInterface::class)->info('Processing...');
}
});
$app->run();
```
### 定時任務
```php
<?php
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->addCrontab('* * * * * *', function(){
$this->get(StdoutLoggerInterface::class)->info('execute every second!');
});
$app->run();
```
### 使用 Hyperf 組件.
```php
<?php
use Hyperf\DB\DB;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->config([
'db.default' => [
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', 3306),
'database' => env('DB_DATABASE', 'hyperf'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
]
]);
$app->get('/', function(){
return DB::query('SELECT * FROM `user` WHERE gender = ?;', [1]);
});
$app->run();
```

View File

@ -21,6 +21,7 @@ namespace App\Controller;
use Hyperf\HttpServer\Annotation\AutoController;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\Paginator\Paginator;
use Hyperf\Utils\Collection;
/**
* @AutoController()
@ -29,15 +30,20 @@ class UserController
{
public function index(RequestInterface $request)
{
$currentPage = $request->input('page', 1);
$perPage = $request->input('per_page', 2);
$users = [
$currentPage = (int) $request->input('page', 1);
$perPage = (int) $request->input('per_page', 2);
// 這裏根據 $currentPage 和 $perPage 進行數據查詢,以下使用 Collection 代替
$collection = new Collection([
['id' => 1, 'name' => 'Tom'],
['id' => 2, 'name' => 'Sam'],
['id' => 3, 'name' => 'Tim'],
['id' => 4, 'name' => 'Joe'],
];
return new Paginator($users, (int) $perPage, (int) $currentPage);
]);
$users = array_values($collection->forPage($currentPage, $perPage)->toArray());
return new Paginator($users, $perPage, $currentPage);
}
}
```

View File

@ -49,7 +49,6 @@ AbstractProvider::setGuzzleOptions([
<?php
! defined('SWOOLE_HOOK_FLAGS') && define('SWOOLE_HOOK_FLAGS', SWOOLE_HOOK_ALL | SWOOLE_HOOK_CURL);
```
## 如何使用 EasyWeChat
@ -78,6 +77,20 @@ $xml = $this->request->getBody()->getContents();
$app['request'] = new Request($get,$post,[],$cookie,$files,$server,$xml);
// Do something...
```
3. 服務器配置
如果需要使用微信公眾平台的服務器配置功能,可以使用以下代碼。
> 以下 `$response``Symfony\Component\HttpFoundation\Response` 並非 `Hyperf\HttpMessage\Server\Response`
> 所以只需將 `Body` 內容直接返回,即可通過微信驗證。
```php
$response = $app->server->serve();
return $response->getBody()->getContents();
```
## 如何替換緩存
@ -92,5 +105,4 @@ use EasyWeChat\Factory;
$app = Factory::miniProgram([]);
$app['cache'] = ApplicationContext::getContainer()->get(CacheInterface::class);
```

View File

@ -14,7 +14,7 @@ Session 組件的配置儲存於 `config/autoload/session.php` 文件中,如
## 配置 Session 中間件
在使用 Session 之前,您需要將 `Hyperf\Session\Middleware\SessionMiddleware` 中間件配置為 HTTP Server 的全局中間件,這樣組件才能介入到請求流程進行對應的處理,`config/autoload/middleware.php` 配置文件示例如下:
在使用 Session 之前,您需要將 `Hyperf\Session\Middleware\SessionMiddleware` 中間件配置為 HTTP Server 的全局中間件,這樣組件才能介入到請求流程進行對應的處理,`config/autoload/middlewares.php` 配置文件示例如下:
```php
<?php

View File

@ -66,7 +66,7 @@ return [
```
框架中使用 `Snowfalke` 十分簡單,只需要從 `DI` 中取出 `IdGeneratorInterface` 對象即可。
框架中使用 `Snowflake` 十分簡單,只需要從 `DI` 中取出 `IdGeneratorInterface` 對象即可。
```php
<?php

View File

@ -0,0 +1,290 @@
Socket.io是一款非常流行的應用層實時通訊協議和框架可以輕鬆實現應答、分組、廣播。hyperf/socketio-server支持了Socket.io的WebSocket傳輸協議。
## 安裝
```bash
composer require hyperf/socketio-server
```
hyperf/socketio-server 是基於WebSocket實現的請確保服務端已經添加了WebSocket服務配置。
```php
[
'name' => 'socket-io',
'type' => Server::SERVER_WEBSOCKET,
'host' => '0.0.0.0',
'port' => 9502,
'sock_type' => SWOOLE_SOCK_TCP,
'callbacks' => [
SwooleEvent::ON_HAND_SHAKE => [Hyperf\WebSocketServer\Server::class, 'onHandShake'],
SwooleEvent::ON_MESSAGE => [Hyperf\WebSocketServer\Server::class, 'onMessage'],
SwooleEvent::ON_CLOSE => [Hyperf\WebSocketServer\Server::class, 'onClose'],
],
],
```
## 快速開始
### 服務端
```php
<?php
declare(strict_types=1);
namespace App\Controller;
use Hyperf\SocketIOServer\Annotation\Event;
use Hyperf\SocketIOServer\Annotation\SocketIONamespace;
use Hyperf\SocketIOServer\BaseNamespace;
use Hyperf\SocketIOServer\Socket;
use Hyperf\Utils\Codec\Json;
/**
* @SocketIONamespace("/")
*/
class WebSocketController extends BaseNamespace
{
/**
* @Event("event")
* @param string $data
*/
public function onEvent(Socket $socket, $data)
{
// 應答
return 'Event Received: ' . $data;
}
/**
* @Event("join-room")
* @param string $data
*/
public function onJoinRoom(Socket $socket, $data)
{
// 將當前用户加入房間
$socket->join($data);
// 向房間內其他用户推送(不含當前用户)
$socket->to($data)->emit('event', $socket->getSid() . "has joined {$data}");
// 向房間內所有人廣播(含當前用户)
$this->emit('event', 'There are ' . count($socket->getAdapter()->clients($data)) . " players in {$data}");
}
/**
* @Event("say")
* @param string $data
*/
public function onSay(Socket $socket, $data)
{
$data = Json::decode($data);
$socket->to($data['room'])->emit('event', $socket->getSid() . " say: {$data['message']}");
}
}
```
> 每個 socket 會自動加入以自己 `sid` 命名的房間(`$socket->getSid()`),發送私聊信息就推送到對應 `sid` 即可。
> 框架會自動觸發 `connect``disconnect` 兩個事件。
### 客户端
由於服務端只實現了WebSocket通訊所以客户端要加上 `{transports:["websocket"]}`
```html
<script src="https://cdn.bootcss.com/socket.io/2.3.0/socket.io.js"></script>
<script>
var socket = io('ws://127.0.0.1:9502', { transports: ["websocket"] });
socket.on('connect', data => {
socket.emit('event', 'hello, hyperf', console.log);
socket.emit('join-room', 'room1', console.log);
setInterval(function () {
socket.emit('say', '{"room":"room1", "message":"Hello Hyperf."}');
}, 1000);
});
socket.on('event', console.log);
</script>
```
## API 清單
```php
<?php
function onConnect(\Hyperf\SocketIOServer\Socket $socket){
// sending to the client
$socket->emit('hello', 'can you hear me?', 1, 2, 'abc');
// sending to all clients except sender
$socket->broadcast->emit('broadcast', 'hello friends!');
// sending to all clients in 'game' room except sender
$socket->to('game')->emit('nice game', "let's play a game");
// sending to all clients in 'game1' and/or in 'game2' room, except sender
$socket->to('game1')->to('game2')->emit('nice game', "let's play a game (too)");
// WARNING: `$socket->to($socket->getSid())->emit()` will NOT work, as it will send to everyone in the room
// named `$socket->getSid()` but the sender. Please use the classic `$socket->emit()` instead.
// sending with acknowledgement
$reply = $socket->emit('question', 'do you think so?')->reply();
// sending without compression
$socket->compress(false)->emit('uncompressed', "that's rough");
$io = \Hyperf\Utils\ApplicationContext::getContainer()->get(\Hyperf\SocketIOServer\SocketIO::class);
// sending to all clients in 'game' room, including sender
$io->in('game')->emit('big-announcement', 'the game will start soon');
// sending to all clients in namespace 'myNamespace', including sender
$io->of('/myNamespace')->emit('bigger-announcement', 'the tournament will start soon');
// sending to a specific room in a specific namespace, including sender
$io->of('/myNamespace')->to('room')->emit('event', 'message');
// sending to individual socketid (private message)
$io->to('socketId')->emit('hey', 'I just met you');
// sending to all clients on this node (when using multiple nodes)
$io->local->emit('hi', 'my lovely babies');
// sending to all connected clients
$io->emit('an event sent to all connected clients');
};
```
## 進階教程
### 設置 Socket.io 命名空間
Socket.io 通過自定義命名空間實現多路複用。(注意:不是 PHP 的命名空間)
1. 可以通過 `@SocketIONamespace("/xxx")` 將控制器映射為 xxx 的命名空間,
2. 也可通過
```php
<?php
use Hyperf\SocketIOServer\Collector\SocketIORouter;
use App\Controller\WebSocketController;
SocketIORouter::addNamespace('/xxx' , WebSocketController::class);
```
在路由中添加。
### 開啟 Session
安裝並配置好 hyperf/session 組件及其對應中間件,再通過 `SessionAspect` 切入 SocketIO 來使用 Session 。
```php
<?php
// config/autoload/aspect.php
return [
\Hyperf\SocketIOServer\Aspect\SessionAspect::class,
];
```
> swoole 4.4.17 及以下版本只能讀取 http 創建好的Cookie4.4.18 及以上版本可以在WebSocket握手時創建Cookie
### 調整房間適配器
默認的房間功能通過 Redis 適配器實現,可以適應多進程乃至分佈式場景。
1. 可以替換為內存適配器,只適用於單 worker 場景。
```php
<?php
// config/autoload/dependencies.php
return [
\Hyperf\SocketIOServer\Room\AdapterInterface::class => \Hyperf\SocketIOServer\Room\MemoryAdapter::class,
];
```
2. 可以替換為空適配器,不需要房間功能時可以降低消耗。
```php
<?php
// config/autoload/dependencies.php
return [
\Hyperf\SocketIOServer\Room\AdapterInterface::class => \Hyperf\SocketIOServer\Room\NullAdapter::class,
];
```
### 調整 SocketID (`sid`)
默認 SocketID 使用 `ServerID#FD` 的格式,可以適應分佈式場景。
1. 可以替換為直接使用 Fd 。
```php
<?php
// config/autoload/dependencies.php
return [
\Hyperf\SocketIOServer\SidProvider\SidProviderInterface::class => \Hyperf\SocketIOServer\SidProvider\LocalSidProvider::class,
];
```
2. 也可以替換為 SessionID 。
```php
<?php
// config/autoload/dependencies.php
return [
\Hyperf\SocketIOServer\SidProvider\SidProviderInterface::class => \Hyperf\SocketIOServer\SidProvider\SessionSidProvider::class,
];
```
### 其他事件分發方法
1. 可以手動註冊事件,不使用註解。
```php
<?php
declare(strict_types=1);
namespace App\Controller;
use Hyperf\SocketIOServer\BaseNamespace;
use Hyperf\SocketIOServer\SidProvider\SidProviderInterface;
use Hyperf\SocketIOServer\Socket;
use Hyperf\WebSocketServer\Sender;
class WebSocketController extends BaseNamespace
{
public function __construct(Sender $sender, SidProviderInterface $sidProvider) {
parent::__construct($sender,$sidProvider);
$this->on('event', [$this, 'echo']);
}
public function echo(Socket $socket, $data)
{
$socket->emit('event', $data);
}
}
```
2. 可以在控制器上添加 `@Event()` 註解,以方法名作為事件名來分發。此時應注意其他公有方法可能會和事件名衝突。
```php
<?php
declare(strict_types=1);
namespace App\Controller;
use Hyperf\SocketIOServer\Annotation\SocketIONamespace;
use Hyperf\SocketIOServer\Annotation\Event;
use Hyperf\SocketIOServer\BaseNamespace;
use Hyperf\SocketIOServer\Socket;
/**
* @SocketIONamespace("/")
* @Event()
*/
class WebSocketController extends BaseNamespace
{
public function echo(Socket $socket, $data)
{
$socket->emit('event', $data);
}
}
```

View File

@ -52,6 +52,7 @@
* [模型緩存](zh-hk/db/model-cache.md)
* [數據庫遷移](zh-hk/db/migration.md)
* [極簡 DB 組件](zh-hk/db/db.md)
* [修改器](zh-hk/db/mutators.md)
* 微服務
@ -83,6 +84,7 @@
* [ETCD 協程客户端](zh-hk/etcd.md)
* [WebSocket 服務](zh-hk/websocket-server.md)
* [WebSocket 協程客户端](zh-hk/websocket-client.md)
* [Socket.io 服務](zh-hk/socketio-server.md)
* [自定義進程](zh-hk/process.md)
* [開發者工具](zh-hk/devtool.md)
* [輔助類](zh-hk/utils.md)

View File

@ -209,3 +209,88 @@ class DemoConsumer extends ConsumerMessage
| \Hyperf\Amqp\Result::NACK | 訊息沒有被正確消費掉,以 `basic_nack` 方法來響應 |
| \Hyperf\Amqp\Result::REQUEUE | 訊息沒有被正確消費掉,以 `basic_reject` 方法來響應,並使訊息重新入列 |
| \Hyperf\Amqp\Result::DROP | 訊息沒有被正確消費掉,以 `basic_reject` 方法來響應 |
## RPC 遠端過程呼叫
除了典型的訊息佇列場景,我們還可以通過 AMQP 來實現 RPC 遠端過程呼叫,本元件也為這個實現提供了對應的支援。
### 建立消費者
RPC 使用的消費者,與典型訊息佇列場景的消費者實現基本無差,唯一的區別是需要通過呼叫 `reply` 方法返回資料給生產者。
```php
<?php
declare(strict_types=1);
namespace App\Amqp\Consumer;
use Hyperf\Amqp\Annotation\Consumer;
use Hyperf\Amqp\Message\ConsumerMessage;
use Hyperf\Amqp\Result;
use PhpAmqpLib\Message\AMQPMessage;
/**
* @Consumer(exchange="hyperf", routingKey="hyperf", queue="rpc.reply", name="ReplyConsumer", nums=1, enable=true)
*/
class ReplyConsumer extends ConsumerMessage
{
public function consumeMessage($data, AMQPMessage $message): string
{
$data['message'] .= 'Reply:' . $data['message'];
$this->reply($data, $message);
return Result::ACK;
}
}
```
### 發起 RPC 呼叫
作為生成者發起一次 RPC 遠端過程呼叫也非常的簡單,只需通過依賴注入容器獲得 `Hyperf\Amqp\RpcClient` 物件並呼叫其中的 `call` 方法即可,返回的結果是消費者 reply 的資料,如下所示:
```php
<?php
use Hyperf\Amqp\Message\DynamicRpcMessage;
use Hyperf\Amqp\RpcClient;
use Hyperf\Utils\ApplicationContext;
$rpcClient = ApplicationContext::getContainer()->get(RpcClient::class);
// 在 DynamicRpcMessage 上設定與 Consumer 一致的 Exchange 和 RoutingKey
$result = $rpcClient->call(new DynamicRpcMessage('hyperf', 'hyperf', ['message' => 'Hello Hyperf']));
// $result:
// array(1) {
// ["message"]=>
// string(18) "Reply:Hello Hyperf"
// }
```
### 抽象 RpcMessage
上面的 RPC 呼叫過程是直接通過 `Hyperf\Amqp\Message\DynamicRpcMessage` 類來完成 Exchange 和 RoutingKey 的定義,並傳遞訊息資料,在生產專案的設計上,我們可以對 RpcMessage 進行一層抽象,以統一 Exchange 和 RoutingKey 的定義。
我們可以建立對應的 RpcMessage 類如 `App\Amqp\FooRpcMessage` 如下:
```php
<?php
use Hyperf\Amqp\Message\RpcMessage;
class FooRpcMessage extends RpcMessage
{
protected $exchange = 'hyperf';
protected $routingKey = 'hyperf';
public function __construct($data)
{
// 要傳遞資料
$this->payload = $data;
}
}
```
這樣我們進行 RPC 呼叫時,只需直接傳遞 `FooRpcMessage` 例項到 `call` 方法即可,無需每次呼叫時都去定義 Exchange 和 RoutingKey。

View File

@ -18,7 +18,7 @@ composer require hyperf/async-queue
|:----------------:|:---------:|:-------------------------------------------:|:---------------------------------------:|
| driver | string | Hyperf\AsyncQueue\Driver\RedisDriver::class | 無 |
| channel | string | queue | 佇列字首 |
| redis.pool | string | default | redis連線池 |
| redis.pool | string | default | redis 連線池 |
| timeout | int | 2 | pop 訊息的超時時間 |
| retry_seconds | int,array | 5 | 失敗後重新嘗試間隔 |
| handle_timeout | int | 10 | 訊息處理超時時間 |

View File

@ -1,5 +1,59 @@
# 版本更新記錄
# v1.1.32 - 2020-05-21
## 修復
- [#1734](https://github.com/hyperf/hyperf/pull/1734) 修復模型多型查詢,關聯關係為空時,也會查詢 SQL 的問題;
- [#1739](https://github.com/hyperf/hyperf/pull/1739) 修復 `hyperf/filesystem` 元件 OSS HOOK 位運算錯誤,導致 resource 判斷不準確的問題;
- [#1743](https://github.com/hyperf/hyperf/pull/1743) 修復 `grafana.json` 中錯誤的`refId` 欄位值;
- [#1748](https://github.com/hyperf/hyperf/pull/1748) 修復 `hyperf/amqp` 元件在使用其他連線池時,對應的 `concurrent.limit` 配置不生效的問題;
- [#1750](https://github.com/hyperf/hyperf/pull/1750) 修復連線池元件,在連線關閉失敗時會導致計數有誤的問題;
- [#1754](https://github.com/hyperf/hyperf/pull/1754) 修復 BASE Server 服務,啟動提示沒有考慮 UDP 服務的情況;
- [#1764](https://github.com/hyperf/hyperf/pull/1764) 修復當時間值為 null 時datatime 驗證器執行失敗的 BUG
- [#1769](https://github.com/hyperf/hyperf/pull/1769) 修復 `hyperf/socketio-server` 元件中,客戶端初始化斷開連線操作時會報 Notice 的錯誤的問題;
## 新增
- [#1724](https://github.com/hyperf/hyperf/pull/1724) 新增模型方法 `Model::orWhereHasMorph` ,`Model::whereDoesntHaveMorph` and `Model::orWhereDoesntHaveMorph`
- [#1742](https://github.com/hyperf/hyperf/pull/1742) 新增模型 自定義型別轉換器 功能;
- 新增 interface `Castable`, `CastsAttributes``CastsInboundAttributes`
- 新增方法 `Model\Builder::withCasts`
- 新增方法 `Model::loadMorph`, `Model::loadMorphCount``Model::syncAttributes`
# v1.1.31 - 2020-05-14
## 新增
- [#1723](https://github.com/hyperf/hyperf/pull/1723) 異常處理器集成了 filp/whoops 。
- [#1730](https://github.com/hyperf/hyperf/pull/1730) 為命令 `gen:model` 可選項 `--refresh-fillable` 新增簡寫 `-R`
## 修復
- [#1696](https://github.com/hyperf/hyperf/pull/1696) 修復方法 `Context::copy` 傳入欄位 `keys` 後無法正常使用的BUG。
- [#1708](https://github.com/hyperf/hyperf/pull/1708) [#1718](https://github.com/hyperf/hyperf/pull/1718) 修復 `hyperf/socketio-server` 元件記憶體溢位等BUG。
## 優化
- [#1710](https://github.com/hyperf/hyperf/pull/1710) MAC系統下不再使用 `cli_set_process_title` 方法設定程序名。
# v1.1.30 - 2020-05-07
## 新增
- [#1616](https://github.com/hyperf/hyperf/pull/1616) 新增 ORM 方法 `morphWith``whereHasMorph`
- [#1651](https://github.com/hyperf/hyperf/pull/1651) 新增 `socket.io-server` 元件。
- [#1666](https://github.com/hyperf/hyperf/pull/1666) [#1669](https://github.com/hyperf/hyperf/pull/1669) 新增 AMQP RPC 客戶端。
## 修復
- [#1682](https://github.com/hyperf/hyperf/pull/1682) 修復 `RpcPoolTransporter` 的連線池配置不生效的 BUG。
- [#1683](https://github.com/hyperf/hyperf/pull/1683) 修復 `RpcConnection` 連線失敗後,相同協程內無法正常重置連線的 BUG。
## 優化
- [#1670](https://github.com/hyperf/hyperf/pull/1670) 優化掉 `Cache 元件` 一條無意義的刪除指令。
# v1.1.28 - 2020-04-30
## 新增
@ -26,16 +80,16 @@
## 新增
- [#1575](https://github.com/hyperf/hyperf/pull/1575) 為指令碼 `gen:model` 生成的模型,自動新增 `relation` `scope``attributes` 的變數註釋。
- [#1586](https://github.com/hyperf/hyperf/pull/1586) 新增 `symfony/event-dispatcher` 元件小於 `4.3` 時的 `conflict` 配置。用於解決使用者使用了 `4.3` 以下版本時,導致 `SymfonyDispatcher` 實現衝突的BUG。
- [#1586](https://github.com/hyperf/hyperf/pull/1586) 新增 `symfony/event-dispatcher` 元件小於 `4.3` 時的 `conflict` 配置。用於解決使用者使用了 `4.3` 以下版本時,導致 `SymfonyDispatcher` 實現衝突的 BUG。
- [#1597](https://github.com/hyperf/hyperf/pull/1597) 為 `AMQP` 消費者,新增最大消費次數 `maxConsumption`
- [#1603](https://github.com/hyperf/hyperf/pull/1603) 為 `WebSocket` 服務新增基於 `fd` 儲存的 `Context`
## 修復
- [#1553](https://github.com/hyperf/hyperf/pull/1553) 修復 `jsonrpc` 服務,釋出了相同名字不同協議到 `consul`客戶端無法正常工作的BUG。
- [#1589](https://github.com/hyperf/hyperf/pull/1589) 修復了檔案鎖在協程下可能會造成死鎖的BUG。
- [#1607](https://github.com/hyperf/hyperf/pull/1607) 修復了重寫後的 `go` 方法,返回值與 `swoole` 原生方法不符的BUG。
- [#1624](https://github.com/hyperf/hyperf/pull/1624) 修復當路由 `Handler` 是匿名函式時,指令碼 `describe:routes` 執行失敗的BUG。
- [#1553](https://github.com/hyperf/hyperf/pull/1553) 修復 `jsonrpc` 服務,釋出了相同名字不同協議到 `consul` 後,客戶端無法正常工作的 BUG。
- [#1589](https://github.com/hyperf/hyperf/pull/1589) 修復了檔案鎖在協程下可能會造成死鎖的 BUG。
- [#1607](https://github.com/hyperf/hyperf/pull/1607) 修復了重寫後的 `go` 方法,返回值與 `swoole` 原生方法不符的 BUG。
- [#1624](https://github.com/hyperf/hyperf/pull/1624) 修復當路由 `Handler` 是匿名函式時,指令碼 `describe:routes` 執行失敗的 BUG。
# v1.1.26 - 2020-04-16
@ -47,9 +101,9 @@
- [#1563](https://github.com/hyperf/hyperf/pull/1563) 修復服務關停後,定時器的 `onOneServer` 配置不會被重置。
- [#1565](https://github.com/hyperf/hyperf/pull/1565) 當 `DB` 元件重連 `Mysql` 時,重置事務等級為 0。
- [#1572](https://github.com/hyperf/hyperf/pull/1572) 修復 `Hyperf\GrpcServer\CoreMiddleware`自定義類的父類找不到時報錯的BUG。
- [#1577](https://github.com/hyperf/hyperf/pull/1577) 修復 `describe:routes` 指令碼 `server` 配置不生效的BUG。
- [#1579](https://github.com/hyperf/hyperf/pull/1579) 修復 `migrate:refresh` 指令碼 `step` 引數不為 `int` 時會報錯的BUG。
- [#1572](https://github.com/hyperf/hyperf/pull/1572) 修復 `Hyperf\GrpcServer\CoreMiddleware` 中,自定義類的父類找不到時報錯的 BUG。
- [#1577](https://github.com/hyperf/hyperf/pull/1577) 修復 `describe:routes` 指令碼 `server` 配置不生效的 BUG。
- [#1579](https://github.com/hyperf/hyperf/pull/1579) 修復 `migrate:refresh` 指令碼 `step` 引數不為 `int` 時會報錯的 BUG。
## 變更

555
doc/zh-tw/db/mutators.md Normal file
View File

@ -0,0 +1,555 @@
# 修改器
> 本文件大量借鑑於 [LearnKu](https://learnku.com) 十分感謝 LearnKu 對 PHP 社群做出的貢獻。
當你在模型例項中獲取或設定某些屬性值的時候,訪問器和修改器允許你對模型屬性值進行格式化。
## 訪問器 & 修改器
### 定義一個訪問器
若要定義一個訪問器, 則需在模型上建立一個 `getFooAttribute` 方法,要訪問的 `Foo` 欄位需使用「駝峰式」命名。 在這個示例中,我們將為 `first_name` 屬性定義一個訪問器。當模型嘗試獲取 `first_name` 屬性時,將自動呼叫此訪問器:
```php
<?php
namespace App;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 獲取使用者的姓名.
*
* @param string $value
* @return string
*/
public function getFirstNameAttribute($value)
{
return ucfirst($value);
}
}
```
如你所見,欄位的原始值被傳遞到訪問器中,允許你對它進行處理並返回結果。如果想獲取被修改後的值,你可以在模型例項上訪問 `first_name` 屬性:
```php
$user = App\User::find(1);
$firstName = $user->first_name;
```
當然,你也可以通過已有的屬性值,使用訪問器返回新的計算值:
```php
namespace App;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 獲取使用者的姓名.
*
* @return string
*/
public function getFullNameAttribute()
{
return "{$this->first_name} {$this->last_name}";
}
}
```
### 定義一個修改器
若要定義一個修改器,則需在模型上面定義 `setFooAttribute` 方法。要訪問的 `Foo` 欄位使用「駝峰式」命名。讓我們再來定義一個 `first_name` 屬性的修改器。當我們嘗試在模式上在設定 `first_name` 屬性值時,該修改器將被自動呼叫:
```php
<?php
namespace App;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 設定使用者的姓名.
*
* @param string $value
* @return void
*/
public function setFirstNameAttribute($value)
{
$this->attributes['first_name'] = strtolower($value);
}
}
```
修改器會獲取屬性已經被設定的值,允許你修改並且將其值設定到模型內部的 `$attributes` 屬性上。舉個例子,如果我們嘗試將 `first_name` 屬性的值設定為 `Sally`
```php
$user = App\User::find(1);
$user->first_name = 'Sally';
```
在這個例子中,`setFirstNameAttribute` 方法在呼叫的時候接受 `Sally` 這個值作為引數。接著修改器會應用 `strtolower` 函式並將處理的結果設定到內部的 `$attributes` 陣列。
## 日期轉化器
預設情況下,模型會將 `created_at``updated_at` 欄位轉換為 `Carbon` 例項,它繼承了 `PHP` 原生的 `DateTime` 類並提供了各種有用的方法。你可以通過設定模型的 `$dates` 屬性來新增其他日期屬性:
```php
<?php
namespace App;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 應該轉換為日期格式的屬性.
*
* @var array
*/
protected $dates = [
'seen_at',
];
}
```
> Tip: 你可以通過將模型的公有屬性 $timestamps 值設定為 false 來禁用預設的 created_at 和 updated_at 時間戳。
當某個欄位是日期格式時,你可以將值設定為一個 `UNIX` 時間戳,日期時間 `(Y-m-d)` 字串,或者 `DateTime` / `Carbon` 例項。日期值會被正確格式化並儲存到你的資料庫中:
就如上面所說,當獲取到的屬性包含在 `$dates` 屬性中時,都會自動轉換為 `Carbon` 例項,允許你在屬性上使用任意的 `Carbon` 方法:
```php
$user = App\User::find(1);
return $user->deleted_at->getTimestamp();
```
### 時間格式
時間戳都將以 `Y-m-d H:i:s` 形式格式化。如果你需要自定義時間戳格式,可在模型中設定 `$dateFormat` 屬性。這個屬性決定了日期屬性將以何種形式儲存在資料庫中,以及當模型序列化成陣列或 `JSON` 時的格式:
```php
<?php
namespace App;
use Hyperf\DbConnection\Model\Model;
class Flight extends Model
{
/**
* 這個屬性應該被轉化為原生型別.
*
* @var string
*/
protected $dateFormat = 'U';
}
```
## 屬性型別轉換
模型中的 `$casts` 屬性提供了一個便利的方法來將屬性轉換為常見的資料型別。`$casts` 屬性應是一個數組,且陣列的鍵是那些需要被轉換的屬性名稱,值則是你希望轉換的資料型別。
支援轉換的資料型別有:`integer`, `real`, `float`, `double`, `decimal:<digits>`, `string`, `boolean`, `object`, `array`, `collection`, `date`, `datetime``timestamp`。 當需要轉換為 `decimal` 型別時,你需要定義小數位的個數,如: `decimal:2`
示例, 讓我們把以整數( `0``1` )形式儲存在資料庫中的 `is_admin` 屬性轉成布林值:
```php
<?php
namespace App;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'is_admin' => 'boolean',
];
}
```
現在當你訪問 `is_admin` 屬性時,雖然儲存在資料庫裡的值是一個整數型別,但是返回值總是會被轉換成布林值型別:
```php
$user = App\User::find(1);
if ($user->is_admin) {
//
}
```
### 自定義型別轉換
模型內建了多種常用的型別轉換。但是,使用者偶爾會需要將資料轉換成自定義型別。現在,該需求可以通過定義一個實現 `CastsAttributes` 介面的類來完成
實現了該介面的類必須事先定義一個 `get` 和 `set` 方法。 `get` 方法負責將從資料庫中獲取的原始資料轉換成對應的型別,而 `set` 方法則是將資料轉換成對應的資料庫型別以便存入資料庫中。舉個例子,下面我們將內建的 `json` 型別轉換以自定義型別轉換的形式重新實現一遍:
```php
<?php
namespace App\Casts;
use Hyperf\Contract\CastsAttributes;
class Json implements CastsAttributes
{
/**
* 將取出的資料進行轉換
*/
public function get($model, $key, $value, $attributes)
{
return json_decode($value, true);
}
/**
* 轉換成將要進行儲存的值
*/
public function set($model, $key, $value, $attributes)
{
return json_encode($value);
}
}
```
定義好自定義型別轉換後,可以使用其類名稱將其附加到模型屬性:
```php
<?php
namespace App;
use App\Casts\Json;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 應進行型別轉換的屬性
*
* @var array
*/
protected $casts = [
'options' => Json::class,
];
}
```
#### 值物件型別轉換
你不僅可以將資料轉換成原生的資料型別,還可以將資料轉換成物件。兩種自定義型別轉換的定義方式非常類似。但是將資料轉換成物件的自定義轉換類中的 `set` 方法需要返回鍵值對陣列,用於設定原始、可儲存的值到對應的模型中。
舉個例子,定義一個自定義型別轉換類用於將多個模型屬性值轉換成單個 `Address` 值物件,假設 `Address` 物件有兩個公有屬性 `lineOne``lineTwo`
```php
<?php
namespace App\Casts;
use App\Address;
use Hyperf\Contract\CastsAttributes;
class AddressCaster implements CastsAttributes
{
/**
* 將取出的資料進行轉換
* @return \App\Address
*/
public function get($model, $key, $value, $attributes)
{
return new Address(
$attributes['address_line_one'],
$attributes['address_line_two']
);
}
/**
* 轉換成將要進行儲存的值
*/
public function set($model, $key, $value, $attributes)
{
return [
'address_line_one' => $value->lineOne,
'address_line_two' => $value->lineTwo,
];
}
}
```
進行值物件型別轉換後,任何對值物件的資料變更將會自動在模型儲存前同步回模型當中:
```php
<?php
$user = App\User::find(1);
$user->address->lineOne = 'Updated Address Value';
$user->address->lineTwo = '#10000';
$user->save();
var_dump($user->getAttributes());
//[
// 'address_line_one' => 'Updated Address Value',
// 'address_line_two' => '#10000'
//];
```
**這裡的實現與 Laravel 不同,如果出現以下用法,請需要格外注意**
```php
$user = App\User::find(1);
var_dump($user->getAttributes());
//[
// 'address_line_one' => 'Address Value',
// 'address_line_two' => '#10000'
//];
$user->address->lineOne = 'Updated Address Value';
$user->address->lineTwo = '#20000';
// 直接修改 address 的欄位後,是無法立馬再 attributes 中生效的,但可以直接通過 $user->address 拿到修改後的資料。
var_dump($user->getAttributes());
//[
// 'address_line_one' => 'Address Value',
// 'address_line_two' => '#10000'
//];
// 當我們儲存資料或者刪除資料後attributes 便會改成修改後的資料。
$user->save();
var_dump($user->getAttributes());
//[
// 'address_line_one' => 'Updated Address Value',
// 'address_line_two' => '#20000'
//];
```
如果修改 `address` 後,不想要儲存,也不想通過 `address->lineOne` 獲取 `address_line_one` 的資料,還可以使用以下 方法
```php
$user = App\User::find(1);
$user->address->lineOne = 'Updated Address Value';
$user->syncAttributes();
var_dump($user->getAttributes());
```
當然,如果您仍然需要修改對應的 `value` 後,同步修改 `attributes` 的功能,可以嘗試使用以下方式。首先,我們實現一個 `UserInfo` 並繼承 `CastsValue`
```php
namespace App\Caster;
use Hyperf\Database\Model\CastsValue;
/**
* @property string $name
* @property int $gender
*/
class UserInfo extends CastsValue
{
}
```
然後實現對應的 `UserInfoCaster`
```php
<?php
declare(strict_types=1);
namespace App\Caster;
use Hyperf\Contract\CastsAttributes;
use Hyperf\Utils\Arr;
class UserInfoCaster implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes)
{
return new UserInfo($model, Arr::only($attributes, ['name', 'gender']));
}
public function set($model, string $key, $value, array $attributes)
{
return [
'name' => $value->name,
'gender' => $value->gender,
];
}
}
```
當我們再使用以下方式修改 UserInfo 時,便可以同步修改到 attributes 的資料。
```php
/** @var User $user */
$user = User::query()->find(100);
$user->userInfo->name = 'John1';
var_dump($user->getAttributes()); // ['name' => 'John1']
```
#### 入站型別轉換
有時候你可能只需要對寫入模型的屬性值進行型別轉換而不需要對從模型中獲取的屬性值進行任何處理。一個典型入站型別轉換的例子就是「hashing」。入站型別轉換類需要實現 `CastsInboundAttributes` 介面,只需要實現 `set` 方法。
```php
<?php
namespace App\Casts;
use Hyperf\Contract\CastsInboundAttributes;
class Hash implements CastsInboundAttributes
{
/**
* 雜湊演算法
*
* @var string
*/
protected $algorithm;
/**
* 建立一個新的型別轉換類例項
*/
public function __construct($algorithm = 'md5')
{
$this->algorithm = $algorithm;
}
/**
* 轉換成將要進行儲存的值
*/
public function set($model, $key, $value, $attributes)
{
return hash($this->algorithm, $value);
}
}
```
#### 型別轉換引數
當將自定義型別轉換附加到模型時,可以指定傳入的型別轉換引數。傳入型別轉換引數需使用 `:` 將引數與類名分隔,多個引數之間使用逗號分隔。這些引數將會傳遞到型別轉換類的建構函式中:
```php
<?php
namespace App;
use App\Casts\Json;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 應進行型別轉換的屬性
*
* @var array
*/
protected $casts = [
'secret' => Hash::class.':sha256',
];
}
```
### 陣列 & `JSON` 轉換
當你在資料庫儲存序列化的 `JSON` 的資料時,`array` 型別的轉換非常有用。比如:如果你的資料庫具有被序列化為 `JSON``JSON``TEXT` 欄位型別,並且在模型中加入了 `array` 型別轉換,那麼當你訪問的時候就會自動被轉換為 `PHP` 陣列:
```php
<?php
namespace App;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 應進行型別轉換的屬性
*
* @var array
*/
protected $casts = [
'options' => 'array',
];
}
```
一旦定義了轉換,你訪問 `options` 屬性時他會自動從 `JSON` 型別反序列化為 `PHP` 陣列。當你設定了 `options` 屬性的值時,給定的陣列也會自動序列化為 `JSON` 型別儲存:
```php
$user = App\User::find(1);
$options = $user->options;
$options['key'] = 'value';
$user->options = $options;
$user->save();
```
### Date 型別轉換
當使用 `date``datetime` 屬性時,可以指定日期的格式。 這種格式會被用在模型序列化為陣列或者 `JSON`
```php
<?php
namespace App;
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
/**
* 應進行型別轉換的屬性
*
* @var array
*/
protected $casts = [
'created_at' => 'datetime:Y-m-d',
];
}
```
### 查詢時型別轉換
有時候需要在查詢執行過程中對特定屬性進行型別轉換,例如需要從資料庫表中獲取資料的時候。舉個例子,請參考以下查詢:
```php
use App\Post;
use App\User;
$users = User::select([
'users.*',
'last_posted_at' => Post::selectRaw('MAX(created_at)')
->whereColumn('user_id', 'users.id')
])->get();
```
在該查詢獲取到的結果集中,`last_posted_at` 屬性將會是一個字串。假如我們在執行查詢時進行 `date` 型別轉換將更方便。你可以通過使用 `withCasts` 方法來完成上述操作:
```php
$users = User::select([
'users.*',
'last_posted_at' => Post::selectRaw('MAX(created_at)')
->whereColumn('user_id', 'users.id')
])->withCasts([
'last_posted_at' => 'date'
])->get();
```

View File

@ -212,7 +212,6 @@ return $this->belongsToMany('App\Role')->wherePivot('approved', 1);
return $this->belongsToMany('App\Role')->wherePivotIn('priority', [1, 2]);
```
## 預載入
當以屬性方式訪問 `Hyperf` 關聯時,關聯資料「懶載入」。這著直到第一次訪問屬性時關聯資料才會被真實載入。不過 `Hyperf` 能在查詢父模型時「預先載入」子關聯。預載入可以緩解 N + 1 查詢問題。為了說明 N + 1 查詢問題,考慮 `User` 模型關聯到 `Role` 的情形:
@ -264,3 +263,249 @@ SELECT * FROM `user`;
SELECT * FROM `role` WHERE id in (1, 2, 3, ...);
```
## 多型關聯
多型關聯允許目標模型藉助關聯關係,關聯多個模型。
### 一對一(多型)
#### 表結構
一對一多型關聯與簡單的一對一關聯類似;不過,目標模型能夠在一個關聯上從屬於多個模型。
例如Book 和 User 可能共享一個關聯到 Image 模型的關係。使用一對一多型關聯允許使用一個唯一圖片列表同時用於 Book 和 User。讓我們先看看錶結構
```
book
id - integer
title - string
user
id - integer
name - string
image
id - integer
url - string
imageable_id - integer
imageable_type - string
```
image 表中的 imageable_id 欄位會根據 imageable_type 的不同代表不同含義預設情況下imageable_type 直接是相關模型類名。
#### 模型示例
```php
<?php
namespace App\Model;
class Image extends Model
{
public function imageable()
{
return $this->morphTo();
}
}
class Book extends Model
{
public function image()
{
return $this->morphOne(Image::class, 'imageable');
}
}
class User extends Model
{
public function image()
{
return $this->morphOne(Image::class, 'imageable');
}
}
```
#### 獲取關聯
按照上述定義模型後,我們就可以通過模型關係獲取對應的模型。
比如,我們獲取某使用者的圖片。
```php
use App\Model\User;
$user = User::find(1);
$image = $user->image;
```
或者我們獲取某個圖片對應使用者或書本。`imageable` 會根據 `imageable_type` 獲取對應的 `User` 或者 `Book`
```php
use App\Model\Image;
$image = Image::find(1);
$imageable = $image->imageable;
```
### 一對多(多型)
#### 模型示例
```php
<?php
namespace App\Model;
class Image extends Model
{
public function imageable()
{
return $this->morphTo();
}
}
class Book extends Model
{
public function images()
{
return $this->morphMany(Image::class, 'imageable');
}
}
class User extends Model
{
public function images()
{
return $this->morphMany(Image::class, 'imageable');
}
}
```
#### 獲取關聯
獲取使用者所有的圖片
```php
use App\Model\User;
$user = User::query()->find(1);
foreach ($user->images as $image) {
// ...
}
```
### 自定義多型對映
預設情況下,框架要求 `type` 必須儲存對應模型類名,比如上述 `imageable_type` 必須是對應的 `User::class``Book::class`,但顯然在實際應用中,這是十分不方便的。所以我們可以自定義對映關係,來解耦資料庫與應用內部結構。
```php
use App\Model;
use Hyperf\Database\Model\Relations\Relation;
Relation::morphMap([
'user' => Model\User::class,
'book' => Model\Book::class,
]);
```
因為 `Relation::morphMap` 修改後會常駐記憶體,所以我們可以在專案啟動時,就建立好對應的關係對映。我們可以建立以下監聽器:
```php
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Listener;
use App\Model;
use Hyperf\Database\Model\Relations\Relation;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Framework\Event\BootApplication;
/**
* @Listener
*/
class MorphMapRelationListener implements ListenerInterface
{
public function listen(): array
{
return [
BootApplication::class,
];
}
public function process(object $event)
{
Relation::morphMap([
'user' => Model\User::class,
'book' => Model\Book::class,
]);
}
}
```
### 巢狀預載入 `morphTo` 關聯
如果你希望載入一個 `morphTo` 關係,以及該關係可能返回的各種實體的巢狀關係,可以將 `with` 方法與 `morphTo` 關係的 `morphWith` 方法結合使用。
比如我們打算預載入 image 的 book.user 的關係。
```php
use App\Model\Book;
use App\Model\Image;
use Hyperf\Database\Model\Relations\MorphTo;
$images = Image::query()->with([
'imageable' => function (MorphTo $morphTo) {
$morphTo->morphWith([
Book::class => ['user'],
]);
},
])->get();
```
對應的SQL查詢如下
```sql
// 查詢所有圖片
select * from `images`;
// 查詢圖片對應的使用者列表
select * from `user` where `user`.`id` in (1, 2);
// 查詢圖片對應的書本列表
select * from `book` where `book`.`id` in (1, 2, 3);
// 查詢書本列表對應的使用者列表
select * from `user` where `user`.`id` in (1, 2);
```
### 多型關聯查詢
要查詢 `MorphTo` 關聯的存在,可以使用 `whereHasMorph` 方法及其相應的方法:
以下示例會查詢,書本或使用者 `ID` 為 1 的圖片列表。
```php
use App\Model\Book;
use App\Model\Image;
use App\Model\User;
use Hyperf\Database\Model\Builder;
$images = Image::query()->whereHasMorph(
'imageable',
[
User::class,
Book::class,
],
function (Builder $query) {
$query->where('imageable_id', 1);
}
)->get();
```

View File

@ -106,6 +106,33 @@ class IndexController extends Controller
```
在上面這個例子,我們先假設 `FooException` 是存在的一個異常,以及假設已經完成了該處理器的配置,那麼當業務丟擲一個沒有被捕獲處理的異常時,就會根據配置的順序依次傳遞,整一個處理流程可以理解為一個管道,若前一個異常處理器呼叫 `$this->stopPropagation()` 則不再往後傳遞,若最後一個配置的異常處理器仍不對該異常進行捕獲處理,那麼就會交由 Hyperf 的預設異常處理器處理了。
## 整合 Whoops
框架提供了 Whoops 整合。
首先安裝 Whoops
```php
composer require --dev filp/whoops
```
然後配置 Whoops 專用異常處理器。
```php
// config/autoload/exceptions.php
return [
'handler' => [
'http' => [
\Hyperf\ExceptionHandler\Handler\WhoopsExceptionHandler::class,
],
],
];
```
效果如圖:
![whoops](/imgs/whoops.png)
## Error 監聽器
框架提供了 `error_reporting()` 錯誤級別的監聽器 `Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler`

View File

@ -130,7 +130,7 @@ return [
1. S3 儲存請確認安裝 `hyperf/guzzle` 元件以提供協程化支援。阿里雲、七牛雲端儲存請[開啟 Curl Hook](/zh-cn/coroutine?id=swoole-runtime-hook-level)來使用協程。因 Curl Hook 的引數支援性問題,請使用 Swoole 4.4.13 以上版本。
2. minIO, ceph radosgw 等私有物件儲存方案均支援 S3 協議,可以使用 S3 介面卡。
3. 使用Local驅動時根目錄是配置好的地址而不是作業系統的根目錄。例如Local驅動 `root` 設定為 `/var/www`, 則本地磁碟上的 `/var/www/public/file.txt` 通過 flysystem API 訪問時應使用 `/public/file.txt``public/file.txt`
3. 使用 Local 驅動時根目錄是配置好的地址而不是作業系統的根目錄。例如Local 驅動 `root` 設定為 `/var/www`, 則本地磁碟上的 `/var/www/public/file.txt` 通過 flysystem API 訪問時應使用 `/public/file.txt``public/file.txt`
4. 以阿里雲 OSS 為例1 核 1 程序讀操作效能對比:
```bash
@ -239,7 +239,7 @@ return [
'accessKey' => env('QINIU_ACCESS_KEY'),
'secretKey' => env('QINIU_SECRET_KEY'),
'bucket' => env('QINIU_BUCKET'),
'domain' => env('QINBIU_DOMAIN'),
'domain' => env('QINIU_DOMAIN'),
],
],
];

BIN
doc/zh-tw/imgs/whoops.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 KiB

260
doc/zh-tw/nano.md Normal file
View File

@ -0,0 +1,260 @@
通過 `hyperf/nano` 可以在無骨架、零配置的情況下快速搭建 Hyperf 應用。
## 安裝
```php
composer install hyperf/nano
```
## 快速開始
```php
<?php
// index.php
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create('0.0.0.0', 9051);
$app->get('/', function () {
$user = $this->request->input('user', 'nano');
$method = $this->request->getMethod();
return [
'message' => "hello {$user}",
'method' => $method,
];
});
$app->run();
```
啟動:
```bash
php index.php start
```
簡潔如此。
## 特性
* 無骨架
* 零配置
* 快速啟動
* 閉包風格
* 支援註解外的全部 Hyperf 功能
* 相容全部 Hyperf 元件
* Phar 友好
## 更多示例
### 路由
$app 集成了 Hyperf 路由器的所有方法。
```php
<?php
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->addGroup('/nano', function () use ($app) {
$app->addRoute(['GET', 'POST'], '/{id:\d+}', function($id) {
return '/nano/'.$id;
});
$app->put('/{name:.+}', function($name) {
return '/nano/'.$name;
});
});
$app->run();
```
### DI 容器
```php
<?php
use Hyperf\Nano\ContainerProxy;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
class Foo {
public function bar() {
return 'bar';
}
}
$app = AppFactory::create();
$app->getContainer()->set(Foo::class, new Foo());
$app->get('/', function () {
/** @var ContainerProxy $this */
$foo = $this->get(Foo::class);
return $foo->bar();
});
$app->run();
```
> 所有 $app 管理的閉包回撥中,$this 都被繫結到了 `Hyperf\Nano\ContainerProxy` 上。
### 中介軟體
```php
<?php
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->get('/', function () {
return $this->request->getAttribute('key');
});
$app->addMiddleware(function ($request, $handler) {
$request = $request->withAttribute('key', 'value');
return $handler->handle($request);
});
$app->run();
```
> 除了閉包之外,所有 $app->addXXX() 方法還接受類名作為引數。可以傳入對應的 Hyperf 類。
### 異常處理
```php
<?php
use Hyperf\HttpMessage\Stream\SwooleStream;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->get('/', function () {
throw new \Exception();
});
$app->addExceptionHandler(function ($throwable, $response) {
return $response->withStatus('418')
->withBody(new SwooleStream('I\'m a teapot'));
});
$app->run();
```
### 命令列
```php
<?php
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->addCommand('echo', function(){
$this->get(StdoutLoggerInterface::class)->info('A new command called echo!');
});
$app->run();
```
執行
```bash
php index.php echo
```
### 事件監聽
```php
<?php
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Framework\Event\BootApplication;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->addListener(BootApplication::class, function($event){
$this->get(StdoutLoggerInterface::class)->info('App started');
});
$app->run();
```
### 自定義程序
```php
<?php
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->addProcess(function(){
while (true) {
sleep(1);
$this->get(StdoutLoggerInterface::class)->info('Processing...');
}
});
$app->run();
```
### 定時任務
```php
<?php
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->addCrontab('* * * * * *', function(){
$this->get(StdoutLoggerInterface::class)->info('execute every second!');
});
$app->run();
```
### 使用 Hyperf 元件.
```php
<?php
use Hyperf\DB\DB;
use Hyperf\Nano\Factory\AppFactory;
require_once __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
$app->config([
'db.default' => [
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', 3306),
'database' => env('DB_DATABASE', 'hyperf'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
]
]);
$app->get('/', function(){
return DB::query('SELECT * FROM `user` WHERE gender = ?;', [1]);
});
$app->run();
```

View File

@ -21,6 +21,7 @@ namespace App\Controller;
use Hyperf\HttpServer\Annotation\AutoController;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\Paginator\Paginator;
use Hyperf\Utils\Collection;
/**
* @AutoController()
@ -29,15 +30,20 @@ class UserController
{
public function index(RequestInterface $request)
{
$currentPage = $request->input('page', 1);
$perPage = $request->input('per_page', 2);
$users = [
$currentPage = (int) $request->input('page', 1);
$perPage = (int) $request->input('per_page', 2);
// 這裡根據 $currentPage 和 $perPage 進行資料查詢,以下使用 Collection 代替
$collection = new Collection([
['id' => 1, 'name' => 'Tom'],
['id' => 2, 'name' => 'Sam'],
['id' => 3, 'name' => 'Tim'],
['id' => 4, 'name' => 'Joe'],
];
return new Paginator($users, (int) $perPage, (int) $currentPage);
]);
$users = array_values($collection->forPage($currentPage, $perPage)->toArray());
return new Paginator($users, $perPage, $currentPage);
}
}
```

View File

@ -49,7 +49,6 @@ AbstractProvider::setGuzzleOptions([
<?php
! defined('SWOOLE_HOOK_FLAGS') && define('SWOOLE_HOOK_FLAGS', SWOOLE_HOOK_ALL | SWOOLE_HOOK_CURL);
```
## 如何使用 EasyWeChat
@ -78,6 +77,20 @@ $xml = $this->request->getBody()->getContents();
$app['request'] = new Request($get,$post,[],$cookie,$files,$server,$xml);
// Do something...
```
3. 伺服器配置
如果需要使用微信公眾平臺的伺服器配置功能,可以使用以下程式碼。
> 以下 `$response``Symfony\Component\HttpFoundation\Response` 並非 `Hyperf\HttpMessage\Server\Response`
> 所以只需將 `Body` 內容直接返回,即可通過微信驗證。
```php
$response = $app->server->serve();
return $response->getBody()->getContents();
```
## 如何替換快取
@ -92,5 +105,4 @@ use EasyWeChat\Factory;
$app = Factory::miniProgram([]);
$app['cache'] = ApplicationContext::getContainer()->get(CacheInterface::class);
```

View File

@ -14,7 +14,7 @@ Session 元件的配置儲存於 `config/autoload/session.php` 檔案中,如
## 配置 Session 中介軟體
在使用 Session 之前,您需要將 `Hyperf\Session\Middleware\SessionMiddleware` 中介軟體配置為 HTTP Server 的全域性中介軟體,這樣元件才能介入到請求流程進行對應的處理,`config/autoload/middleware.php` 配置檔案示例如下:
在使用 Session 之前,您需要將 `Hyperf\Session\Middleware\SessionMiddleware` 中介軟體配置為 HTTP Server 的全域性中介軟體,這樣元件才能介入到請求流程進行對應的處理,`config/autoload/middlewares.php` 配置檔案示例如下:
```php
<?php

View File

@ -66,7 +66,7 @@ return [
```
框架中使用 `Snowfalke` 十分簡單,只需要從 `DI` 中取出 `IdGeneratorInterface` 物件即可。
框架中使用 `Snowflake` 十分簡單,只需要從 `DI` 中取出 `IdGeneratorInterface` 物件即可。
```php
<?php

View File

@ -0,0 +1,290 @@
Socket.io是一款非常流行的應用層實時通訊協議和框架可以輕鬆實現應答、分組、廣播。hyperf/socketio-server支援了Socket.io的WebSocket傳輸協議。
## 安裝
```bash
composer require hyperf/socketio-server
```
hyperf/socketio-server 是基於WebSocket實現的請確保服務端已經添加了WebSocket服務配置。
```php
[
'name' => 'socket-io',
'type' => Server::SERVER_WEBSOCKET,
'host' => '0.0.0.0',
'port' => 9502,
'sock_type' => SWOOLE_SOCK_TCP,
'callbacks' => [
SwooleEvent::ON_HAND_SHAKE => [Hyperf\WebSocketServer\Server::class, 'onHandShake'],
SwooleEvent::ON_MESSAGE => [Hyperf\WebSocketServer\Server::class, 'onMessage'],
SwooleEvent::ON_CLOSE => [Hyperf\WebSocketServer\Server::class, 'onClose'],
],
],
```
## 快速開始
### 服務端
```php
<?php
declare(strict_types=1);
namespace App\Controller;
use Hyperf\SocketIOServer\Annotation\Event;
use Hyperf\SocketIOServer\Annotation\SocketIONamespace;
use Hyperf\SocketIOServer\BaseNamespace;
use Hyperf\SocketIOServer\Socket;
use Hyperf\Utils\Codec\Json;
/**
* @SocketIONamespace("/")
*/
class WebSocketController extends BaseNamespace
{
/**
* @Event("event")
* @param string $data
*/
public function onEvent(Socket $socket, $data)
{
// 應答
return 'Event Received: ' . $data;
}
/**
* @Event("join-room")
* @param string $data
*/
public function onJoinRoom(Socket $socket, $data)
{
// 將當前使用者加入房間
$socket->join($data);
// 向房間內其他使用者推送(不含當前使用者)
$socket->to($data)->emit('event', $socket->getSid() . "has joined {$data}");
// 向房間內所有人廣播(含當前使用者)
$this->emit('event', 'There are ' . count($socket->getAdapter()->clients($data)) . " players in {$data}");
}
/**
* @Event("say")
* @param string $data
*/
public function onSay(Socket $socket, $data)
{
$data = Json::decode($data);
$socket->to($data['room'])->emit('event', $socket->getSid() . " say: {$data['message']}");
}
}
```
> 每個 socket 會自動加入以自己 `sid` 命名的房間(`$socket->getSid()`),傳送私聊資訊就推送到對應 `sid` 即可。
> 框架會自動觸發 `connect``disconnect` 兩個事件。
### 客戶端
由於服務端只實現了WebSocket通訊所以客戶端要加上 `{transports:["websocket"]}`
```html
<script src="https://cdn.bootcss.com/socket.io/2.3.0/socket.io.js"></script>
<script>
var socket = io('ws://127.0.0.1:9502', { transports: ["websocket"] });
socket.on('connect', data => {
socket.emit('event', 'hello, hyperf', console.log);
socket.emit('join-room', 'room1', console.log);
setInterval(function () {
socket.emit('say', '{"room":"room1", "message":"Hello Hyperf."}');
}, 1000);
});
socket.on('event', console.log);
</script>
```
## API 清單
```php
<?php
function onConnect(\Hyperf\SocketIOServer\Socket $socket){
// sending to the client
$socket->emit('hello', 'can you hear me?', 1, 2, 'abc');
// sending to all clients except sender
$socket->broadcast->emit('broadcast', 'hello friends!');
// sending to all clients in 'game' room except sender
$socket->to('game')->emit('nice game', "let's play a game");
// sending to all clients in 'game1' and/or in 'game2' room, except sender
$socket->to('game1')->to('game2')->emit('nice game', "let's play a game (too)");
// WARNING: `$socket->to($socket->getSid())->emit()` will NOT work, as it will send to everyone in the room
// named `$socket->getSid()` but the sender. Please use the classic `$socket->emit()` instead.
// sending with acknowledgement
$reply = $socket->emit('question', 'do you think so?')->reply();
// sending without compression
$socket->compress(false)->emit('uncompressed', "that's rough");
$io = \Hyperf\Utils\ApplicationContext::getContainer()->get(\Hyperf\SocketIOServer\SocketIO::class);
// sending to all clients in 'game' room, including sender
$io->in('game')->emit('big-announcement', 'the game will start soon');
// sending to all clients in namespace 'myNamespace', including sender
$io->of('/myNamespace')->emit('bigger-announcement', 'the tournament will start soon');
// sending to a specific room in a specific namespace, including sender
$io->of('/myNamespace')->to('room')->emit('event', 'message');
// sending to individual socketid (private message)
$io->to('socketId')->emit('hey', 'I just met you');
// sending to all clients on this node (when using multiple nodes)
$io->local->emit('hi', 'my lovely babies');
// sending to all connected clients
$io->emit('an event sent to all connected clients');
};
```
## 進階教程
### 設定 Socket.io 名稱空間
Socket.io 通過自定義名稱空間實現多路複用。(注意:不是 PHP 的名稱空間)
1. 可以通過 `@SocketIONamespace("/xxx")` 將控制器對映為 xxx 的名稱空間,
2. 也可通過
```php
<?php
use Hyperf\SocketIOServer\Collector\SocketIORouter;
use App\Controller\WebSocketController;
SocketIORouter::addNamespace('/xxx' , WebSocketController::class);
```
在路由中新增。
### 開啟 Session
安裝並配置好 hyperf/session 元件及其對應中介軟體,再通過 `SessionAspect` 切入 SocketIO 來使用 Session 。
```php
<?php
// config/autoload/aspect.php
return [
\Hyperf\SocketIOServer\Aspect\SessionAspect::class,
];
```
> swoole 4.4.17 及以下版本只能讀取 http 建立好的Cookie4.4.18 及以上版本可以在WebSocket握手時建立Cookie
### 調整房間介面卡
預設的房間功能通過 Redis 介面卡實現,可以適應多程序乃至分散式場景。
1. 可以替換為記憶體介面卡,只適用於單 worker 場景。
```php
<?php
// config/autoload/dependencies.php
return [
\Hyperf\SocketIOServer\Room\AdapterInterface::class => \Hyperf\SocketIOServer\Room\MemoryAdapter::class,
];
```
2. 可以替換為空介面卡,不需要房間功能時可以降低消耗。
```php
<?php
// config/autoload/dependencies.php
return [
\Hyperf\SocketIOServer\Room\AdapterInterface::class => \Hyperf\SocketIOServer\Room\NullAdapter::class,
];
```
### 調整 SocketID (`sid`)
預設 SocketID 使用 `ServerID#FD` 的格式,可以適應分散式場景。
1. 可以替換為直接使用 Fd 。
```php
<?php
// config/autoload/dependencies.php
return [
\Hyperf\SocketIOServer\SidProvider\SidProviderInterface::class => \Hyperf\SocketIOServer\SidProvider\LocalSidProvider::class,
];
```
2. 也可以替換為 SessionID 。
```php
<?php
// config/autoload/dependencies.php
return [
\Hyperf\SocketIOServer\SidProvider\SidProviderInterface::class => \Hyperf\SocketIOServer\SidProvider\SessionSidProvider::class,
];
```
### 其他事件分發方法
1. 可以手動註冊事件,不使用註解。
```php
<?php
declare(strict_types=1);
namespace App\Controller;
use Hyperf\SocketIOServer\BaseNamespace;
use Hyperf\SocketIOServer\SidProvider\SidProviderInterface;
use Hyperf\SocketIOServer\Socket;
use Hyperf\WebSocketServer\Sender;
class WebSocketController extends BaseNamespace
{
public function __construct(Sender $sender, SidProviderInterface $sidProvider) {
parent::__construct($sender,$sidProvider);
$this->on('event', [$this, 'echo']);
}
public function echo(Socket $socket, $data)
{
$socket->emit('event', $data);
}
}
```
2. 可以在控制器上新增 `@Event()` 註解,以方法名作為事件名來分發。此時應注意其他公有方法可能會和事件名衝突。
```php
<?php
declare(strict_types=1);
namespace App\Controller;
use Hyperf\SocketIOServer\Annotation\SocketIONamespace;
use Hyperf\SocketIOServer\Annotation\Event;
use Hyperf\SocketIOServer\BaseNamespace;
use Hyperf\SocketIOServer\Socket;
/**
* @SocketIONamespace("/")
* @Event()
*/
class WebSocketController extends BaseNamespace
{
public function echo(Socket $socket, $data)
{
$socket->emit('event', $data);
}
}
```

View File

@ -52,6 +52,7 @@
* [模型快取](zh-tw/db/model-cache.md)
* [資料庫遷移](zh-tw/db/migration.md)
* [極簡 DB 元件](zh-tw/db/db.md)
* [修改器](zh-tw/db/mutators.md)
* 微服務
@ -83,6 +84,7 @@
* [ETCD 協程客戶端](zh-tw/etcd.md)
* [WebSocket 服務](zh-tw/websocket-server.md)
* [WebSocket 協程客戶端](zh-tw/websocket-client.md)
* [Socket.io 服務](zh-tw/socketio-server.md)
* [自定義程序](zh-tw/process.md)
* [開發者工具](zh-tw/devtool.md)
* [輔助類](zh-tw/utils.md)

View File

@ -49,6 +49,7 @@
<directory suffix="Test.php">./src/session/tests</directory>
<directory suffix="Test.php">./src/snowflake/tests</directory>
<directory suffix="Test.php">./src/socket/tests</directory>
<directory suffix="Test.php">./src/socketio-server/tests</directory>
<directory suffix="Test.php">./src/super-globals/tests</directory>
<directory suffix="Test.php">./src/task/tests</directory>
<directory suffix="Test.php">./src/testing/tests</directory>

View File

@ -20,6 +20,10 @@ use Psr\Container\ContainerInterface;
class Builder
{
/**
* @deprecated v2.0
* @var string
*/
protected $name = 'default';
/**

View File

@ -66,7 +66,7 @@ class Consumer extends Builder
$channel = $connection->getConfirmChannel();
$this->declare($consumerMessage, $channel);
$concurrent = $this->getConcurrent();
$concurrent = $this->getConcurrent($consumerMessage->getPoolName());
$maxConsumption = $consumerMessage->getMaxConsumption();
$currentConsumption = 0;
@ -137,10 +137,10 @@ class Consumer extends Builder
}
}
protected function getConcurrent(): ?Concurrent
protected function getConcurrent(string $pool): ?Concurrent
{
$config = $this->container->get(ConfigInterface::class);
$concurrent = (int) $config->get('amqp.' . $this->name . '.concurrent.limit', 0);
$concurrent = (int) $config->get('amqp.' . $pool . '.concurrent.limit', 0);
if ($concurrent > 1) {
return new Concurrent($concurrent);
}

View File

@ -36,7 +36,7 @@ class RpcClient extends Builder
$body = $connection->getAMQPMessage($timeout)->getBody();
return $rpcMessage->unserialize($body);
} finally {
$connection && $connection->release();
isset($connection) && $connection->release();
}
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Amqp;
use Hyperf\Amqp\Consumer;
use Hyperf\Amqp\Pool\PoolFactory;
use Hyperf\Utils\Coroutine\Concurrent;
use HyperfTest\Amqp\Stub\ContainerStub;
use Mockery;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
/**
* @internal
* @coversNothing
*/
class ConsumerTest extends TestCase
{
public function testConsumerConcurrentLimit()
{
$container = ContainerStub::getContainer();
$consumer = new Consumer($container, Mockery::mock(PoolFactory::class), Mockery::mock(LoggerInterface::class));
$ref = new \ReflectionClass($consumer);
$method = $ref->getMethod('getConcurrent');
$method->setAccessible(true);
/** @var Concurrent $concurrent */
$concurrent = $method->invokeArgs($consumer, ['default']);
$this->assertSame(10, $concurrent->getLimit());
/** @var Concurrent $concurrent */
$concurrent = $method->invokeArgs($consumer, ['co']);
$this->assertSame(5, $concurrent->getLimit());
}
}

View File

@ -13,6 +13,8 @@ namespace HyperfTest\Amqp\Stub;
use Hyperf\Amqp\Consumer;
use Hyperf\Amqp\Pool\PoolFactory;
use Hyperf\Config\Config;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Di\Container;
use Hyperf\Utils\ApplicationContext;
@ -45,6 +47,22 @@ class ContainerStub
$container->shouldReceive('get')->with(Consumer::class)->andReturnUsing(function () use ($container) {
return new Consumer($container, $container->get(PoolFactory::class), $container->get(StdoutLoggerInterface::class));
});
$container->shouldReceive('get')->with(ConfigInterface::class)->andReturnUsing(function () {
return new Config([
'amqp' => [
'default' => [
'concurrent' => [
'limit' => 10,
],
],
'co' => [
'concurrent' => [
'limit' => 5,
],
],
],
]);
});
return $container;
}

View File

@ -159,12 +159,26 @@ abstract class Command extends SymfonyCommand
return $this->output->askQuestion($question);
}
/**
* Give the user a multiple choice from an array of answers.
*/
public function choiceMultiple(
string $question,
array $choices,
$default = null,
?int $attempts = null
): array {
$question = new ChoiceQuestion($question, $choices, $default);
$question->setMaxAttempts($attempts)->setMultiselect(true);
return $this->output->askQuestion($question);
}
/**
* Give the user a single choice from an array of answers.
*
* @param null|mixed $default
* @param null|mixed $attempts
* @param null|mixed $multiple
* @param null|bool $multiple Deprecated: use choiceMultiple method instead.
*/
public function choice(
string $question,
@ -173,11 +187,7 @@ abstract class Command extends SymfonyCommand
$attempts = null,
$multiple = null
): string {
$question = new ChoiceQuestion($question, $choices, $default);
$question->setMaxAttempts($attempts)->setMultiselect($multiple);
return $this->output->askQuestion($question);
return $this->choiceMultiple($question, $choices, $default, $attempts)[0];
}
/**

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
interface Castable
{
/**
* Get the name of the caster class to use when casting from / to this cast target.
*
* @return CastsAttributes|CastsInboundAttributes|string
*/
public static function castUsing();
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
interface CastsAttributes
{
/**
* Transform the attribute from the underlying model values.
*
* @param object $model
* @param mixed $value
* @return mixed
*/
public function get($model, string $key, $value, array $attributes);
/**
* Transform the attribute to its underlying model values.
*
* @param object $model
* @param mixed $value
* @return array|string
*/
public function set($model, string $key, $value, array $attributes);
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
interface CastsInboundAttributes
{
/**
* Transform the attribute to its underlying model values.
*
* @param object $model
* @param mixed $value
* @return array
*/
public function set($model, string $key, $value, array $attributes);
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Contract;
interface Synchronized
{
/**
* Whether the data has been synchronized.
*/
public function isSynchronized(): bool;
}

View File

@ -11,6 +11,9 @@ declare(strict_types=1);
*/
namespace Hyperf\Database\Commands\Ast;
use Hyperf\Contract\Castable;
use Hyperf\Contract\CastsAttributes;
use Hyperf\Contract\CastsInboundAttributes;
use Hyperf\Database\Commands\ModelOption;
use Hyperf\Database\Model\Builder;
use Hyperf\Database\Model\Collection;
@ -53,7 +56,7 @@ class ModelUpdateVisitor extends NodeVisitorAbstract
];
/**
* @var string
* @var Model
*/
protected $class;
@ -91,7 +94,7 @@ class ModelUpdateVisitor extends NodeVisitorAbstract
public function __construct($class, $columns, ModelOption $option)
{
$this->class = $class;
$this->class = new $class();
$this->columns = $columns;
$this->option = $option;
$this->initPropertiesFromMethods();
@ -129,9 +132,32 @@ class ModelUpdateVisitor extends NodeVisitorAbstract
protected function rewriteCasts(Node\Stmt\PropertyProperty $node): Node\Stmt\PropertyProperty
{
$items = [];
$keys = [];
if ($node->default instanceof Node\Expr\Array_) {
$items = $node->default->items;
}
if ($this->option->isForceCasts()) {
$items = [];
$casts = $this->class->getCasts();
foreach ($node->default->items as $item) {
$caster = $this->class->getCasts()[$item->key->value] ?? null;
if ($caster && $this->isCaster($caster)) {
$items[] = $item;
}
}
}
foreach ($items as $item) {
$keys[] = $item->key->value;
}
foreach ($this->columns as $column) {
$name = $column['column_name'];
$type = $column['cast'] ?? null;
if (in_array($name, $keys)) {
continue;
}
if ($type || $type = $this->formatDatabaseType($column['data_type'])) {
$items[] = new Node\Expr\ArrayItem(
new Node\Scalar\String_($type),
@ -146,6 +172,16 @@ class ModelUpdateVisitor extends NodeVisitorAbstract
return $node;
}
/**
* @param object|string $caster
*/
protected function isCaster($caster): bool
{
return is_subclass_of($caster, CastsAttributes::class) ||
is_subclass_of($caster, Castable::class) ||
is_subclass_of($caster, CastsInboundAttributes::class);
}
protected function parseProperty(): string
{
$doc = '/**' . PHP_EOL;
@ -174,12 +210,10 @@ class ModelUpdateVisitor extends NodeVisitorAbstract
protected function initPropertiesFromMethods()
{
/** @var ReflectionClass $reflection */
$reflection = self::getReflector()->reflect($this->class);
$reflection = self::getReflector()->reflect(get_class($this->class));
$methods = $reflection->getImmediateMethods();
$namespace = $reflection->getDeclaringNamespaceAst();
if (empty($methods)) {
return;
}
$casts = $this->class->getCasts();
sort($methods);
/** @var ReflectionMethod $method */
@ -251,6 +285,26 @@ class ModelUpdateVisitor extends NodeVisitorAbstract
}
}
}
// The custom caster.
foreach ($casts as $key => $caster) {
if (is_subclass_of($caster, Castable::class)) {
$caster = $caster::castUsing();
}
if (is_subclass_of($caster, CastsAttributes::class)) {
$ref = self::getReflector()->reflect($caster);
$method = $ref->getMethod('get');
if ($ast = $method->getReturnStatementsAst()[0]) {
if ($ast instanceof Node\Stmt\Return_
&& $ast->expr instanceof Node\Expr\New_
&& $ast->expr->class instanceof Node\Name\FullyQualified
) {
$this->setProperty($key, [$ast->expr->class->toCodeString()], true, true);
}
}
}
}
}
protected function setProperty(string $name, array $type = null, bool $read = null, bool $write = null, string $comment = '', bool $nullable = false)

View File

@ -119,7 +119,7 @@ class ModelCommand extends Command
$this->addOption('prefix', 'P', InputOption::VALUE_OPTIONAL, 'What prefix that you want the Model set.');
$this->addOption('inheritance', 'i', InputOption::VALUE_OPTIONAL, 'The inheritance that you want the Model extends.');
$this->addOption('uses', 'U', InputOption::VALUE_OPTIONAL, 'The default class uses of the Model.');
$this->addOption('refresh-fillable', null, InputOption::VALUE_NONE, 'Whether generate fillable argement for model.');
$this->addOption('refresh-fillable', 'R', InputOption::VALUE_NONE, 'Whether generate fillable argement for model.');
$this->addOption('table-mapping', 'M', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Table mappings for model.');
$this->addOption('ignore-tables', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Ignore tables for creating models.');
$this->addOption('with-comments', null, InputOption::VALUE_NONE, 'Whether generate the property comments for model.');

View File

@ -975,6 +975,19 @@ class Builder
);
}
/**
* Apply query-time casts to the model instance.
*
* @param array $casts
* @return $this
*/
public function withCasts($casts)
{
$this->model->mergeCasts($casts);
return $this;
}
/**
* Get the underlying query builder instance.
*

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Database\Model;
use Hyperf\Contract\Synchronized;
use Hyperf\Utils\Contracts\Arrayable;
abstract class CastsValue implements Synchronized, Arrayable
{
/**
* @var Model
*/
protected $model;
/**
* @var array
*/
protected $items;
/**
* @var bool
*/
protected $isSynchronized;
public function __construct(Model $model, $itmes = [])
{
$this->model = $model;
$this->items = $itmes;
}
public function __get($name)
{
return $this->items[$name];
}
public function __set($name, $value)
{
$this->items[$name] = $value;
$this->isSynchronized = false;
$this->model->syncAttributes();
$this->isSynchronized = true;
}
public function isSynchronized(): bool
{
return $this->isSynchronized;
}
public function toArray(): array
{
return $this->items;
}
}

View File

@ -14,6 +14,10 @@ namespace Hyperf\Database\Model\Concerns;
use Carbon\Carbon;
use Carbon\CarbonInterface;
use DateTimeInterface;
use Hyperf\Contract\Castable;
use Hyperf\Contract\CastsAttributes;
use Hyperf\Contract\CastsInboundAttributes;
use Hyperf\Contract\Synchronized;
use Hyperf\Database\Model\JsonEncodingException;
use Hyperf\Database\Model\Relations\Relation;
use Hyperf\Utils\Arr;
@ -53,12 +57,44 @@ trait HasAttributes
protected $changes = [];
/**
* The attributes that should be cast to native types.
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [];
/**
* The attributes that have been cast using custom classes.
*
* @var array
*/
protected $classCastCache = [];
/**
* The built-in, primitive cast types supported.
*
* @var array
*/
protected static $primitiveCastTypes = [
'array',
'bool',
'boolean',
'collection',
'custom_datetime',
'date',
'datetime',
'decimal',
'double',
'float',
'int',
'integer',
'json',
'object',
'real',
'string',
'timestamp',
];
/**
* The attributes that should be mutated to dates.
*
@ -182,8 +218,9 @@ trait HasAttributes
// If the attribute exists in the attribute array or has a "get" mutator we will
// get the attribute's value. Otherwise, we will proceed as if the developers
// are asking for a relationship's value. This covers both types of values.
if (array_key_exists($key, $this->attributes) ||
$this->hasGetMutator($key)) {
if (array_key_exists($key, $this->getAttributes()) ||
$this->hasGetMutator($key) ||
$this->isClassCastable($key)) {
return $this->getAttributeValue($key);
}
@ -204,31 +241,7 @@ trait HasAttributes
*/
public function getAttributeValue($key)
{
$value = $this->getAttributeFromArray($key);
// If the attribute has a get mutator, we will call that then return what
// it returns as the value, which is useful for transforming values on
// retrieval from the model to a form that is more useful for usage.
if ($this->hasGetMutator($key)) {
return $this->mutateAttribute($key, $value);
}
// If the attribute exists within the cast array, we will convert it to
// an appropriate native PHP type dependant upon the associated value
// given with the key in the pair. Dayle made this comment line up.
if ($this->hasCast($key)) {
return $this->castAttribute($key, $value);
}
// If the attribute is listed as a date, we will convert it to a DateTime
// instance on retrieval, which makes it quite convenient to work with
// date fields without having to create a mutator for each property.
if (in_array($key, $this->getDates()) &&
! is_null($value)) {
return $this->asDateTime($value);
}
return $value;
return $this->transformModelValue($key, $this->getAttributeFromArray($key));
}
/**
@ -264,6 +277,16 @@ trait HasAttributes
return method_exists($this, 'get' . Str::studly($key) . 'Attribute');
}
/**
* Merge new casts with existing casts on the model.
*
* @param array $casts
*/
public function mergeCasts($casts)
{
$this->casts = array_merge($this->casts, $casts);
}
/**
* Set a given attribute on the model.
*
@ -286,6 +309,12 @@ trait HasAttributes
$value = $this->fromDateTime($value);
}
if ($this->isClassCastable($key)) {
$this->setClassCastableAttribute($key, $value);
return $this;
}
if ($this->isJsonCastable($key) && ! is_null($value)) {
$value = $this->castAttributeAsJson($key, $value);
}
@ -385,8 +414,8 @@ trait HasAttributes
$defaults = [static::CREATED_AT, static::UPDATED_AT];
return $this->usesTimestamps()
? array_unique(array_merge($this->dates, $defaults))
: $this->dates;
? array_unique(array_merge($this->dates, $defaults))
: $this->dates;
}
/**
@ -452,6 +481,15 @@ trait HasAttributes
return $this->attributes;
}
/**
* @return $this
*/
public function syncAttributes()
{
$this->mergeAttributesFromClassCasts();
return $this;
}
/**
* Set the array of model attributes. No checking is done.
*
@ -466,6 +504,8 @@ trait HasAttributes
$this->syncOriginal();
}
$this->classCastCache = [];
return $this;
}
@ -478,7 +518,16 @@ trait HasAttributes
*/
public function getOriginal($key = null, $default = null)
{
return Arr::get($this->original, $key, $default);
if ($key) {
return $this->transformModelValue(
$key,
Arr::get($this->original, $key, $default)
);
}
return collect($this->original)->mapWithKeys(function ($value, $key) {
return [$key => $this->transformModelValue($key, $value)];
})->all();
}
/**
@ -505,7 +554,7 @@ trait HasAttributes
*/
public function syncOriginal()
{
$this->original = $this->attributes;
$this->original = $this->getAttributes();
return $this;
}
@ -531,8 +580,10 @@ trait HasAttributes
{
$attributes = is_array($attributes) ? $attributes : func_get_args();
$modelAttributes = $this->getAttributes();
foreach ($attributes as $attribute) {
$this->original[$attribute] = $this->attributes[$attribute];
$this->original[$attribute] = $modelAttributes[$attribute];
}
return $this;
@ -630,25 +681,28 @@ trait HasAttributes
return false;
}
$original = $this->getOriginal($key);
$original = Arr::get($this->original, $key);
if ($current === $original) {
return true;
}
if (is_null($current)) {
return false;
}
if ($this->isDateAttribute($key)) {
return $this->fromDateTime($current) ===
$this->fromDateTime($original);
$this->fromDateTime($original);
}
if ($this->hasCast($key)) {
if ($this->hasCast($key, static::$primitiveCastTypes)) {
return $this->castAttribute($key, $current) ===
$this->castAttribute($key, $original);
$this->castAttribute($key, $original);
}
return is_numeric($current) && is_numeric($original)
&& strcmp((string) $current, (string) $original) === 0;
&& strcmp((string) $current, (string) $original) === 0;
}
/**
@ -784,6 +838,10 @@ trait HasAttributes
if ($attributes[$key] && $this->isCustomDateTimeCast($value)) {
$attributes[$key] = $attributes[$key]->format(explode(':', $value, 2)[1]);
}
if ($attributes[$key] instanceof Arrayable) {
$attributes[$key] = $attributes[$key]->toArray();
}
}
return $attributes;
@ -796,7 +854,9 @@ trait HasAttributes
*/
protected function getArrayableAttributes()
{
return $this->getArrayableItems($this->attributes);
$this->syncAttributes();
return $this->getArrayableItems($this->getAttributes());
}
/**
@ -850,9 +910,7 @@ trait HasAttributes
*/
protected function getAttributeFromArray($key)
{
if (isset($this->attributes[$key])) {
return $this->attributes[$key];
}
return $this->getAttributes()[$key] ?? null;
}
/**
@ -867,6 +925,14 @@ trait HasAttributes
$relation = $this->{$method}();
if (! $relation instanceof Relation) {
if (is_null($relation)) {
throw new LogicException(sprintf(
'%s::%s must return a relationship instance, but "null" was returned. Was the "return" keyword used?',
static::class,
$method
));
}
throw new LogicException(sprintf(
'%s::%s must return a relationship instance.',
static::class,
@ -898,7 +964,9 @@ trait HasAttributes
*/
protected function mutateAttributeForArray($key, $value)
{
$value = $this->mutateAttribute($key, $value);
$value = $this->isClassCastable($key)
? $this->getClassCastableAttributeValue($key, $value)
: $this->mutateAttribute($key, $value);
return $value instanceof Arrayable ? $value->toArray() : $value;
}
@ -911,11 +979,13 @@ trait HasAttributes
*/
protected function castAttribute($key, $value)
{
if (is_null($value)) {
$castType = $this->getCastType($key);
if (is_null($value) && in_array($castType, static::$primitiveCastTypes)) {
return $value;
}
switch ($this->getCastType($key)) {
switch ($castType) {
case 'int':
case 'integer':
return (int) $value;
@ -944,9 +1014,40 @@ trait HasAttributes
return $this->asDateTime($value);
case 'timestamp':
return $this->asTimestamp($value);
default:
return $value;
}
if ($this->isClassCastable($key)) {
return $this->getClassCastableAttributeValue($key, $value);
}
return $value;
}
/**
* Cast the given attribute using a custom cast class.
*
* @param string $key
* @param mixed $value
* @return mixed
*/
protected function getClassCastableAttributeValue($key, $value)
{
if (isset($this->classCastCache[$key])) {
return $this->classCastCache[$key];
}
$caster = $this->resolveCasterClass($key);
$value = $caster instanceof CastsInboundAttributes
? $value
: $caster->get($this, $key, $value, $this->attributes);
if ($caster instanceof CastsInboundAttributes || ! is_object($value)) {
unset($this->classCastCache[$key]);
} else {
$this->classCastCache[$key] = $value;
}
return $value;
}
/**
@ -977,7 +1078,7 @@ trait HasAttributes
protected function isCustomDateTimeCast($cast)
{
return strncmp($cast, 'date:', 5) === 0 ||
strncmp($cast, 'datetime:', 9) === 0;
strncmp($cast, 'datetime:', 9) === 0;
}
/**
@ -1010,8 +1111,48 @@ trait HasAttributes
*/
protected function isDateAttribute($key)
{
return in_array($key, $this->getDates()) ||
$this->isDateCastable($key);
return in_array($key, $this->getDates(), true) ||
$this->isDateCastable($key);
}
/**
* Set the value of a class castable attribute.
*
* @param string $key
* @param mixed $value
*/
protected function setClassCastableAttribute($key, $value)
{
$caster = $this->resolveCasterClass($key);
if (is_null($value)) {
$this->attributes = array_merge($this->attributes, array_map(
function () {
},
$this->normalizeCastClassResponse($key, $caster->set(
$this,
$key,
$this->{$key},
$this->attributes
))
));
} else {
$this->attributes = array_merge(
$this->attributes,
$this->normalizeCastClassResponse($key, $caster->set(
$this,
$key,
$value,
$this->attributes
))
);
}
if ($caster instanceof CastsInboundAttributes || ! is_object($value)) {
unset($this->classCastCache[$key]);
} else {
$this->classCastCache[$key] = $value;
}
}
/**
@ -1038,7 +1179,7 @@ trait HasAttributes
protected function getArrayAttributeByKey($key)
{
return isset($this->attributes[$key]) ?
$this->fromJson($this->attributes[$key]) : [];
$this->fromJson($this->attributes[$key]) : [];
}
/**
@ -1136,13 +1277,16 @@ trait HasAttributes
return Carbon::instance(Carbon::createFromFormat('Y-m-d', $value)->startOfDay());
}
$format = $this->getDateFormat();
// Finally, we will just assume this date is in the format used by default on
// the database connection and use that format to create the Carbon object
// that is returned back out to the developers after we convert it here.
return Carbon::createFromFormat(
str_replace('.v', '.u', $this->getDateFormat()),
$value
);
if (Carbon::hasFormat($value, $format)) {
return Carbon::createFromFormat($format, $value);
}
return Carbon::parse($value);
}
/**
@ -1200,7 +1344,96 @@ trait HasAttributes
}
/**
* Determine if the given attributes were changed.
* Determine if the given key is cast using a custom class.
*
* @param string $key
* @return bool
*/
protected function isClassCastable($key)
{
return array_key_exists($key, $this->getCasts()) &&
class_exists($class = $this->parseCasterClass($this->getCasts()[$key])) &&
! in_array($class, static::$primitiveCastTypes);
}
/**
* Resolve the custom caster class for a given key.
*
* @param string $key
* @return CastsAttributes|CastsInboundAttributes
*/
protected function resolveCasterClass($key)
{
$castType = $this->getCasts()[$key];
$arguments = [];
if (is_string($castType) && strpos($castType, ':') !== false) {
$segments = explode(':', $castType, 2);
$castType = $segments[0];
$arguments = explode(',', $segments[1]);
}
if (is_subclass_of($castType, Castable::class)) {
$castType = $castType::castUsing();
}
if (is_object($castType)) {
return $castType;
}
return new $castType(...$arguments);
}
/**
* Parse the given caster class, removing any arguments.
*
* @param string $class
* @return string
*/
protected function parseCasterClass($class)
{
return strpos($class, ':') === false
? $class
: explode(':', $class, 2)[0];
}
/**
* Merge the cast class attributes back into the model.
*/
protected function mergeAttributesFromClassCasts()
{
foreach ($this->classCastCache as $key => $value) {
if ($value instanceof Synchronized && $value->isSynchronized()) {
continue;
}
$caster = $this->resolveCasterClass($key);
$this->attributes = array_merge(
$this->attributes,
$caster instanceof CastsInboundAttributes
? [$key => $value]
: $this->normalizeCastClassResponse($key, $caster->set($this, $key, $value, $this->attributes))
);
}
}
/**
* Normalize the response from a custom class caster.
*
* @param string $key
* @param mixed $value
* @return array
*/
protected function normalizeCastClassResponse($key, $value)
{
return is_array($value) ? $value : [$key => $value];
}
/**
* Determine if any of the given attributes were changed.
*
* @param array $changes
* @param null|array|string $attributes
@ -1227,6 +1460,40 @@ trait HasAttributes
return false;
}
/**
* Transform a raw model value using mutators, casts, etc.
*
* @param string $key
* @param mixed $value
* @return mixed
*/
protected function transformModelValue($key, $value)
{
// If the attribute has a get mutator, we will call that then return what
// it returns as the value, which is useful for transforming values on
// retrieval from the model to a form that is more useful for usage.
if ($this->hasGetMutator($key)) {
return $this->mutateAttribute($key, $value);
}
// If the attribute exists within the cast array, we will convert it to
// an appropriate native PHP type dependent upon the associated value
// given with the key in the pair. Dayle made this comment line up.
if ($this->hasCast($key)) {
return $this->castAttribute($key, $value);
}
// If the attribute is listed as a date, we will convert it to a DateTime
// instance on retrieval, which makes it quite convenient to work with
// date fields without having to create a mutator for each property.
if ($value !== null
&& \in_array($key, $this->getDates(), false)) {
return $this->asDateTime($value);
}
return $value;
}
/**
* Get all of the attribute mutator methods.
*

View File

@ -25,7 +25,7 @@ trait QueriesRelationships
/**
* Add a relationship count / exists condition to the query.
*
* @param string $relation
* @param Relation|string $relation
* @param string $operator
* @param int $count
* @param string $boolean
@ -245,13 +245,48 @@ trait QueriesRelationships
* @param array|string $types
* @param string $operator
* @param int $count
* @return mixed
* @return $this
*/
public function whereHasMorph($relation, $types, Closure $callback = null, $operator = '>=', $count = 1)
{
return $this->hasMorph($relation, $types, $operator, $count, 'and', $callback);
}
/**
* Add a polymorphic relationship count / exists condition to the query with where clauses and an "or".
*
* @param array|string $types
* @param \Closure $callback
* @return $this
*/
public function orWhereHasMorph(string $relation, $types, Closure $callback = null, string $operator = '>=', int $count = 1)
{
return $this->hasMorph($relation, $types, $operator, $count, 'or', $callback);
}
/**
* Add a polymorphic relationship count / exists condition to the query with where clauses.
*
* @param array|string $types
* @return $this
*/
public function whereDoesntHaveMorph(string $relation, $types, Closure $callback = null)
{
return $this->doesntHaveMorph($relation, $types, 'and', $callback);
}
/**
* Add a polymorphic relationship count / exists condition to the query with where clauses and an "or".
*
* @param array|string $types
* @param \Closure $callback
* @return $this
*/
public function orWhereDoesntHaveMorph(string $relation, $types, Closure $callback = null)
{
return $this->doesntHaveMorph($relation, $types, 'or', $callback);
}
/**
* Add a polymorphic relationship count / exists condition to the query.
*
@ -260,7 +295,7 @@ trait QueriesRelationships
* @param string $operator
* @param int $count
* @param string $boolean
* @return mixed
* @return $this
*/
public function hasMorph($relation, $types, $operator = '>=', $count = 1, $boolean = 'and', Closure $callback = null)
{
@ -269,7 +304,7 @@ trait QueriesRelationships
$types = (array) $types;
if ($types === ['*']) {
$types = $this->model->newModelQuery()->distinct()->pluck($relation->getMorphType())->all();
$types = $this->model->newModelQuery()->distinct()->pluck($relation->getMorphType())->filter()->all();
foreach ($types as &$type) {
$type = Relation::getMorphedModel($type) ?? $type;
@ -294,6 +329,18 @@ trait QueriesRelationships
}, null, null, $boolean);
}
/**
* Add a polymorphic relationship count / exists condition to the query.
*
* @param array|string $types
* @param string $boolean
* @return $this
*/
public function doesntHaveMorph(string $relation, $types, $boolean = 'and', Closure $callback = null)
{
return $this->hasMorph($relation, $types, '<', 1, $boolean, $callback);
}
/**
* Add nested relationship count / exists conditions to the query.
*

View File

@ -218,6 +218,20 @@ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializab
return $this->toJson();
}
/**
* Prepare the object for serialization.
*
* @return array
*/
public function __sleep()
{
$this->mergeAttributesFromClassCasts();
$this->classCastCache = [];
return array_keys(get_object_vars($this));
}
/**
* When a model is being unserialized, check if it needs to be booted.
*/
@ -340,6 +354,8 @@ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializab
$model->setTable($this->getTable());
$model->mergeCasts($this->casts);
return $model;
}
@ -428,6 +444,22 @@ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializab
return $this;
}
/**
* Eager load relationships on the polymorphic relation of a model.
*
* @param string $relation
* @param array $relations
* @return $this
*/
public function loadMorph($relation, $relations)
{
$className = get_class($this->{$relation});
$this->{$relation}->load($relations[$className] ?? []);
return $this;
}
/**
* Eager load relations on the model if they are not already eager loaded.
*
@ -458,6 +490,22 @@ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializab
return $this;
}
/**
* Eager load relationship counts on the polymorphic relation of a model.
*
* @param string $relation
* @param array $relations
* @return $this
*/
public function loadMorphCount($relation, $relations)
{
$className = get_class($this->{$relation});
$this->{$relation}->loadCount($relations[$className] ?? []);
return $this;
}
/**
* Update the model in the database.
*
@ -504,6 +552,8 @@ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializab
*/
public function save(array $options = []): bool
{
$this->mergeAttributesFromClassCasts();
$query = $this->newModelQuery();
// If the "saving" event returns false we'll bail out of the save and return
@ -595,6 +645,8 @@ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializab
*/
public function delete()
{
$this->mergeAttributesFromClassCasts();
if (is_null($this->getKeyName())) {
throw new Exception('No primary key defined on model.');
}
@ -848,7 +900,7 @@ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializab
$this->getUpdatedAtColumn(),
];
$attributes = Arr::except($this->attributes, $except ? array_unique(array_merge($except, $defaults)) : $defaults);
$attributes = Arr::except($this->getAttributes(), $except ? array_unique(array_merge($except, $defaults)) : $defaults);
return tap(new static(), function ($instance) use ($attributes) {
// @var \Hyperf\Database\Model\Model $instance
@ -927,7 +979,7 @@ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializab
*/
public function getTable()
{
return isset($this->table) ? $this->table : Str::snake(Str::pluralStudly(class_basename($this)));
return $this->table ?? Str::snake(Str::pluralStudly(class_basename($this)));
}
/**

View File

@ -0,0 +1,422 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Database;
use Hyperf\Contract\Castable;
use Hyperf\Contract\CastsAttributes;
use Hyperf\Contract\CastsInboundAttributes;
use Hyperf\Database\Model\CastsValue;
use Hyperf\Database\Model\Model;
use Hyperf\Utils\Arr;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class DatabaseModelCustomCastingTest extends TestCase
{
protected function tearDown()
{
\Mockery::close();
}
public function testBasicCustomCasting()
{
$model = new TestModelWithCustomCast();
$model->uppercase = 'taylor';
$this->assertSame('TAYLOR', $model->uppercase);
$this->assertSame('TAYLOR', $model->getAttributes()['uppercase']);
$this->assertSame('TAYLOR', $model->toArray()['uppercase']);
$unserializedModel = unserialize(serialize($model));
$this->assertSame('TAYLOR', $unserializedModel->uppercase);
$this->assertSame('TAYLOR', $unserializedModel->getAttributes()['uppercase']);
$this->assertSame('TAYLOR', $unserializedModel->toArray()['uppercase']);
$model->syncOriginal();
$model->uppercase = 'dries';
$this->assertEquals('TAYLOR', $model->getOriginal('uppercase'));
$model = new TestModelWithCustomCast();
$model->uppercase = 'taylor';
$model->syncOriginal();
$model->uppercase = 'dries';
$model->getOriginal();
$this->assertEquals('DRIES', $model->uppercase);
$model = new TestModelWithCustomCast();
$model->address = $address = new Address('110 Kingsbrook St.', 'My Childhood House');
$address->lineOne = '117 Spencer St.';
$this->assertSame('117 Spencer St.', $model->syncAttributes()->getAttributes()['address_line_one']);
$model = new TestModelWithCustomCast();
$model->setRawAttributes([
'address_line_one' => '110 Kingsbrook St.',
'address_line_two' => 'My Childhood House',
]);
$this->assertSame('110 Kingsbrook St.', $model->address->lineOne);
$this->assertSame('My Childhood House', $model->address->lineTwo);
$this->assertSame('110 Kingsbrook St.', $model->toArray()['address_line_one']);
$this->assertSame('My Childhood House', $model->toArray()['address_line_two']);
$model->address->lineOne = '117 Spencer St.';
$this->assertFalse(isset($model->toArray()['address']));
$this->assertSame('117 Spencer St.', $model->toArray()['address_line_one']);
$this->assertSame('My Childhood House', $model->toArray()['address_line_two']);
$this->assertSame('117 Spencer St.', json_decode($model->toJson(), true)['address_line_one']);
$this->assertSame('My Childhood House', json_decode($model->toJson(), true)['address_line_two']);
$model->address = null;
$this->assertNull($model->toArray()['address_line_one']);
$this->assertNull($model->toArray()['address_line_two']);
$model->options = ['foo' => 'bar'];
$this->assertEquals(['foo' => 'bar'], $model->options);
$this->assertEquals(['foo' => 'bar'], $model->options);
$model->options = ['foo' => 'bar'];
$model->options = ['foo' => 'bar'];
$this->assertEquals(['foo' => 'bar'], $model->options);
$this->assertEquals(['foo' => 'bar'], $model->options);
$this->assertEquals(json_encode(['foo' => 'bar']), $model->getAttributes()['options']);
$model = new TestModelWithCustomCast(['options' => []]);
$model->syncOriginal();
$model->options = ['foo' => 'bar'];
$this->assertTrue($model->isDirty('options'));
}
public function testOneWayCasting()
{
// CastsInboundAttributes is used for casting that is unidirectional... only use case I can think of is one-way hashing...
$model = new TestModelWithCustomCast();
$model->password = 'secret';
$this->assertEquals(hash('sha256', 'secret'), $model->password);
$this->assertEquals(hash('sha256', 'secret'), $model->getAttributes()['password']);
$this->assertEquals(hash('sha256', 'secret'), $model->getAttributes()['password']);
$this->assertEquals(hash('sha256', 'secret'), $model->password);
$model->password = 'secret2';
$this->assertEquals(hash('sha256', 'secret2'), $model->password);
$this->assertEquals(hash('sha256', 'secret2'), $model->getAttributes()['password']);
$this->assertEquals(hash('sha256', 'secret2'), $model->getAttributes()['password']);
$this->assertEquals(hash('sha256', 'secret2'), $model->password);
}
public function testSettingRawAttributesClearsTheCastCache()
{
$model = new TestModelWithCustomCast();
$model->setRawAttributes([
'address_line_one' => '110 Kingsbrook St.',
'address_line_two' => 'My Childhood House',
]);
$this->assertSame('110 Kingsbrook St.', $model->address->lineOne);
$model->setRawAttributes([
'address_line_one' => '117 Spencer St.',
'address_line_two' => 'My Childhood House',
]);
$this->assertSame('117 Spencer St.', $model->address->lineOne);
}
public function testWithCastableInterface()
{
$model = new TestModelWithCustomCast();
$model->setRawAttributes([
'value_object_with_caster' => serialize(new ValueObject('hello')),
]);
$this->assertInstanceOf(ValueObject::class, $model->value_object_with_caster);
$model->setRawAttributes([
'value_object_caster_with_argument' => null,
]);
$this->assertEquals('argument', $model->value_object_caster_with_argument);
$model->setRawAttributes([
'value_object_caster_with_caster_instance' => serialize(new ValueObject('hello')),
]);
$this->assertInstanceOf(ValueObject::class, $model->value_object_caster_with_caster_instance);
}
public function testGetAttribute()
{
$model = new TestModelWithCustomCast();
$model->mergeCasts([
'mockery' => MockeryAttribute::class,
]);
$mockery = \Mockery::mock(CastsAttributes::class);
$mockery->shouldReceive('get')->withAnyArgs()->andReturn(function ($_, $key, $value, $attributes) {
$obj = new \stdClass();
$obj->value = $attributes[$key . '_origin'] - 1;
return $obj;
});
$mockery->shouldReceive('set')->withAnyArgs()->once()->andReturnUsing(function ($_, $key, $value, $attributes) {
return [
$key . '_origin' => $value->value + 1,
];
});
MockeryAttribute::$attribute = $mockery;
$std = new \stdClass();
$std->value = 1;
$model->mockery = $std;
$this->assertSame(1, $model->mockery->value);
}
public function testResolveCasterClass()
{
$model = new TestModelWithCustomCast();
$ref = new \ReflectionClass($model);
$method = $ref->getMethod('resolveCasterClass');
$method->setAccessible(true);
CastUsing::$castsAttributes = UppercaseCaster::class;
$this->assertNotSame($method->invokeArgs($model, ['cast_using']), $method->invokeArgs($model, ['cast_using']));
CastUsing::$castsAttributes = new UppercaseCaster();
$this->assertSame($method->invokeArgs($model, ['cast_using']), $method->invokeArgs($model, ['cast_using']));
}
public function testIsSynchronized()
{
$model = new TestModelWithCustomCast();
$model->user = $user = new UserInfo($model, ['name' => 'Hyperf', 'gender' => 1]);
$model->syncOriginal();
$attributes = $model->getAttributes();
$this->assertSame(['name' => 'Hyperf', 'gender' => 1], $attributes);
$user->name = 'Nano';
$attributes = $model->getAttributes();
$this->assertSame(['name' => 'Nano', 'gender' => 1], $attributes);
$this->assertSame(['name' => 'Nano'], $model->getDirty());
$this->assertSame(2, UserInfoCaster::$setCount);
$this->assertSame(0, UserInfoCaster::$getCount);
}
}
class TestModelWithCustomCast extends Model
{
/**
* The attributes that aren't mass assignable.
*
* @var array
*/
protected $guarded = [];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'address' => AddressCaster::class,
'user' => UserInfoCaster::class,
'password' => HashCaster::class,
'other_password' => HashCaster::class . ':md5',
'uppercase' => UppercaseCaster::class,
'options' => JsonCaster::class,
'value_object_with_caster' => ValueObject::class,
'value_object_caster_with_argument' => ValueObject::class . ':argument',
'value_object_caster_with_caster_instance' => ValueObjectWithCasterInstance::class,
'cast_using' => CastUsing::class,
];
}
class CastUsing implements Castable
{
/**
* @var CastsAttributes
*/
public static $castsAttributes;
public static function castUsing()
{
return self::$castsAttributes;
}
}
class HashCaster implements CastsInboundAttributes
{
public function __construct($algorithm = 'sha256')
{
$this->algorithm = $algorithm;
}
public function set($model, $key, $value, $attributes)
{
return [$key => hash($this->algorithm, $value)];
}
}
class UppercaseCaster implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return strtoupper($value);
}
public function set($model, $key, $value, $attributes)
{
return [$key => strtoupper($value)];
}
}
class AddressCaster implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return new Address($attributes['address_line_one'], $attributes['address_line_two']);
}
public function set($model, $key, $value, $attributes)
{
return ['address_line_one' => $value->lineOne, 'address_line_two' => $value->lineTwo];
}
}
class UserInfoCaster implements CastsAttributes
{
public static $setCount = 0;
public static $getCount = 0;
public function get($model, string $key, $value, array $attributes)
{
++self::$getCount;
return new UserInfo($model, Arr::only($attributes, ['name', 'gender']));
}
public function set($model, string $key, $value, array $attributes)
{
++self::$setCount;
return [
'name' => $value->name,
'gender' => $value->gender,
];
}
}
class JsonCaster implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return json_decode($value, true);
}
public function set($model, $key, $value, $attributes)
{
return json_encode($value);
}
}
class ValueObjectCaster implements CastsAttributes
{
private $argument;
public function __construct($argument = null)
{
$this->argument = $argument;
}
public function get($model, $key, $value, $attributes)
{
if ($this->argument) {
return $this->argument;
}
return unserialize($value);
}
public function set($model, $key, $value, $attributes)
{
return serialize($value);
}
}
class MockeryAttribute implements CastsAttributes
{
/**
* @var CastsAttributes
*/
public static $attribute;
public function get($model, string $key, $value, array $attributes)
{
return self::$attribute->get($model, $key, $value, $attributes);
}
public function set($model, string $key, $value, array $attributes)
{
return self::$attribute->set($model, $key, $value, $attributes);
}
}
class ValueObject implements Castable
{
public static function castUsing()
{
return ValueObjectCaster::class;
}
}
class ValueObjectWithCasterInstance extends ValueObject
{
public static function castUsing()
{
return new ValueObjectCaster();
}
}
class Address
{
public $lineOne;
public $lineTwo;
public function __construct($lineOne, $lineTwo)
{
$this->lineOne = $lineOne;
$this->lineTwo = $lineTwo;
}
}
/**
* @property string $name
* @property int $gender
*/
class UserInfo extends CastsValue
{
}

View File

@ -1158,6 +1158,16 @@ class ModelBuilderTest extends TestCase
Carbon::setTestNow(null);
}
public function testWithCastsMethod()
{
$builder = new Builder($this->getMockQueryBuilder());
$model = $this->getMockModel();
$builder->setModel($model);
$model->shouldReceive('mergeCasts')->with(['foo' => 'bar'])->once();
$builder->withCasts(['foo' => 'bar']);
}
protected function mockConnectionForModel($model, $database)
{
$grammarClass = 'Hyperf\Database\Query\Grammars\\' . $database . 'Grammar';

View File

@ -98,6 +98,20 @@ class ModelMorphEagerLoadingTest extends TestCase
}
}
public function testMorphAssociationEmpty()
{
$this->getContainer();
$images = Image::query()->whereHasMorph(
'imageable',
['*'],
function (Builder $query) {
$query->where('imageable_id', 1);
}
)->get();
$this->assertSame(2, $images->count());
}
public function testWhereHasMorph()
{
$this->getContainer();
@ -123,6 +137,117 @@ class ModelMorphEagerLoadingTest extends TestCase
}
}
public function testOrWhereHasMorph()
{
$this->getContainer();
$images = Image::query()
->whereHasMorph(
'imageable',
[
User::class,
],
function (Builder $query) {
$query->where('id', '=', 1);
}
)
->orWhereHasMorph(
'imageable',
[
Book::class,
],
function (Builder $query) {
$query->where('id', '=', 1);
}
)->get();
$this->assertSame(1, $images[0]->imageable->id);
$this->assertSame(1, $images[1]->imageable->id);
$sqls = [
['select * from `images` where ((`imageable_type` = ? and exists (select * from `user` where `images`.`imageable_id` = `user`.`id` and `id` = ?))) or ((`imageable_type` = ? and exists (select * from `book` where `images`.`imageable_id` = `book`.`id` and `id` = ?)))', ['user', 1, 'book', 1]],
['select * from `user` where `user`.`id` = ? limit 1', [1]],
['select * from `book` where `book`.`id` = ? limit 1', [1]],
];
while ($event = $this->channel->pop(0.001)) {
if ($event instanceof QueryExecuted) {
$this->assertSame([$event->sql, $event->bindings], array_shift($sqls));
}
}
}
public function testWhereDoesntHaveMorph()
{
$this->getContainer();
$images = Image::query()
->whereDoesntHaveMorph(
'imageable',
[
User::class,
Book::class,
],
function (Builder $query, $type) {
if ($type === User::class) {
$query->where('id', '<>', 1);
}
if ($type === Book::class) {
$query->where('id', '<>', 1);
}
}
)
->get();
$res = $images->every(function ($item, $key) {
return $item->imageable->id == 1;
});
$this->assertSame(true, $res);
$sqls = [
['select * from `images` where ((`imageable_type` = ? and not exists (select * from `user` where `images`.`imageable_id` = `user`.`id` and `id` <> ?)) or (`imageable_type` = ? and not exists (select * from `book` where `images`.`imageable_id` = `book`.`id` and `id` <> ?)))', ['user', 1, 'book', 1]],
['select * from `user` where `user`.`id` = ? limit 1', [1]],
['select * from `book` where `book`.`id` = ? limit 1', [1]],
];
while ($event = $this->channel->pop(0.001)) {
if ($event instanceof QueryExecuted) {
$this->assertSame([$event->sql, $event->bindings], array_shift($sqls));
}
}
}
public function testOrWhereDoesntHaveMorph()
{
$this->getContainer();
$images = Image::query()
->whereDoesntHaveMorph(
'imageable',
[
User::class,
],
function (Builder $query) {
$query->where('id', '<>', 1);
}
)
->orWhereDoesntHaveMorph(
'imageable',
[
Book::class,
],
function (Builder $query) {
$query->where('id', '<>', 1);
}
)
->get();
$res = $images->every(function ($item, $key) {
return $item->imageable->id == 1;
});
$this->assertSame(true, $res);
$sqls = [
['select * from `images` where ((`imageable_type` = ? and not exists (select * from `user` where `images`.`imageable_id` = `user`.`id` and `id` <> ?))) or ((`imageable_type` = ? and not exists (select * from `book` where `images`.`imageable_id` = `book`.`id` and `id` <> ?)))', ['user', 1, 'book', 1]],
['select * from `user` where `user`.`id` = ? limit 1', [1]],
['select * from `book` where `book`.`id` = ? limit 1', [1]],
];
while ($event = $this->channel->pop(0.001)) {
if ($event instanceof QueryExecuted) {
$this->assertSame([$event->sql, $event->bindings], array_shift($sqls));
}
}
}
protected function getContainer()
{
$dispatcher = Mockery::mock(EventDispatcherInterface::class);

View File

@ -37,6 +37,7 @@
},
"autoload-dev": {
"psr-4": {
"HyperfTest\\Devtool\\": "tests/"
}
},
"config": {

View File

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ExceptionHandler\Handler;
use Hyperf\Contract\SessionInterface;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Hyperf\Utils\Context;
use Hyperf\Utils\Str;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Throwable;
use Whoops\Handler\JsonResponseHandler;
use Whoops\Handler\PlainTextHandler;
use Whoops\Handler\PrettyPageHandler;
use Whoops\Handler\XmlResponseHandler;
use Whoops\Run;
class WhoopsExceptionHandler extends ExceptionHandler
{
protected static $preference = [
'text/html' => PrettyPageHandler::class,
'application/json' => JsonResponseHandler::class,
'application/xml' => XmlResponseHandler::class,
];
public function handle(Throwable $throwable, ResponseInterface $response)
{
$whoops = new Run();
[$handler, $contentType] = $this->negotiateHandler();
$whoops->pushHandler($handler);
$whoops->allowQuit(false);
ob_start();
$whoops->{Run::EXCEPTION_HANDLER}($throwable);
$content = ob_get_clean();
return $response
->withStatus(500)
->withHeader('Content-Type', $contentType)
->withBody(new SwooleStream($content));
}
public function isValid(Throwable $throwable): bool
{
return env('APP_ENV') !== 'prod' && class_exists(Run::class);
}
private function negotiateHandler()
{
/** @var ServerRequestInterface $request */
$request = Context::get(ServerRequestInterface::class);
$accepts = $request->getHeaderLine('accept');
foreach (self::$preference as $contentType => $handler) {
if (Str::contains($accepts, $contentType)) {
return [$this->setupHandler(new $handler()), $contentType];
}
}
return [new PlainTextHandler(), 'text/plain'];
}
private function setupHandler($handler)
{
if ($handler instanceof PrettyPageHandler) {
$handler->handleUnconditionally(true);
if (defined('BASE_PATH')) {
$handler->setApplicationRootPath(BASE_PATH);
}
$request = Context::get(ServerRequestInterface::class);
$handler->addDataTableCallback('PSR7 Query', [$request, 'getQueryParams']);
$handler->addDataTableCallback('PSR7 Post', [$request, 'getParsedBody']);
$handler->addDataTableCallback('PSR7 Server', [$request, 'getServerParams']);
$handler->addDataTableCallback('PSR7 Cookie', [$request, 'getCookieParams']);
$handler->addDataTableCallback('PSR7 File', [$request, 'getUploadedFiles']);
$handler->addDataTableCallback('PSR7 Attribute', [$request, 'getAttributes']);
$session = Context::get(SessionInterface::class);
if ($session) {
$handler->addDataTableCallback('Hyperf Session', [$session, 'all']);
}
}
return $handler;
}
}

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\ExceptionHandler;
use GuzzleHttp\Psr7\Response;
use Hyperf\ExceptionHandler\Handler\WhoopsExceptionHandler;
use Hyperf\HttpMessage\Server\Request;
use Hyperf\Nats\Exception;
use Hyperf\Utils\Context;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* @internal
* @coversNothing
*/
class WhoopsExceptionHandlerTest extends TestCase
{
public function testPlainTextWhoops()
{
Context::set(ServerRequestInterface::class, new Request('GET', '/'));
$handler = new WhoopsExceptionHandler();
$response = $handler->handle(new Exception(), new Response());
$this->assertInstanceOf(ResponseInterface::class, $response);
$this->assertEquals(500, $response->getStatusCode());
$this->assertEquals('text/plain', $response->getHeader('Content-Type')[0]);
}
public function testHtmlWhoops()
{
$request = new Request('GET', '/');
$request = $request->withHeader('accept', ['text/html,application/json,application/xml']);
Context::set(ServerRequestInterface::class, $request);
$handler = new WhoopsExceptionHandler();
$response = $handler->handle(new Exception(), new Response());
$this->assertInstanceOf(ResponseInterface::class, $response);
$this->assertEquals(500, $response->getStatusCode());
$this->assertEquals('text/html', $response->getHeader('Content-Type')[0]);
}
public function testJsonWhoops()
{
$request = new Request('GET', '/');
$request = $request->withHeader('accept', ['application/json,application/xml']);
Context::set(ServerRequestInterface::class, $request);
$handler = new WhoopsExceptionHandler();
$response = $handler->handle(new Exception(), new Response());
$this->assertInstanceOf(ResponseInterface::class, $response);
$this->assertEquals(500, $response->getStatusCode());
$this->assertEquals('application/json', $response->getHeader('Content-Type')[0]);
}
public function testXmlWhoops()
{
$request = new Request('GET', '/');
$request = $request->withHeader('accept', ['application/xml']);
Context::set(ServerRequestInterface::class, $request);
$handler = new WhoopsExceptionHandler();
$response = $handler->handle(new Exception(), new Response());
$this->assertInstanceOf(ResponseInterface::class, $response);
$this->assertEquals(500, $response->getStatusCode());
$this->assertEquals('application/xml', $response->getHeader('Content-Type')[0]);
}
}

View File

@ -17,7 +17,7 @@
},
"autoload-dev": {
"psr-4": {
"HyperfTest\\": "tests"
"HyperfTest\\Filesystem\\": "tests"
}
},
"require": {

View File

@ -13,10 +13,10 @@ namespace Oss\OssClient {
function is_resource($resource)
{
if (! function_exists('swoole_hook_flags')) {
return true;
return \is_resource($resource) || $resource instanceof \Swoole\Curl\Handler;
}
if (swoole_hook_flags() ^ SWOOLE_HOOK_CURL) {
return true;
if (swoole_hook_flags() & SWOOLE_HOOK_CURL) {
return \is_resource($resource) || $resource instanceof \Swoole\Curl\Handler;
}
return \is_resource($resource);
}
@ -26,10 +26,10 @@ namespace Oss\Http {
function is_resource($resource)
{
if (! function_exists('swoole_hook_flags')) {
return true;
return \is_resource($resource) || $resource instanceof \Swoole\Curl\Handler;
}
if (swoole_hook_flags() ^ SWOOLE_HOOK_CURL) {
return true;
if (swoole_hook_flags() & SWOOLE_HOOK_CURL) {
return \is_resource($resource) || $resource instanceof \Swoole\Curl\Handler;
}
return \is_resource($resource);
}

View File

@ -9,7 +9,7 @@ declare(strict_types=1);
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Cases;
namespace HyperfTest\Filesystem\Cases;
use PHPUnit\Framework\TestCase;

View File

@ -9,7 +9,7 @@ declare(strict_types=1);
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Cases;
namespace HyperfTest\Filesystem\Cases;
use Hyperf\Config\Config;
use Hyperf\Contract\ConfigInterface;

View File

@ -111,8 +111,11 @@ class JsonRpcPoolTransporter implements TransporterInterface
public function getConnection(): RpcConnection
{
$class = spl_object_hash($this) . '.Connection';
if (Context::has($class)) {
return Context::get($class);
/** @var RpcConnection $connection */
if (Context::has($class) && $connection = Context::get($class)) {
if ($connection->check()) {
return $connection;
}
}
$connection = $this->getPool()->get();
@ -130,6 +133,7 @@ class JsonRpcPoolTransporter implements TransporterInterface
$config = [
'connect_timeout' => $this->config['connect_timeout'],
'settings' => $this->config['settings'],
'pool' => $this->config['pool'],
'node' => function () {
return $this->getNode();
},

View File

@ -31,7 +31,7 @@ class RpcPool extends Pool
{
$this->name = $name;
$this->config = $config;
$options = [];
$options = $config['pool'] ?? [];
$this->frequency = make(Frequency::class);
parent::__construct($container, $options);
}

View File

@ -54,6 +54,25 @@ class JsonRpcPoolTransporterTest extends TestCase
$this->assertSame($settings, $transporter->getConfig()['settings']);
}
public function testJsonRpcPoolTransporterGetPool()
{
$container = $this->getContainer();
$factory = new PoolFactory($container);
$transporter = new JsonRpcPoolTransporter($factory, [
'pool' => ['min_connections' => 8, 'max_connections' => 88],
'settings' => $settings = [
'open_length_check' => true,
'package_length_type' => 'N',
'package_length_offset' => 0,
'package_body_offset' => 4,
],
]);
$options = $transporter->getPool()->getOption();
$this->assertSame(8, $options->getMinConnections());
$this->assertSame(88, $options->getMaxConnections());
}
public function testJsonRpcPoolTransporterSendLengthCheck()
{
$container = $this->getContainer();
@ -96,6 +115,28 @@ class JsonRpcPoolTransporterTest extends TestCase
$this->assertSame($data, $packer->unpack($string));
}
public function testGetConnection()
{
$container = $this->getContainer();
$factory = $container->get(PoolFactory::class);
$transporter = new JsonRpcPoolTransporter($factory, [
'pool' => ['min_connections' => 10],
'settings' => $settings = [
'open_length_check' => true,
'package_length_type' => 'N',
'package_length_offset' => 0,
'package_body_offset' => 4,
],
]);
$conn = $transporter->getConnection();
$conn2 = $transporter->getConnection();
$this->assertSame($conn, $conn2);
$conn->close();
$conn2 = $transporter->getConnection();
$this->assertNotEquals($conn, $conn2);
}
public function testsplObjectHash()
{
$class = new \stdClass();

View File

@ -39,6 +39,13 @@ class RpcConnectionStub extends RpcConnection
public function reconnect(): bool
{
$this->lastUseTime = microtime(true);
return true;
}
public function close(): bool
{
$this->lastUseTime = 0.0;
return true;
}
}

View File

@ -1194,7 +1194,7 @@
"instant": true,
"intervalFactor": 1,
"legendFormat": "",
"refId": "H"
"refId": "F"
}
],
"timeFrom": null,

View File

@ -15,6 +15,7 @@ use Hyperf\Contract\ConnectionInterface;
use Hyperf\Contract\FrequencyInterface;
use Hyperf\Contract\PoolInterface;
use Hyperf\Contract\PoolOptionInterface;
use Hyperf\Contract\StdoutLoggerInterface;
use Psr\Container\ContainerInterface;
use RuntimeException;
use Throwable;
@ -83,9 +84,16 @@ abstract class Pool implements PoolInterface
if ($num > 0) {
/** @var ConnectionInterface $conn */
while ($this->currentConnections > $this->option->getMinConnections() && $conn = $this->channel->pop($this->option->getWaitTimeout())) {
$conn->close();
--$this->currentConnections;
--$num;
try {
$conn->close();
} catch (\Throwable $exception) {
if ($this->container->has(StdoutLoggerInterface::class) && $logger = $this->container->get(StdoutLoggerInterface::class)) {
$logger->error((string) $exception);
}
} finally {
--$this->currentConnections;
--$num;
}
if ($num <= 0) {
// Ignore connections queued during flushing.

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Pool;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Utils\ApplicationContext;
use HyperfTest\Pool\Stub\FooPool;
use Mockery;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
/**
* @internal
* @coversNothing
*/
class PoolTest extends TestCase
{
protected function tearDown()
{
Mockery::close();
}
public function testPoolFlush()
{
$container = $this->getContainer();
$container->shouldReceive('has')->with(StdoutLoggerInterface::class)->andReturn(true);
$container->shouldReceive('get')->with(StdoutLoggerInterface::class)->andReturn(value(function () {
$logger = Mockery::mock(StdoutLoggerInterface::class);
$logger->shouldReceive('error')->withAnyArgs()->times(4)->andReturn(true);
return $logger;
}));
$pool = new FooPool($container, []);
$conns = [];
for ($i = 0; $i < 5; ++$i) {
$conns[] = $pool->get();
}
foreach ($conns as $conn) {
$pool->release($conn);
}
$pool->flush();
$this->assertSame(1, $pool->getConnectionsInChannel());
$this->assertSame(1, $pool->getCurrentConnections());
}
protected function getContainer()
{
$container = Mockery::mock(ContainerInterface::class);
ApplicationContext::setContainer($container);
return $container;
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Pool\Stub;
use Hyperf\Contract\ConnectionInterface;
use Hyperf\Pool\Pool;
use Mockery;
class FooPool extends Pool
{
protected function createConnection(): ConnectionInterface
{
return Mockery::mock(ConnectionInterface::class);
}
}

View File

@ -51,18 +51,22 @@ class AfterWorkerStartListener implements ListenerInterface
/** @var Port $server */
foreach (ServerManager::list() as $name => [$type, $server]) {
$listen = $server->host . ':' . $server->port;
$type = value(function () use ($type) {
$sockType = $server->type;
$type = value(function () use ($type, $sockType) {
switch ($type) {
case Server::SERVER_BASE:
return 'TCP';
break;
if (($sockType === SWOOLE_SOCK_TCP) || ($sockType === SWOOLE_SOCK_TCP6)) {
return 'TCP';
}
if (($sockType === SWOOLE_SOCK_UDP) || ($sockType === SWOOLE_SOCK_UDP6)) {
return 'UDP';
}
return 'UNKNOWN';
case Server::SERVER_WEBSOCKET:
return 'WebSocket';
break;
case Server::SERVER_HTTP:
default:
return 'HTTP';
break;
}
});
$this->logger->info(sprintf('%s Server listening at %s', $type, $listen));

View File

@ -81,6 +81,15 @@ class InitProcessTitleListener implements ListenerInterface
protected function setTitle(string $title)
{
@cli_set_process_title($title);
if ($this->isSupportedOS()) {
@cli_set_process_title($title);
}
}
protected function isSupportedOS(): bool
{
return ! in_array(PHP_OS, [
'Darwin',
]);
}
}

View File

@ -18,6 +18,8 @@ class InitProcessTitleListenerStub extends InitProcessTitleListener
{
public function setTitle(string $title)
{
Context::set('test.server.process.title', $title);
if ($this->isSupportedOS()) {
Context::set('test.server.process.title', $title);
}
}
}

View File

@ -20,6 +20,8 @@ class InitProcessTitleListenerStub2 extends InitProcessTitleListener
public function setTitle(string $title)
{
Context::set('test.server.process.title', $title);
if ($this->isSupportedOS()) {
Context::set('test.server.process.title', $title);
}
}
}

1
src/socketio-server/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
/tests export-ignore

View File

@ -0,0 +1,290 @@
Socket.io是一款非常流行的应用层实时通讯协议和框架可以轻松实现应答、分组、广播。hyperf/socketio-server支持了Socket.io的WebSocket传输协议。
## 安装
```bash
composer require hyperf/socketio-server
```
hyperf/socketio-server 是基于WebSocket实现的请确保服务端已经添加了WebSocket服务配置。
```php
[
'name' => 'socket-io',
'type' => Server::SERVER_WEBSOCKET,
'host' => '0.0.0.0',
'port' => 9502,
'sock_type' => SWOOLE_SOCK_TCP,
'callbacks' => [
SwooleEvent::ON_HAND_SHAKE => [Hyperf\WebSocketServer\Server::class, 'onHandShake'],
SwooleEvent::ON_MESSAGE => [Hyperf\WebSocketServer\Server::class, 'onMessage'],
SwooleEvent::ON_CLOSE => [Hyperf\WebSocketServer\Server::class, 'onClose'],
],
],
```
## 快速开始
### 服务端
```php
<?php
declare(strict_types=1);
namespace App\Controller;
use Hyperf\SocketIOServer\Annotation\Event;
use Hyperf\SocketIOServer\Annotation\SocketIONamespace;
use Hyperf\SocketIOServer\BaseNamespace;
use Hyperf\SocketIOServer\Socket;
use Hyperf\Utils\Codec\Json;
/**
* @SocketIONamespace("/")
*/
class WebSocketController extends BaseNamespace
{
/**
* @Event("event")
* @param string $data
*/
public function onEvent(Socket $socket, $data)
{
// 应答
return 'Event Received: ' . $data;
}
/**
* @Event("join-room")
* @param string $data
*/
public function onJoinRoom(Socket $socket, $data)
{
// 将当前用户加入房间
$socket->join($data);
// 向房间内其他用户推送(不含当前用户)
$socket->to($data)->emit('event', $socket->getSid() . "has joined {$data}");
// 向房间内所有人广播(含当前用户)
$this->emit('event', 'There are ' . count($socket->getAdapter()->clients($data)) . " players in {$data}");
}
/**
* @Event("say")
* @param string $data
*/
public function onSay(Socket $socket, $data)
{
$data = Json::decode($data);
$socket->to($data['room'])->emit('event', $socket->getSid() . " say: {$data['message']}");
}
}
```
> 每个 socket 会自动加入以自己 `sid` 命名的房间(`$socket->getSid()`),发送私聊信息就推送到对应 `sid` 即可。
> 框架会触发 `connect``disconnect` 两个事件。
### 客户端
由于服务端只实现了WebSocket通讯所以客户端要加上 `{transports:["websocket"]}`
```html
<script src="https://cdn.bootcss.com/socket.io/2.3.0/socket.io.js"></script>
<script>
var socket = io('ws://127.0.0.1:9502', { transports: ["websocket"] });
socket.on('connect', data => {
socket.emit('event', 'hello, hyperf', console.log);
socket.emit('join-room', 'room1', console.log);
setInterval(function () {
socket.emit('say', '{"room":"room1", "message":"Hello Hyperf."}');
}, 1000);
});
socket.on('event', console.log);
</script>
```
## API 清单
```php
<?php
function onConnect(\Hyperf\SocketIOServer\Socket $socket){
// sending to the client
$socket->emit('hello', 'can you hear me?', 1, 2, 'abc');
// sending to all clients except sender
$socket->broadcast->emit('broadcast', 'hello friends!');
// sending to all clients in 'game' room except sender
$socket->to('game')->emit('nice game', "let's play a game");
// sending to all clients in 'game1' and/or in 'game2' room, except sender
$socket->to('game1')->to('game2')->emit('nice game', "let's play a game (too)");
// WARNING: `$socket->to($socket->getSid())->emit()` will NOT work, as it will send to everyone in the room
// named `$socket->getSid()` but the sender. Please use the classic `$socket->emit()` instead.
// sending with acknowledgement
$reply = $socket->emit('question', 'do you think so?')->reply();
// sending without compression
$socket->compress(false)->emit('uncompressed', "that's rough");
$io = \Hyperf\Utils\ApplicationContext::getContainer()->get(\Hyperf\SocketIOServer\SocketIO::class);
// sending to all clients in 'game' room, including sender
$io->in('game')->emit('big-announcement', 'the game will start soon');
// sending to all clients in namespace 'myNamespace', including sender
$io->of('/myNamespace')->emit('bigger-announcement', 'the tournament will start soon');
// sending to a specific room in a specific namespace, including sender
$io->of('/myNamespace')->to('room')->emit('event', 'message');
// sending to individual socketid (private message)
$io->to('socketId')->emit('hey', 'I just met you');
// sending to all clients on this node (when using multiple nodes)
$io->local->emit('hi', 'my lovely babies');
// sending to all connected clients
$io->emit('an event sent to all connected clients');
};
```
## 进阶教程
### 设置 Socket.io 命名空间
Socket.io 通过自定义命名空间实现多路复用。(注意:不是 PHP 的命名空间)
1. 可以通过 `@SocketIONamespace("/xxx")` 将控制器映射为 xxx 的命名空间,
2. 也可通过
```php
<?php
use Hyperf\SocketIOServer\Collector\SocketIORouter;
use App\Controller\WebSocketController;
SocketIORouter::addNamespace('/xxx' , WebSocketController::class);
```
在路由中添加。
### 开启 Session
安装并配置好 hyperf/session 组件及其对应中间件,再通过 `SessionAspect` 切入 SocketIO 来使用 Session 。
```php
<?php
// config/autoload/aspect.php
return [
\Hyperf\SocketIOServer\Aspect\SessionAspect::class,
];
```
> swoole 4.4.17 及以下版本只能读取 http 创建好的Cookie4.4.18 及以上版本可以在WebSocket握手时创建Cookie
### 调整房间适配器
默认的房间功能通过 Redis 适配器实现,可以适应多进程乃至分布式场景。
1. 可以替换为内存适配器,只适用于单 worker 场景。
```php
<?php
// config/autoload/dependencies.php
return [
\Hyperf\SocketIOServer\Room\AdapterInterface::class => \Hyperf\SocketIOServer\Room\MemoryAdapter::class,
];
```
2. 可以替换为空适配器,不需要房间功能时可以降低消耗。
```php
<?php
// config/autoload/dependencies.php
return [
\Hyperf\SocketIOServer\Room\AdapterInterface::class => \Hyperf\SocketIOServer\Room\NullAdapter::class,
];
```
### 调整 SocketID (`sid`)
默认 SocketID 使用 `ServerID#FD` 的格式,可以适应分布式场景。
1. 可以替换为直接使用 Fd 。
```php
<?php
// config/autoload/dependencies.php
return [
\Hyperf\SocketIOServer\SidProvider\SidProviderInterface::class => \Hyperf\SocketIOServer\SidProvider\LocalSidProvider::class,
];
```
2. 也可以替换为 SessionID 。
```php
<?php
// config/autoload/dependencies.php
return [
\Hyperf\SocketIOServer\SidProvider\SidProviderInterface::class => \Hyperf\SocketIOServer\SidProvider\SessionSidProvider::class,
];
```
### 其他事件分发方法
1. 可以手动注册事件,不使用注解。
```php
<?php
declare(strict_types=1);
namespace App\Controller;
use Hyperf\SocketIOServer\BaseNamespace;
use Hyperf\SocketIOServer\SidProvider\SidProviderInterface;
use Hyperf\SocketIOServer\Socket;
use Hyperf\WebSocketServer\Sender;
class WebSocketController extends BaseNamespace
{
public function __construct(Sender $sender, SidProviderInterface $sidProvider) {
parent::__construct($sender,$sidProvider);
$this->on('event', [$this, 'echo']);
}
public function echo(Socket $socket, $data)
{
$socket->emit('event', $data);
}
}
```
2. 可以在控制器上添加 `@Event()` 注解,以方法名作为事件名来分发。此时应注意其他公有方法可能会和事件名冲突。
```php
<?php
declare(strict_types=1);
namespace App\Controller;
use Hyperf\SocketIOServer\Annotation\SocketIONamespace;
use Hyperf\SocketIOServer\Annotation\Event;
use Hyperf\SocketIOServer\BaseNamespace;
use Hyperf\SocketIOServer\Socket;
/**
* @SocketIONamespace("/")
* @Event()
*/
class WebSocketController extends BaseNamespace
{
public function echo(Socket $socket, $data)
{
$socket->emit('event', $data);
}
}
```

View File

@ -0,0 +1,59 @@
{
"name": "hyperf/socketio-server",
"type": "library",
"license": "MIT",
"keywords": [
"php",
"hyperf"
],
"description": "Socket.io implementation for hyperf",
"autoload": {
"psr-4": {
"Hyperf\\SocketIOServer\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"HyperfTest\\SocketIOServer\\": "tests"
}
},
"require": {
"php": ">=7.2",
"ext-json": "*",
"ext-redis": "*",
"ext-swoole": ">=4.4",
"hyperf/di": "~1.1.0",
"hyperf/redis": "~1.1.0",
"hyperf/websocket-server": "~1.1.0",
"mix/redis-subscribe": "^2.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.14",
"hyperf/command": "~1.1.0",
"hyperf/config": "~1.1.0",
"hyperf/session": "~1.1.0",
"hyperf/testing": "~1.1.0",
"mockery/mockery": "^1.3",
"phpstan/phpstan": "^0.10.5"
},
"suggest": {
"hyperf/command": "Required to use RemoveRedisGarbage command",
"hyperf/session": "Required to use session"
},
"config": {
"sort-packages": true
},
"scripts": {
"test": "co-phpunit -c phpunit.xml --colors=always",
"analyse": "phpstan analyse --memory-limit 300M -l 3 ./src",
"cs-fix": "php-cs-fixer fix $1"
},
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
},
"hyperf": {
"config": "Hyperf\\SocketIOServer\\ConfigProvider"
}
}
}

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\SocketIOServer\Annotation;
use Hyperf\Di\Annotation\AbstractAnnotation;
use Hyperf\Di\ReflectionManager;
use Hyperf\SocketIOServer\Collector\EventAnnotationCollector;
use Roave\BetterReflection\Reflection\Adapter\ReflectionMethod;
/**
* @Annotation
* @Target({"CLASS", "METHOD"})
*/
class Event extends AbstractAnnotation
{
public $event = 'event';
public function __construct($value = [])
{
parent::__construct();
$this->bindMainProperty('event', $value);
}
public function collectMethod(string $className, ?string $target): void
{
EventAnnotationCollector::collectEvent($className, $target, $this);
parent::collectMethod($className, $target);
}
public function collectClass(string $className): void
{
$methods = ReflectionManager::reflectClass($className)->getMethods(ReflectionMethod::IS_PUBLIC);
foreach ($methods as $method) {
$target = $method->getName();
EventAnnotationCollector::collectEvent($className, $target, new Event(['value' => $target]));
}
parent::collectClass($className);
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\SocketIOServer\Annotation;
use Hyperf\Di\Annotation\AbstractAnnotation;
use Hyperf\SocketIOServer\Collector\SocketIORouter;
/**
* @Annotation
* @Target({"CLASS"})
*/
class SocketIONamespace extends AbstractAnnotation
{
public $namespace = '/';
public function __construct($value = [])
{
parent::__construct();
$this->bindMainProperty('namespace', $value);
}
public function collectClass(string $className): void
{
SocketIORouter::addNamespace($this->namespace, $className);
parent::collectClass($className);
}
}

Some files were not shown because too many files have changed in this diff Show More