diff --git a/.php_cs b/.php_cs index bcc188a8c..f96a7942b 100644 --- a/.php_cs +++ b/.php_cs @@ -51,9 +51,6 @@ return PhpCsFixer\Config::create() 'comment_types' => [ ], ], - 'list_syntax' => [ - 'syntax' => 'short', - ], 'yoda_style' => [ 'always_move_variable' => false, 'equal' => false, diff --git a/.travis.yml b/.travis.yml index 63ac014e3..6caba21c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,11 +5,11 @@ sudo: required matrix: include: - php: 7.2 - env: SW_VERSION="4.4.5" + env: SW_VERSION="4.4.7" - php: 7.3 - env: SW_VERSION="4.4.5" + env: SW_VERSION="4.4.7" - php: master - env: SW_VERSION="4.4.5" + env: SW_VERSION="4.4.7" allow_failures: - php: master diff --git a/CHANGELOG.md b/CHANGELOG.md index 7168a51a4..51408ce14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,23 @@ # v1.1.2 - TBD +## Added + +- [#722](https://github.com/hyperf-cloud/hyperf/pull/722) Added config `concurrent.limit` for AMQP consumer. + ## Changed - [#678](https://github.com/hyperf-cloud/hyperf/pull/678) Added ignore-tables for `gen:model`, and ignore `migrations` table, and `migrations` table will not generate when execute the `gen:model` command. ## Fixed +- [#678](https://github.com/hyperf-cloud/hyperf/pull/678) Added ignore-tables for `gen:model`, and ignore `migrations` table. +- [#694](https://github.com/hyperf-cloud/hyperf/pull/694) Fixed `validationData` method of `Hyperf\Validation\Request\FormRequest` does not contains the uploaded files. - [#700](https://github.com/hyperf-cloud/hyperf/pull/700) Fixed the `download` method of `Hyperf\HttpServer\Contract\ResponseInterface` does not works as expected. - [#701](https://github.com/hyperf-cloud/hyperf/pull/701) Fixed the custom process will not restart automatically when throw an uncaptured exception. - [#704](https://github.com/hyperf-cloud/hyperf/pull/704) Fixed bug that `Call to a member function getName() on null` in `Hyperf\Validation\Middleware\ValidationMiddleware` when the argument of action method does not define the argument type. +- [#713](https://github.com/hyperf-cloud/hyperf/pull/713) Fixed `ignoreAnnotations` does not works when cache is used. - [#717](https://github.com/hyperf-cloud/hyperf/pull/717) Fixed the validator will be created repeatedly in `getValidatorInstance`. +- [#724](https://github.com/hyperf-cloud/hyperf/pull/724) Fixed `db:seed` command without database selected. # v1.1.1 - 2019-10-08 diff --git a/doc/zh/amqp.md b/doc/zh/amqp.md index 12604dff4..f978ffc6f 100644 --- a/doc/zh/amqp.md +++ b/doc/zh/amqp.md @@ -9,6 +9,18 @@ composer require hyperf/amqp ``` ## 默认配置 + +| 配置 | 类型 | 默认值 | 备注 | +|:----------------:|:------:|:---------:|:--------------:| +| host | string | localhost | Host | +| port | int | 5672 | 端口号 | +| user | string | guest | 用户名 | +| password | string | guest | 密码 | +| vhost | string | / | vhost | +| concurrent.limit | int | 0 | 同时消费的数量 | +| pool | object | | 连接池配置 | +| params | object | | 基本配置 | + ```php 'guest', 'password' => 'guest', 'vhost' => '/', + 'concurrent' => [ + 'limit' => 1, + ], 'pool' => [ 'min_connections' => 1, 'max_connections' => 10, @@ -150,9 +165,9 @@ class DemoConsumer extends ConsumerMessage 框架会根据 `Consumer` 内的 `consume` 方法所返回的结果来决定该消息的响应行为,共有 4 中响应结果,分别为 `\Hyperf\Amqp\Result::ACK`、`\Hyperf\Amqp\Result::NACK`、`\Hyperf\Amqp\Result::REQUEUE`、`\Hyperf\Amqp\Result::DROP`,每个返回值分别代表如下行为: -| 返回值 | 行为 | -|-------------------------------|-----| -| \Hyperf\Amqp\Result::ACK | 确认消息正确被消费掉了 | -| \Hyperf\Amqp\Result::NACK | 消息没有被正确消费掉,以 `basic_nack` 方法来响应 | -| \Hyperf\Amqp\Result::REQUEUE | 消息没有被正确消费掉,以 `basic_reject` 方法来响应,并使消息重新入列 | -| \Hyperf\Amqp\Result::DROP | 消息没有被正确消费掉,以 `basic_reject` 方法来响应 | \ No newline at end of file +| 返回值 | 行为 | +|------------------------------|----------------------------------------------------------------------| +| \Hyperf\Amqp\Result::ACK | 确认消息正确被消费掉了 | +| \Hyperf\Amqp\Result::NACK | 消息没有被正确消费掉,以 `basic_nack` 方法来响应 | +| \Hyperf\Amqp\Result::REQUEUE | 消息没有被正确消费掉,以 `basic_reject` 方法来响应,并使消息重新入列 | +| \Hyperf\Amqp\Result::DROP | 消息没有被正确消费掉,以 `basic_reject` 方法来响应 | \ No newline at end of file diff --git a/doc/zh/coroutine.md b/doc/zh/coroutine.md index 2dcb290cc..9f2aadb99 100644 --- a/doc/zh/coroutine.md +++ b/doc/zh/coroutine.md @@ -26,11 +26,11 @@ $db->connect($config, function ($db, $r) { // 从 users 表中查询一条数据 $sql = 'select * from users where id = 1'; $db->query($sql, function(swoole_mysql $db, $r) { - if ($r === true) { - $rows = $db->affected_rows; + if ($r !== false) { // 查询成功后修改一条数据 - $updateSql = 'update users set name='new name' where id = 1'; + $updateSql = 'update users set name="new name" where id = 1'; $db->query($updateSql, function (swoole_mysql $db, $r) { + $rows = $db->affected_rows; if ($r === true) { return $this->response->end('更新成功'); } @@ -40,6 +40,9 @@ $db->connect($config, function ($db, $r) { }); }); ``` + +> 注意 `MySQL` 等异步模块已在[4.3.0](https://wiki.swoole.com/wiki/page/p-4.3.0.html)中移除,并转移到了[swoolw_async](https://github.com/swoole/ext-async)。 + 从上面的代码片段可以看出,每一个操作几乎就需要一个回调函数,在复杂的业务场景中回调的层次感和代码结构绝对会让你崩溃,其实不难看出这样的写法有点类似 `JavaScript` 上的异步方法的写法,而 `JavaScript` 也为此提供了不少的解决方案(当然方案是源于其它编程语言),如 `Promise`,`yield + generator`, `async/await`,`Promise` 则是对回调的一种封装方式,而 `yield + generator` 和 `async/await` 则需要在代码上显性的增加一些代码语法标记,这些相对比回调函数来说,不妨都是一些非常不错的解决方案,但是你需要另花时间来理解它的实现机制和语法。 Swoole 协程也是对异步回调的一种解决方案,在 `PHP` 语言下,`Swoole` 协程与 `yield + generator` 都属于协程的解决方案,协程的解决方案可以使代码以近乎于同步代码的书写方式来书写异步代码,显性的区别则是 `yield + generator` 的协程机制下,每一处 `I/O` 操作的调用代码都需要在前面加上 `yield` 语法实现协程切换,每一层调用都需要加上,否则会出现意料之外的错误,而 `Swoole` 协程的解决方案对比于此就高明多了,在遇到 `I/O` 时底层自动的进行隐式协程切换,无需添加任何的额外语法,无需在代码前加上 `yield`,协程切换的过程无声无息,极大的减轻了维护异步系统的心智负担。 @@ -266,4 +269,4 @@ use Hyperf\Utils\Context; $request = Context::override(ServerRequestInterface::class, function (ServerRequestInterface $request) { return $request->withAddedHeader('foo', 'bar'); }); -``` \ No newline at end of file +``` diff --git a/doc/zh/db/model.md b/doc/zh/db/model.md index 7a6efb6bc..6e20f8118 100644 --- a/doc/zh/db/model.md +++ b/doc/zh/db/model.md @@ -14,7 +14,7 @@ $ php bin/hyperf.php gen:model table_name 1.0.* 版本: ``` -$ php bin/hyperf.php gen:model table_name +$ php bin/hyperf.php db:model table_name ``` 创建的模型如下 diff --git a/doc/zh/di.md b/doc/zh/di.md index 3174f3a70..72f7ab6e9 100644 --- a/doc/zh/di.md +++ b/doc/zh/di.md @@ -3,7 +3,7 @@ ## 简介 Hyperf 默认采用 [hyperf/di](https://github.com/hyperf-cloud/di) 作为框架的依赖注入管理容器,尽管从设计上我们允许您更换其它的依赖注入管理容器,但我们强烈不建议您更换该组件。 -[hyperf/di](https://github.com/hyperf-cloud/di) 是一个强大的用于管理类的依赖关并完成自动注入的组件,与传统依赖注入容器的区别在于更符合长生命周期的应用使用、提供了 [注解及注解注入](zh/annotation.md) 的支持、提供了无比强大的 [AOP 面向切面编程](zh/aop.md) 能力,这些能力及易用性作为 Hyperf 的核心输出,我们自信的认为该组件是最优秀的。 +[hyperf/di](https://github.com/hyperf-cloud/di) 是一个强大的用于管理类的依赖关系并完成自动注入的组件,与传统依赖注入容器的区别在于更符合长生命周期的应用使用、提供了 [注解及注解注入](zh/annotation.md) 的支持、提供了无比强大的 [AOP 面向切面编程](zh/aop.md) 能力,这些能力及易用性作为 Hyperf 的核心输出,我们自信的认为该组件是最优秀的。 ## 安装 diff --git a/doc/zh/json-rpc.md b/doc/zh/json-rpc.md index 43de11de3..260abc25b 100644 --- a/doc/zh/json-rpc.md +++ b/doc/zh/json-rpc.md @@ -57,7 +57,7 @@ class CalculatorService implements CalculatorServiceInterface `name` 属性为定义该服务的名称,这里定义一个全局唯一的名字即可,Hyperf 会根据该属性生成对应的 ID 注册到服务中心去; `protocol` 属性为定义该服务暴露的协议,目前仅支持 `jsonrpc` 和 `jsonrpc-http`,分别对应于 TCP 协议和 HTTP 协议下的两种协议,默认值为 `jsonrpc-http`,这里的值对应在 `Hyperf\Rpc\ProtocolManager` 里面注册的协议的 `key`,这两个本质上都是 JSON RPC 协议,区别在于数据格式化、数据打包、数据传输器等不同。 `server` 属性为绑定该服务类发布所要承载的 `Server`,默认值为 `jsonrpc-http`,该属性对应 `config/autoload/server.php` 文件内 `servers` 下所对应的 `name`,这里也就意味着我们需要定义一个对应的 `Server`,我们下一章节具体阐述这里应该怎样去处理; -`publishTo` 属性为定义该服务所要发布的服务中心,目前仅支持 `consul` 或为空,为空时代表不发布该服务到服务中心去,但也就意味着您需要手动处理服务发现的问题,当值为 `consul` 时需要对应配置好 [hyperf/consul](./consul.md) 组件的相关配置,要使用此功能需安装 [hyperf/service-governance](https://github.com/hyperf-cloud/service-governance) 组件,具体可参考 [服务注册](./service-register.md) 章节; +`publishTo` 属性为定义该服务所要发布的服务中心,目前仅支持 `consul` 或为空,为空时代表不发布该服务到服务中心去,但也就意味着您需要手动处理服务发现的问题,当值为 `consul` 时需要对应配置好 [hyperf/consul](./consul.md) 组件的相关配置,要使用此功能需安装 [hyperf/service-governance](https://github.com/hyperf-cloud/service-governance) 组件,具体可参考 [服务注册](zh/service-register.md) 章节; > 使用 `@RpcService` 注解需 `use Hyperf\RpcServer\Annotation\RpcService;` 命名空间。 diff --git a/doc/zh/middleware/middleware.md b/doc/zh/middleware/middleware.md index a3ab2d70a..4e004236e 100644 --- a/doc/zh/middleware/middleware.md +++ b/doc/zh/middleware/middleware.md @@ -73,6 +73,7 @@ Router::addGroup( ```php [ // 这里对应您当前的 Server 名称 'http' => [ - ValidationExceptionHandler::class, + \Hyperf\Validation\ValidationExceptionHandler::class, ], ], ]; diff --git a/src/amqp/publish/amqp.php b/src/amqp/publish/amqp.php index 331c76702..2c7d7277f 100644 --- a/src/amqp/publish/amqp.php +++ b/src/amqp/publish/amqp.php @@ -17,6 +17,9 @@ return [ 'user' => env('AMQP_USER', 'guest'), 'password' => env('AMQP_PASSWORD', 'guest'), 'vhost' => env('AMQP_VHOST', '/'), + 'concurrent' => [ + 'limit' => 1, + ], 'pool' => [ 'min_connections' => 1, 'max_connections' => 10, diff --git a/src/amqp/src/Consumer.php b/src/amqp/src/Consumer.php index 657b2d4db..770947df7 100644 --- a/src/amqp/src/Consumer.php +++ b/src/amqp/src/Consumer.php @@ -16,7 +16,9 @@ use Hyperf\Amqp\Exception\MessageException; use Hyperf\Amqp\Message\ConsumerMessageInterface; use Hyperf\Amqp\Message\MessageInterface; use Hyperf\Amqp\Pool\PoolFactory; +use Hyperf\Contract\ConfigInterface; use Hyperf\ExceptionHandler\Formatter\FormatterInterface; +use Hyperf\Utils\Coroutine\Concurrent; use PhpAmqpLib\Channel\AMQPChannel; use PhpAmqpLib\Message\AMQPMessage; use Psr\Container\ContainerInterface; @@ -52,6 +54,7 @@ class Consumer extends Builder $channel = $connection->getConfirmChannel(); $this->declare($consumerMessage, $channel); + $concurrent = $this->getConcurrent(); $channel->basic_consume( $consumerMessage->getQueue(), @@ -60,41 +63,13 @@ class Consumer extends Builder false, false, false, - function (AMQPMessage $message) use ($consumerMessage) { - $data = $consumerMessage->unserialize($message->getBody()); - /** @var AMQPChannel $channel */ - $channel = $message->delivery_info['channel']; - $deliveryTag = $message->delivery_info['delivery_tag']; - [$result] = parallel([function () use ($consumerMessage, $data) { - try { - return $consumerMessage->consume($data); - } catch (Throwable $exception) { - if ($this->container->has(FormatterInterface::class)) { - $formatter = $this->container->get(FormatterInterface::class); - $this->logger->error($formatter->format($exception)); - } else { - $this->logger->error($exception->getMessage()); - } - - return Result::DROP; - } - }]); - - if ($result === Result::ACK) { - $this->logger->debug($deliveryTag . ' acked.'); - return $channel->basic_ack($deliveryTag); - } - if ($result === Result::NACK) { - $this->logger->debug($deliveryTag . ' uacked.'); - return $channel->basic_nack($deliveryTag); - } - if ($consumerMessage->isRequeue() && $result === Result::REQUEUE) { - $this->logger->debug($deliveryTag . ' requeued.'); - return $channel->basic_reject($deliveryTag, true); + function (AMQPMessage $message) use ($consumerMessage, $concurrent) { + $callback = $this->getCallback($consumerMessage, $message); + if (! $concurrent instanceof Concurrent) { + return parallel([$callback]); } - $this->logger->debug($deliveryTag . ' rejected.'); - $channel->basic_reject($deliveryTag, false); + return $concurrent->create($callback); } ); @@ -129,4 +104,54 @@ class Consumer extends Builder $channel->queue_bind($message->getQueue(), $message->getExchange(), $routingKey); } } + + protected function getConcurrent(): ?Concurrent + { + $config = $this->container->get(ConfigInterface::class); + $concurrent = (int) $config->get('amqp.' . $this->name . '.concurrent.limit', 0); + if ($concurrent > 1) { + return new Concurrent($concurrent); + } + + return null; + } + + protected function getCallback(ConsumerMessageInterface $consumerMessage, AMQPMessage $message) + { + return function () use ($consumerMessage, $message) { + $data = $consumerMessage->unserialize($message->getBody()); + /** @var AMQPChannel $channel */ + $channel = $message->delivery_info['channel']; + $deliveryTag = $message->delivery_info['delivery_tag']; + + try { + $result = $consumerMessage->consume($data); + } catch (Throwable $exception) { + if ($this->container->has(FormatterInterface::class)) { + $formatter = $this->container->get(FormatterInterface::class); + $this->logger->error($formatter->format($exception)); + } else { + $this->logger->error($exception->getMessage()); + } + + $result = Result::DROP; + } + + if ($result === Result::ACK) { + $this->logger->debug($deliveryTag . ' acked.'); + return $channel->basic_ack($deliveryTag); + } + if ($result === Result::NACK) { + $this->logger->debug($deliveryTag . ' uacked.'); + return $channel->basic_nack($deliveryTag); + } + if ($consumerMessage->isRequeue() && $result === Result::REQUEUE) { + $this->logger->debug($deliveryTag . ' requeued.'); + return $channel->basic_reject($deliveryTag, true); + } + + $this->logger->debug($deliveryTag . ' rejected.'); + return $channel->basic_reject($deliveryTag, false); + }; + } } diff --git a/src/async-queue/src/Command/InfoCommand.php b/src/async-queue/src/Command/InfoCommand.php index 0d09c104a..c375aa91a 100644 --- a/src/async-queue/src/Command/InfoCommand.php +++ b/src/async-queue/src/Command/InfoCommand.php @@ -48,7 +48,7 @@ class InfoCommand extends HyperfCommand protected function configure() { - $this->setDescription('Delete all message from failed queue.'); + $this->setDescription('Get all messages from the queue.'); $this->addArgument('name', InputArgument::OPTIONAL, 'The name of queue.', 'default'); } } diff --git a/src/database/src/Commands/Seeders/SeedCommand.php b/src/database/src/Commands/Seeders/SeedCommand.php index 2ddfec98e..93e387e53 100644 --- a/src/database/src/Commands/Seeders/SeedCommand.php +++ b/src/database/src/Commands/Seeders/SeedCommand.php @@ -64,7 +64,7 @@ class SeedCommand extends BaseCommand $this->seed->setOutput($this->output); - if ($this->input->hasOption('database')) { + if ($this->input->hasOption('database') && $this->input->getOption('database')) { $this->seed->setConnection($this->input->getOption('database')); } diff --git a/src/di/src/Annotation/Scanner.php b/src/di/src/Annotation/Scanner.php index 2d66eb3ee..3ec5769f2 100644 --- a/src/di/src/Annotation/Scanner.php +++ b/src/di/src/Annotation/Scanner.php @@ -25,18 +25,16 @@ class Scanner */ private $parser; - /** - * @var array - */ - private $ignoreAnnotations = []; - public function __construct(array $ignoreAnnotations = ['mixin']) { $this->parser = new Ast(); - $this->ignoreAnnotations = $ignoreAnnotations; // TODO: this method is deprecated and will be removed in doctrine/annotations 2.0 AnnotationRegistry::registerLoader('class_exists'); + + foreach ($ignoreAnnotations as $annotation) { + AnnotationReader::addGlobalIgnoredName($annotation); + } } public function scan(array $paths): array @@ -49,9 +47,6 @@ class Scanner $finder = new Finder(); $finder->files()->in($paths)->name('*.php'); - array_walk($this->ignoreAnnotations, function ($value) { - AnnotationReader::addGlobalIgnoredName($value); - }); $meta = []; foreach ($finder as $file) { try { diff --git a/src/di/tests/AnnotationTest.php b/src/di/tests/AnnotationTest.php new file mode 100644 index 000000000..58a035333 --- /dev/null +++ b/src/di/tests/AnnotationTest.php @@ -0,0 +1,41 @@ +collect([Ignore::class]); + $annotations = AnnotationCollector::get(Ignore::class . '._c'); + $this->assertArrayHasKey(IgnoreDemoAnnotation::class, $annotations); + + AnnotationCollector::clear(); + + $scaner = new Scanner(['IgnoreDemoAnnotation']); + $scaner->collect([Ignore::class]); + $annotations = AnnotationCollector::get(Ignore::class . '._c'); + $this->assertNull($annotations); + } +} diff --git a/src/di/tests/Stub/Ignore.php b/src/di/tests/Stub/Ignore.php new file mode 100644 index 000000000..225643047 --- /dev/null +++ b/src/di/tests/Stub/Ignore.php @@ -0,0 +1,20 @@ +all(); + return array_merge_recursive($this->all(), $this->getUploadedFiles()); } /** diff --git a/src/validation/tests/Cases/FormRequestTest.php b/src/validation/tests/Cases/FormRequestTest.php new file mode 100644 index 000000000..835518b17 --- /dev/null +++ b/src/validation/tests/Cases/FormRequestTest.php @@ -0,0 +1,71 @@ +shouldReceive('getUploadedFiles')->andReturn([ + 'file' => $file, + ]); + $psrRequest->shouldReceive('getParsedBody')->andReturn([ + 'id' => 1, + ]); + $psrRequest->shouldReceive('getQueryParams')->andReturn([]); + + Context::set(ServerRequestInterface::class, $psrRequest); + $request = new DemoRequest(Mockery::mock(ContainerInterface::class)); + + $this->assertEquals(['id' => 1, 'file' => $file], $request->getValidationData()); + } + + public function testRequestValidationDataWithSameKey() + { + $psrRequest = Mockery::mock(ServerRequestInterface::class); + $file = new UploadedFile('/tmp/tmp_name', 32, 0); + $psrRequest->shouldReceive('getUploadedFiles')->andReturn([ + 'file' => [$file], + ]); + $psrRequest->shouldReceive('getParsedBody')->andReturn([ + 'file' => ['Invalid File.'], + ]); + $psrRequest->shouldReceive('getQueryParams')->andReturn([]); + + Context::set(ServerRequestInterface::class, $psrRequest); + $request = new DemoRequest(Mockery::mock(ContainerInterface::class)); + + $this->assertEquals(['file' => ['Invalid File.', $file]], $request->getValidationData()); + } +} diff --git a/src/validation/tests/Cases/Stub/DemoRequest.php b/src/validation/tests/Cases/Stub/DemoRequest.php index e66cfd9c3..b3cd55c0a 100644 --- a/src/validation/tests/Cases/Stub/DemoRequest.php +++ b/src/validation/tests/Cases/Stub/DemoRequest.php @@ -40,6 +40,11 @@ class DemoRequest extends FormRequest ]; } + public function getValidationData() + { + return parent::validationData(); + } + protected function withValidator($request) { Context::override('test.validation.DemoRequest.number', function ($id) {