├── .gitattributes
├── .gitignore
├── .phpstorm.meta.php
├── README.md
├── src
├── Exception
│ ├── NotFoundException.php
│ ├── RuntimeException.php
│ └── InvalidArgumentException.php
├── Reflection
│ └── Functions.php
├── RateLimit.php
├── Caster
│ ├── IntegerItemsCaster.php
│ ├── StringItemsCaster.php
│ ├── SafeDateTimeCaster.php
│ ├── StringItems.php
│ └── IntegerItems.php
├── ConfigProvider.php
├── Utils
│ ├── Date.php
│ ├── Sorter.php
│ └── Model.php
├── Schema
│ └── Continuous.php
├── Service.php
├── Test
│ └── FastMockery.php
├── HTTP
│ ├── RetryMiddleware.php
│ └── Guzzle.php
├── Factory.php
├── ContextInstance.php
├── Middleware
│ ├── DebugMiddleware.php
│ ├── DebugDeferMiddleware.php
│ └── RequestHandledDebugMiddleware.php
├── Functions.php
├── ElasticSearch.php
└── ElasticSearch
│ └── Search7.php
├── phpunit.xml
├── .travis.yml
├── composer.json
└── .php-cs-fixer.php
/.gitattributes:
--------------------------------------------------------------------------------
1 | /tests export-ignore
2 | /.github export-ignore
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | composer.lock
3 | *.cache
4 | *.log
5 | .idea/
6 | runtime/
7 |
--------------------------------------------------------------------------------
/.phpstorm.meta.php:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 | ./tests/
14 |
15 |
--------------------------------------------------------------------------------
/src/RateLimit.php:
--------------------------------------------------------------------------------
1 | isRateLimit->__invoke(...$params)) {
28 | usleep($ms * 1000);
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Caster/IntegerItemsCaster.php:
--------------------------------------------------------------------------------
1 | [
21 | ],
22 | 'commands' => [
23 | ],
24 | 'annotations' => [
25 | 'scan' => [
26 | 'paths' => [
27 | __DIR__,
28 | ],
29 | ],
30 | ],
31 | ];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/Utils/Date.php:
--------------------------------------------------------------------------------
1 | continuous;
31 | }
32 |
33 | /**
34 | * 是否为空.
35 | */
36 | public function isEmpty(): bool
37 | {
38 | return $this->empty;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Utils/Sorter.php:
--------------------------------------------------------------------------------
1 | insert($item, $priority);
31 | }
32 | return $queue;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Service.php:
--------------------------------------------------------------------------------
1 | container = $container;
38 | $this->logger = $container->get(StdoutLoggerInterface::class);
39 | $this->factory = $container->get(Factory::class);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Test/FastMockery.php:
--------------------------------------------------------------------------------
1 | container = $container;
24 | }
25 |
26 | /**
27 | * @param mixed $value
28 | */
29 | public function run(string $key, $value, callable $callable): void
30 | {
31 | $entry = $this->container->get($key);
32 | try {
33 | $this->container->set($key, $value);
34 | $callable();
35 | } finally {
36 | $this->container->set($key, $entry);
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Caster/SafeDateTimeCaster.php:
--------------------------------------------------------------------------------
1 | toDateTimeString();
35 | }
36 |
37 | if (! empty($value)) {
38 | return $value;
39 | }
40 |
41 | /* @phpstan-ignore-next-line */
42 | return null;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | sudo: required
4 |
5 | matrix:
6 | include:
7 | - php: 7.3
8 | env: SW_VERSION="4.5.2"
9 | - php: 7.4
10 | env: SW_VERSION="4.5.2"
11 | - php: master
12 | env: SW_VERSION="4.5.2"
13 |
14 | allow_failures:
15 | - php: master
16 |
17 | services:
18 | - mysql
19 | - redis-server
20 | - docker
21 |
22 | before_install:
23 | - export PHP_MAJOR="$(`phpenv which php` -r 'echo phpversion();' | cut -d '.' -f 1)"
24 | - export PHP_MINOR="$(`phpenv which php` -r 'echo phpversion();' | cut -d '.' -f 2)"
25 | - echo $PHP_MAJOR
26 | - echo $PHP_MINOR
27 |
28 | install:
29 | - cd $TRAVIS_BUILD_DIR
30 | - bash ./tests/swoole.install.sh
31 | - phpenv config-rm xdebug.ini || echo "xdebug not available"
32 | - phpenv config-add ./tests/ci.ini
33 | - docker run -d -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" --name elasticsearch elasticsearch:5-alpine
34 |
35 | before_script:
36 | - cd $TRAVIS_BUILD_DIR
37 | - composer config -g process-timeout 900 && composer update
38 | - php tests/setup_elasticsearch.php
39 |
40 | script:
41 | - composer analyse
42 | - composer test
43 |
--------------------------------------------------------------------------------
/src/HTTP/RetryMiddleware.php:
--------------------------------------------------------------------------------
1 | isOk($response) && $retries < $this->retries) {
29 | if ($name) {
30 | $formatter = new MessageFormatter(MessageFormatter::DEBUG);
31 | app()->get(LoggerFactory::class)->get($name)->warning($formatter->format($request, $response));
32 | }
33 |
34 | return true;
35 | }
36 | return false;
37 | }, function () {
38 | return $this->delay;
39 | });
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Factory.php:
--------------------------------------------------------------------------------
1 | items = [
34 | 'model' => $container->get(Model::class),
35 | 'date' => $container->get(Date::class),
36 | 'sorter' => $container->get(Sorter::class),
37 | ];
38 | }
39 |
40 | public function __get($name)
41 | {
42 | if (! isset($this->items[$name])) {
43 | throw new NotFoundException(sprintf('%s is not found.', $name));
44 | }
45 |
46 | return $this->items[$name];
47 | }
48 |
49 | public function __set($name, $value)
50 | {
51 | throw new RuntimeException('set is invalid.');
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Caster/StringItems.php:
--------------------------------------------------------------------------------
1 | items = array_values(array_unique(array_filter($items)));
25 | }
26 |
27 | public function __toString(): string
28 | {
29 | if ($string = implode(',', $this->toArray())) {
30 | return ',' . $string . ',';
31 | }
32 | return '';
33 | }
34 |
35 | public static function makeFromString(string $tagString)
36 | {
37 | $tags = explode(',', $tagString);
38 | return new StringItems($tags);
39 | }
40 |
41 | public function toArray(): array
42 | {
43 | return $this->items;
44 | }
45 |
46 | public function add($data)
47 | {
48 | if (! in_array($data, $this->items)) {
49 | $this->items[] = $data;
50 | }
51 | }
52 |
53 | public function count(): int
54 | {
55 | return count($this->items);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Caster/IntegerItems.php:
--------------------------------------------------------------------------------
1 | items = array_values(array_unique($integers));
34 | }
35 |
36 | public function __toString(): string
37 | {
38 | if ($string = implode(',', $this->toArray())) {
39 | return ',' . $string . ',';
40 | }
41 |
42 | return '';
43 | }
44 |
45 | public static function makeFromString(string $string)
46 | {
47 | return new IntegerItems(explode(',', $string));
48 | }
49 |
50 | public function toArray(): array
51 | {
52 | return $this->items;
53 | }
54 |
55 | /**
56 | * @return $this
57 | */
58 | public function remove(int $id)
59 | {
60 | $index = array_search($id, $this->items);
61 | if (is_int($index)) {
62 | unset($this->items[$index]);
63 | $this->items = array_values(array_unique($this->items));
64 | }
65 | return $this;
66 | }
67 |
68 | /**
69 | * @return $this
70 | */
71 | public function insert(int $id)
72 | {
73 | $this->items[] = $id;
74 | $this->items = array_values(array_unique($this->items));
75 | return $this;
76 | }
77 |
78 | public function count(): int
79 | {
80 | return count($this->items);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/ContextInstance.php:
--------------------------------------------------------------------------------
1 | ids);
34 |
35 | if (empty($diff)) {
36 | return $this;
37 | }
38 |
39 | $this->ids = array_merge($this->ids, $diff);
40 |
41 | $this->mergeModels($this->initModels($diff));
42 |
43 | return $this;
44 | }
45 |
46 | public function first($id, bool $init = false)
47 | {
48 | if ($init && ! isset($this->models[$id])) {
49 | $this->init([$id]);
50 | }
51 | return $this->models[$id] ?? null;
52 | }
53 |
54 | public function find(array $ids): array
55 | {
56 | $result = [];
57 | foreach ($ids as $id) {
58 | $result[$id] = $this->models[$id] ?? null;
59 | }
60 | return $result;
61 | }
62 |
63 | public function all(): array
64 | {
65 | return $this->models;
66 | }
67 |
68 | public function exist($id): bool
69 | {
70 | return isset($this->models[$id]);
71 | }
72 |
73 | public function getCount(): int
74 | {
75 | return count($this->models);
76 | }
77 |
78 | public function mergeModels($models): void
79 | {
80 | foreach ($models as $key => $model) {
81 | if ($this->key) {
82 | $this->models[$model->{$this->key}] = $model;
83 | } else {
84 | $this->models[$key] = $model;
85 | }
86 | }
87 | }
88 |
89 | abstract protected function initModels(array $ids): array;
90 | }
91 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "limingxinleo/hyperf-utils",
3 | "type": "library",
4 | "license": "MIT",
5 | "keywords": [
6 | "php",
7 | "hyperf"
8 | ],
9 | "description": "Utils for Hyperf.",
10 | "autoload": {
11 | "psr-4": {
12 | "Han\\Utils\\": "src/"
13 | },
14 | "files": [
15 | "src/Functions.php",
16 | "src/Reflection/Functions.php"
17 | ]
18 | },
19 | "autoload-dev": {
20 | "psr-4": {
21 | "HyperfTest\\": "tests"
22 | }
23 | },
24 | "require": {
25 | "php": ">=8.1",
26 | "hyperf/collection": "^3.0",
27 | "hyperf/context": "^3.0",
28 | "jetbrains/phpstorm-attributes": "^1.0",
29 | "nesbot/carbon": "^2.0",
30 | "psr/container": "^1.0|^2.0"
31 | },
32 | "require-dev": {
33 | "elasticsearch/elasticsearch": "^7.8",
34 | "friendsofphp/php-cs-fixer": "^3.0",
35 | "hyperf/config": "^3.0",
36 | "hyperf/database": "^3.0",
37 | "hyperf/framework": "^3.0",
38 | "hyperf/guzzle": "^3.0",
39 | "hyperf/logger": "^3.0",
40 | "hyperf/testing": "^3.0",
41 | "mockery/mockery": "^1.3",
42 | "phpstan/phpstan": "^1.0",
43 | "swoole/ide-helper": "dev-master"
44 | },
45 | "suggest": {
46 | "elasticsearch/elasticsearch": "Required to use ElasticSearch. (^7.0)",
47 | "hyperf/guzzle": "Required to use ElasticSearch. (^2.1)",
48 | "hyperf/framework": "Required to use ElasticSearch. (^2.1)",
49 | "hyperf/logger": "Required to use DebugMiddleware. (^2.1)",
50 | "hyperf/http-server": "Required to use DebugMiddleware. (^2.1)",
51 | "mockery/mockery": "Required to use FastMockery. (^1.0)"
52 | },
53 | "minimum-stability": "dev",
54 | "prefer-stable": true,
55 | "config": {
56 | "sort-packages": true
57 | },
58 | "scripts": {
59 | "test": "co-phpunit -c phpunit.xml --colors=always",
60 | "analyse": "phpstan analyse --memory-limit 300M -l 5 ./src",
61 | "cs-fix": "php-cs-fixer fix $1"
62 | },
63 | "extra": {
64 | "branch-alias": {
65 | "dev-master": "3.6-dev"
66 | },
67 | "hyperf": {
68 | "config": "Han\\Utils\\ConfigProvider"
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Utils/Model.php:
--------------------------------------------------------------------------------
1 | count();
29 | if ($limit > 0) {
30 | $items = $builder->offset($offset)->limit($limit)->get($columns);
31 | } else {
32 | $items = new Collection([]);
33 | }
34 |
35 | return [$count, $items];
36 | }
37 |
38 | public function query(Builder $builder, $offset = 0, $limit = 10, $columns = ['*'])
39 | {
40 | return $builder->offset($offset)->limit($limit)->get($columns);
41 | }
42 |
43 | public function loadCache(BaseModel $model, array $relations = []): BaseModel
44 | {
45 | tap(new Collection([$model]), static function (Collection $col) use ($relations) {
46 | /* @phpstan-ignore-next-line */
47 | $col->loadCache($relations);
48 | });
49 |
50 | return $model;
51 | }
52 |
53 | /**
54 | * Returns only the columns from the collection with the specified keys.
55 | *
56 | * @param null|array|string $keys
57 | */
58 | public function columns(Collection $items, $keys): BaseCollection
59 | {
60 | if (is_null($keys)) {
61 | return new BaseCollection([]);
62 | }
63 | $result = [];
64 | $isSingleColumn = is_string($keys);
65 | foreach ($items as $item) {
66 | if ($isSingleColumn) {
67 | $value = $item->{$keys} ?? null;
68 | $result[] = $value instanceof Arrayable ? $value->toArray() : $value;
69 | } else {
70 | $result[] = value(static function () use ($item, $keys) {
71 | $res = [];
72 | foreach ($keys as $key) {
73 | $value = $item->{$key} ?? null;
74 | $res[$key] = $value instanceof Arrayable ? $value->toArray() : $value;
75 | }
76 |
77 | return $res;
78 | });
79 | }
80 | }
81 |
82 | return new BaseCollection($result);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Middleware/DebugMiddleware.php:
--------------------------------------------------------------------------------
1 | container = $container;
34 | }
35 |
36 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
37 | {
38 | $time = microtime(true);
39 | try {
40 | $response = $handler->handle($request);
41 | } catch (\Throwable $exception) {
42 | throw $exception;
43 | } finally {
44 | $logger = $this->container->get(LoggerFactory::class)->get('request');
45 |
46 | // 日志
47 | $time = microtime(true) - $time;
48 | $debug = 'URI: ' . $request->getUri()->getPath() . PHP_EOL;
49 | $debug .= 'TIME: ' . $time . PHP_EOL;
50 | if ($customData = $this->getCustomData()) {
51 | $debug .= 'DATA: ' . $customData . PHP_EOL;
52 | }
53 | $debug .= 'REQUEST: ' . $this->getRequestString($request) . PHP_EOL;
54 | if (isset($response)) {
55 | $debug .= 'RESPONSE: ' . $this->getResponseString($response) . PHP_EOL;
56 | }
57 | if (isset($exception) && $exception instanceof \Throwable) {
58 | $debug .= 'EXCEPTION: ' . $exception->getMessage() . PHP_EOL;
59 | }
60 |
61 | if ($time > 1) {
62 | $logger->error($debug);
63 | } else {
64 | $logger->info($debug);
65 | }
66 | }
67 |
68 | return $response;
69 | }
70 |
71 | protected function getResponseString(ResponseInterface $response): string
72 | {
73 | return (string) $response->getBody();
74 | }
75 |
76 | protected function getRequestString(ServerRequestInterface $request): string
77 | {
78 | $data = $this->container->get(Request::class)->all();
79 |
80 | return Json::encode($data);
81 | }
82 |
83 | protected function getCustomData(): string
84 | {
85 | return '';
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/.php-cs-fixer.php:
--------------------------------------------------------------------------------
1 | setRiskyAllowed(true)
14 | ->setRules([
15 | '@PSR2' => true,
16 | '@Symfony' => true,
17 | '@DoctrineAnnotation' => true,
18 | '@PhpCsFixer' => true,
19 | 'header_comment' => [
20 | 'comment_type' => 'PHPDoc',
21 | 'header' => $header,
22 | 'separate' => 'none',
23 | 'location' => 'after_declare_strict',
24 | ],
25 | 'array_syntax' => [
26 | 'syntax' => 'short'
27 | ],
28 | 'list_syntax' => [
29 | 'syntax' => 'short'
30 | ],
31 | 'concat_space' => [
32 | 'spacing' => 'one'
33 | ],
34 | 'blank_line_before_statement' => [
35 | 'statements' => [
36 | 'declare',
37 | ],
38 | ],
39 | 'general_phpdoc_annotation_remove' => [
40 | 'annotations' => [
41 | 'author'
42 | ],
43 | ],
44 | 'ordered_imports' => [
45 | 'imports_order' => [
46 | 'class', 'function', 'const',
47 | ],
48 | 'sort_algorithm' => 'alpha',
49 | ],
50 | 'single_line_comment_style' => [
51 | 'comment_types' => [
52 | ],
53 | ],
54 | 'yoda_style' => [
55 | 'always_move_variable' => false,
56 | 'equal' => false,
57 | 'identical' => false,
58 | ],
59 | 'phpdoc_align' => [
60 | 'align' => 'left',
61 | ],
62 | 'multiline_whitespace_before_semicolons' => [
63 | 'strategy' => 'no_multi_line',
64 | ],
65 | 'constant_case' => [
66 | 'case' => 'lower',
67 | ],
68 | 'class_attributes_separation' => true,
69 | 'combine_consecutive_unsets' => true,
70 | 'declare_strict_types' => true,
71 | 'linebreak_after_opening_tag' => true,
72 | 'lowercase_static_reference' => true,
73 | 'no_useless_else' => true,
74 | 'no_unused_imports' => true,
75 | 'not_operator_with_successor_space' => true,
76 | 'not_operator_with_space' => false,
77 | 'ordered_class_elements' => true,
78 | 'php_unit_strict' => false,
79 | 'phpdoc_separation' => false,
80 | 'single_quote' => true,
81 | 'standardize_not_equals' => true,
82 | 'multiline_comment_opening_closing' => true,
83 | 'single_line_empty_body' => false,
84 | ])
85 | ->setFinder(
86 | PhpCsFixer\Finder::create()
87 | ->exclude('vendor')
88 | ->in(__DIR__)
89 | )
90 | ->setUsingCache(false);
91 |
--------------------------------------------------------------------------------
/src/Middleware/DebugDeferMiddleware.php:
--------------------------------------------------------------------------------
1 | container = $container;
35 | }
36 |
37 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
38 | {
39 | $time = microtime(true);
40 |
41 | defer(function () use ($time, $request) {
42 | try {
43 | $this->log($time, $request, Context::get(ResponseInterface::class));
44 | } catch (\Throwable $exception) {
45 | }
46 | });
47 |
48 | return $handler->handle($request);
49 | }
50 |
51 | protected function log(float $startTime, ?ServerRequestInterface $request, ?ResponseInterface $response): void
52 | {
53 | $logger = $this->container->get(LoggerFactory::class)->get('request');
54 |
55 | // 日志
56 | $time = microtime(true) - $startTime;
57 | $debug = 'URI: ' . $request->getUri()->getPath() . PHP_EOL;
58 | $debug .= 'TIME: ' . $time . PHP_EOL;
59 | if ($customData = $this->getCustomData()) {
60 | $debug .= 'DATA: ' . $customData . PHP_EOL;
61 | }
62 | if (isset($request)) {
63 | $debug .= 'REQUEST_HEADERS: ' . Json::encode($request->getHeaders()) . PHP_EOL;
64 | $debug .= 'REQUEST_BODY: ' . $this->getRequestString($request) . PHP_EOL;
65 | }
66 |
67 | if (isset($response)) {
68 | $debug .= 'RESPONSE: ' . $this->getResponseString($response) . PHP_EOL;
69 | }
70 |
71 | if ($time > 1) {
72 | $logger->error($debug);
73 | } else {
74 | $logger->info($debug);
75 | }
76 | }
77 |
78 | protected function getResponseString(ResponseInterface $response): string
79 | {
80 | return (string) $response->getBody();
81 | }
82 |
83 | protected function getRequestString(ServerRequestInterface $request): string
84 | {
85 | $data = $this->container->get(Request::class)->all();
86 |
87 | return Json::encode($data);
88 | }
89 |
90 | protected function getCustomData(): string
91 | {
92 | return '';
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/Middleware/RequestHandledDebugMiddleware.php:
--------------------------------------------------------------------------------
1 | container = $container;
32 | }
33 |
34 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
35 | {
36 | $time = microtime(true);
37 | try {
38 | $response = $handler->handle($request);
39 | } catch (\Throwable $exception) {
40 | throw $exception;
41 | } finally {
42 | $logger = $this->container->get(LoggerFactory::class)->get('request');
43 |
44 | // 日志
45 | $time = microtime(true) - $time;
46 | $debug = $request->getMethod() . ' ' . (string) $request->getUri() . PHP_EOL;
47 | $debug .= 'TIME: ' . $time . PHP_EOL;
48 | $debug .= $this->getRequestString($request) . PHP_EOL;
49 | if (isset($response)) {
50 | $debug .= 'RESPONSE: ' . $this->getResponseString($response) . PHP_EOL;
51 | }
52 | if (isset($exception) && $exception instanceof \Throwable) {
53 | $debug .= 'EXCEPTION: ' . $exception->getMessage() . PHP_EOL;
54 | }
55 |
56 | if ($time > $this->getTimeout($request)) {
57 | $logger->error($debug);
58 | } else {
59 | $logger->info($debug);
60 | }
61 | }
62 |
63 | return $response;
64 | }
65 |
66 | protected function getTimeout(ServerRequestInterface $request): float
67 | {
68 | return 1.0;
69 | }
70 |
71 | protected function getResponseString(ResponseInterface $response): string
72 | {
73 | return (string) $response->getBody();
74 | }
75 |
76 | protected function getRequestString(ServerRequestInterface $request): string
77 | {
78 | $result = '';
79 | foreach ($request->getHeaders() as $header => $values) {
80 | foreach ((array) $values as $value) {
81 | $result .= $header . ': ' . $value . PHP_EOL;
82 | }
83 | }
84 |
85 | if (! str_contains($request->getHeaderLine('Content-Type'), 'multipart/form-data')) {
86 | $result .= (string) $request->getBody();
87 | } else {
88 | $result .= 'The body contains boundary data, ignore it.';
89 | }
90 |
91 | return $result;
92 | }
93 |
94 | protected function getCustomData(): string
95 | {
96 | return '';
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/HTTP/Guzzle.php:
--------------------------------------------------------------------------------
1 | get(LoggerFactory::class)->get($name), $formatter);
37 | }
38 |
39 | public function retryMiddleware(?string $name = null): callable
40 | {
41 | return app()->get(RetryMiddleware::class)->getMiddleware($name);
42 | }
43 |
44 | public function initMiddlewares(HandlerStack $stack, string $logName = 'http'): HandlerStack
45 | {
46 | $stack->push($this->retryMiddleware($logName), 'retry');
47 | $stack->push($this->logMiddleware($logName), 'log');
48 |
49 | return $stack;
50 | }
51 |
52 | public function initRetryAndDurationMiddleware(HandlerStack $stack, string $logName = 'http'): HandlerStack
53 | {
54 | $stack->push($this->retryMiddleware($logName), 'retry');
55 |
56 | $formatter = new MessageFormatter(MessageFormatter::DEBUG);
57 | $stack->push(static::log(app()->get(LoggerFactory::class)->get($logName), $formatter));
58 |
59 | return $stack;
60 | }
61 |
62 | /**
63 | * Middleware that logs requests, responses, and errors using a message
64 | * formatter.
65 | *
66 | * @phpstan-param LogLevel::* $logLevel Level at which to log requests.
67 | *
68 | * @param LoggerInterface $logger logs messages
69 | * @param MessageFormatterInterface $formatter formatter used to create message strings
70 | * @param string $logLevel level at which to log requests
71 | *
72 | * @return callable returns a function that accepts the next handler
73 | */
74 | public static function log(LoggerInterface $logger, MessageFormatterInterface $formatter, string $logLevel = 'info'): callable
75 | {
76 | return static function (callable $handler) use ($logger, $formatter, $logLevel): callable {
77 | return static function (RequestInterface $request, array $options = []) use ($handler, $logger, $formatter, $logLevel) {
78 | $ms = microtime(true);
79 | return $handler($request, $options)->then(
80 | static function ($response) use ($logger, $request, $formatter, $logLevel, $ms): ResponseInterface {
81 | $message = $formatter->format($request, $response);
82 | $logger->log($logLevel, $message, [
83 | 'duration' => microtime(true) - $ms,
84 | ]);
85 |
86 | return $response;
87 | },
88 | static function ($reason) use ($logger, $request, $formatter): PromiseInterface {
89 | $response = $reason instanceof RequestException ? $reason->getResponse() : null;
90 | $message = $formatter->format($request, $response, P\Create::exceptionFor($reason));
91 | $logger->error($message);
92 |
93 | return P\Create::rejectionFor($reason);
94 | }
95 | );
96 | };
97 | };
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/Functions.php:
--------------------------------------------------------------------------------
1 | get(Date::class)->load($date);
43 | }
44 |
45 | /**
46 | * @param array|\Traversable $items
47 | */
48 | function sort($items, callable $callable): SplPriorityQueue
49 | {
50 | return app()->get(Sorter::class)->sort($items, $callable);
51 | }
52 |
53 | /**
54 | * @param array|\Traversable $items
55 | */
56 | function spl_sort($items, callable $callable): SplPriorityQueue
57 | {
58 | return app()->get(Sorter::class)->sort($items, $callable);
59 | }
60 |
61 | /**
62 | * Finds an entry of the container by its identifier and returns it.
63 | * @param null|string $id
64 | * @return ContainerInterface|mixed
65 | */
66 | function app($id = null)
67 | {
68 | $container = ApplicationContext::getContainer();
69 | if ($id) {
70 | return $container->get($id);
71 | }
72 |
73 | return $container;
74 | }
75 |
76 | function csv_open(string $path)
77 | {
78 | $dirname = dirname($path);
79 | if (! is_dir($dirname)) {
80 | @mkdir($dirname, 0775, true);
81 | }
82 |
83 | $fp = fopen($path, 'w+');
84 | if (! $fp) {
85 | throw new RuntimeException('Csv init failed.');
86 | }
87 |
88 | fwrite($fp, chr(0xEF) . chr(0xBB) . chr(0xBF));
89 |
90 | return $fp;
91 | }
92 |
93 | function safe_path(string $path): string
94 | {
95 | $dirname = dirname($path);
96 | if (! is_dir($dirname)) {
97 | @mkdir($dirname, 0775, true);
98 | }
99 |
100 | return $path;
101 | }
102 |
103 | function safe_execute(callable $callable)
104 | {
105 | try {
106 | $callable();
107 | } catch (\Throwable) {
108 | }
109 | }
110 |
111 | /**
112 | * 判断模型是否被修改过.
113 | * @param array $expect 被排除检测的 key 列表
114 | */
115 | function model_is_dirty(Model $model, array $expect = []): bool
116 | {
117 | $dirty = $model->getDirty();
118 |
119 | Arr::forget($dirty, $expect);
120 |
121 | return ! empty($dirty);
122 | }
123 |
124 | /**
125 | * WARN: 注意,参数会被 urldecode
126 | * 根据 http query 解析成数组.
127 | */
128 | function http_parse_query(string $query): array
129 | {
130 | parse_str($query, $result);
131 | return $result;
132 | }
133 |
134 | /**
135 | * 移除 Uri 中某个参数.
136 | */
137 | function unset_uri_param(Uri $uri, string $key): Uri
138 | {
139 | $query = $uri->getQuery();
140 | $params = http_parse_query($query);
141 | unset($params[$key]);
142 |
143 | return $uri->withQuery(http_build_query($params));
144 | }
145 |
146 | /**
147 | * 判断数组是否前后连续.
148 | * @param array $array = [[1,5], [5,6], [6,10]]
149 | */
150 | function is_continuous(array $array): Continuous
151 | {
152 | if (! $array) {
153 | return new Continuous(false, empty: true);
154 | }
155 |
156 | $queue = new SplPriorityQueue();
157 | foreach ($array as $item) {
158 | $queue->insert($item, $item[1]);
159 | }
160 |
161 | $min = $max = null;
162 | foreach ($queue as $item) {
163 | if ($max === null) {
164 | $min = $item[0];
165 | $max = $item[1];
166 | continue;
167 | }
168 |
169 | if ($min > $item[1]) {
170 | return new Continuous(false);
171 | }
172 |
173 | $min = min($min, $item[0]);
174 | }
175 |
176 | return new Continuous(true, $min, $max);
177 | }
178 |
179 | function get_user_ip(string $default = ''): string
180 | {
181 | $request = RequestContext::getOrNull();
182 | if (! $request) {
183 | return $default;
184 | }
185 |
186 | $ip = $request->getHeaderLine('x-forwarded-for');
187 | if (! empty($ip)) {
188 | $ip = trim(explode(',', $ip)[0] ?? '');
189 | }
190 |
191 | if (! $ip) {
192 | $ip = $request->getHeaderLine('x-real-ip');
193 | }
194 |
195 | return $ip ?: $default;
196 | }
197 |
--------------------------------------------------------------------------------
/src/ElasticSearch.php:
--------------------------------------------------------------------------------
1 | container = $container;
58 | $this->config = $container->get(ConfigInterface::class);
59 |
60 | $config = $this->config->get('elasticsearch.default');
61 | if (empty($config['host'])) {
62 | throw new InvalidArgumentException('搜索引擎配置不存在');
63 | }
64 |
65 | $this->hosts = (array) $config['host'];
66 | }
67 |
68 | public function client(): Client
69 | {
70 | if (! $this->client instanceof Client) {
71 | $this->client = ClientBuilder::create()
72 | ->setHandler($this->handler())
73 | ->setHosts($this->hosts)
74 | ->build();
75 | }
76 |
77 | return $this->client;
78 | }
79 |
80 | public function handler()
81 | {
82 | if ($this->handler instanceof PoolHandler) {
83 | return $this->handler;
84 | }
85 |
86 | return $this->handler = make(PoolHandler::class, [
87 | 'option' => [
88 | 'max_connections' => 50,
89 | 'max_idle_time' => 1,
90 | ],
91 | ]);
92 | }
93 |
94 | /**
95 | * 判断当前修改过得文档,是否在mapping中存在.
96 | * @param mixed $attributes
97 | */
98 | public function isModified($attributes = []): bool
99 | {
100 | if (is_array($attributes) && count($attributes) > 0) {
101 | $mapping = $this->mapping();
102 | foreach ($mapping as $key => $item) {
103 | if (isset($attributes[$key])) {
104 | return true;
105 | }
106 | }
107 | }
108 |
109 | return false;
110 | }
111 |
112 | /**
113 | * 保存文档.
114 | */
115 | public function put(Model $model): ?array
116 | {
117 | $doc = $this->document($model);
118 | $id = $model->getKey();
119 | $result = null;
120 | try {
121 | $client = $this->client();
122 | $doc = [
123 | 'index' => $this->index(),
124 | 'type' => $this->type(),
125 | 'id' => $id,
126 | 'body' => [
127 | 'doc' => $doc,
128 | 'doc_as_upsert' => true,
129 | ],
130 | 'refresh' => true,
131 | 'retry_on_conflict' => 5,
132 | ];
133 | $result = $client->update($doc);
134 | } catch (\Throwable $ex) {
135 | $logger = $this->container->get(StdoutLoggerInterface::class);
136 | $logger->error((string) $ex);
137 | }
138 |
139 | return $result;
140 | }
141 |
142 | /**
143 | * 删除文档.
144 | * @param mixed $id
145 | */
146 | public function delete($id): ?array
147 | {
148 | $client = $this->client();
149 | $result = null;
150 | $doc = [
151 | 'index' => $this->index(),
152 | 'type' => $this->type(),
153 | 'id' => $id,
154 | ];
155 |
156 | try {
157 | $result = $client->delete($doc);
158 | } catch (\Throwable $ex) {
159 | $logger = $this->container->get(StdoutLoggerInterface::class);
160 | $logger->error((string) $ex);
161 | }
162 |
163 | return $result;
164 | }
165 |
166 | public function search(array $body): array
167 | {
168 | $client = $this->client();
169 |
170 | $params = [
171 | 'index' => $this->index(),
172 | 'type' => $this->type(),
173 | 'body' => $body,
174 | ];
175 |
176 | $res = $client->search($params);
177 |
178 | if (isset($res['hits']['hits']) && $hits = $res['hits']['hits']) {
179 | $ids = [];
180 | foreach ($hits as $item) {
181 | $ids[] = $item['_id'];
182 | }
183 |
184 | return [$res['hits']['total'], $ids];
185 | }
186 |
187 | return [0, []];
188 | }
189 |
190 | public function putIndex(bool $force = false): bool
191 | {
192 | $client = $this->client();
193 | $indices = $client->indices();
194 |
195 | $params = [
196 | 'index' => $this->index(),
197 | ];
198 | $exist = $indices->exists($params);
199 | if ($exist && $force !== false) {
200 | $indices->delete($params);
201 | $exist = false;
202 | }
203 |
204 | if (! $exist) {
205 | $indices->create($params);
206 | return true;
207 | }
208 |
209 | return false;
210 | }
211 |
212 | public function putMapping(): bool
213 | {
214 | $mapping = $this->mapping();
215 | $params = [
216 | 'index' => $this->index(),
217 | 'type' => $this->type(),
218 | 'body' => [
219 | 'properties' => $mapping,
220 | ],
221 | ];
222 |
223 | $indices = $this->client()->indices();
224 | $res = $indices->putMapping($params);
225 | if ($res['acknowledged']) {
226 | return true;
227 | }
228 |
229 | return false;
230 | }
231 |
232 | /**
233 | * 返回搜索引擎实体结构.
234 | */
235 | abstract public function mapping(): array;
236 |
237 | /**
238 | * 搜索引擎索引.
239 | */
240 | abstract public function index(): string;
241 |
242 | /**
243 | * 搜索引擎类型.
244 | */
245 | abstract public function type(): string;
246 |
247 | /**
248 | * 根据模型获取对应document,如果数据字段不一致,请重写此方法.
249 | */
250 | protected function document(Model $model): array
251 | {
252 | $map = $this->mapping();
253 | $data = [];
254 | foreach ($map as $key => $item) {
255 | $data[$key] = $model->{$key};
256 | }
257 |
258 | if (! $this->check($data)) {
259 | throw new InvalidArgumentException('数据参数与定义不一致');
260 | }
261 |
262 | return $data;
263 | }
264 |
265 | protected function check($data): bool
266 | {
267 | $map = $this->mapping();
268 | foreach ($map as $key => $item) {
269 | if (! isset($data[$key])) {
270 | $logger = $this->container->get(StdoutLoggerInterface::class);
271 | $logger->error(sprintf('[%s] Mapping invalid! Not has [%s]', get_called_class(), $key));
272 | return false;
273 | }
274 | }
275 |
276 | return true;
277 | }
278 | }
279 |
--------------------------------------------------------------------------------
/src/ElasticSearch/Search7.php:
--------------------------------------------------------------------------------
1 | container = $container;
58 | $this->config = $container->get(ConfigInterface::class);
59 |
60 | $config = $this->config->get('elasticsearch.default');
61 | if (empty($config['host'])) {
62 | throw new InvalidArgumentException('搜索引擎配置不存在');
63 | }
64 |
65 | $this->hosts = (array) $config['host'];
66 | }
67 |
68 | public function client(): Client
69 | {
70 | if (! $this->client instanceof Client) {
71 | $this->client = ClientBuilder::create()
72 | ->setHandler($this->handler())
73 | ->setHosts($this->hosts)
74 | ->build();
75 | }
76 |
77 | return $this->client;
78 | }
79 |
80 | public function handler(): mixed
81 | {
82 | return null;
83 | }
84 |
85 | /**
86 | * 判断当前修改过得文档,是否在mapping中存在.
87 | * @param mixed $attributes
88 | */
89 | public function isModified($attributes = []): bool
90 | {
91 | if (is_array($attributes) && count($attributes) > 0) {
92 | $mapping = $this->mapping();
93 | foreach ($mapping as $key => $item) {
94 | if (isset($attributes[$key])) {
95 | return true;
96 | }
97 | }
98 | }
99 |
100 | return false;
101 | }
102 |
103 | /**
104 | * 保存文档.
105 | */
106 | public function put(Model $model): ?array
107 | {
108 | $doc = $this->document($model);
109 | $id = $model->getKey();
110 | $result = null;
111 |
112 | try {
113 | $client = $this->client();
114 | $doc = [
115 | 'index' => $this->index(),
116 | 'id' => $id,
117 | 'body' => [
118 | 'doc' => $doc,
119 | 'doc_as_upsert' => true,
120 | ],
121 | 'refresh' => true,
122 | 'retry_on_conflict' => 5,
123 | ];
124 |
125 | $result = $client->update($doc);
126 | } catch (\Throwable $ex) {
127 | $logger = $this->container->get(StdoutLoggerInterface::class);
128 | $logger->error((string) $ex);
129 | }
130 |
131 | return $result;
132 | }
133 |
134 | /**
135 | * @param Collection $models
136 | */
137 | public function bulk(Collection $models): ?array
138 | {
139 | $result = null;
140 |
141 | try {
142 | $client = $this->client();
143 |
144 | $params = [
145 | 'body' => [],
146 | 'refresh' => 'wait_for',
147 | ];
148 |
149 | foreach ($models as $model) {
150 | $params['body'][] = [
151 | 'update' => [
152 | '_index' => $this->index(),
153 | '_id' => $model->getKey(),
154 | ],
155 | ];
156 |
157 | $params['body'][] = [
158 | 'doc' => $this->document($model),
159 | 'doc_as_upsert' => true,
160 | ];
161 | }
162 |
163 | $result = $client->bulk($params);
164 | } catch (\Throwable $exception) {
165 | $this->container->get(StdoutLoggerInterface::class)->error((string) $exception);
166 | }
167 |
168 | return $result;
169 | }
170 |
171 | /**
172 | * 删除文档.
173 | * @param mixed $id
174 | */
175 | public function delete($id): ?array
176 | {
177 | $client = $this->client();
178 | $result = null;
179 | $doc = [
180 | 'index' => $this->index(),
181 | 'id' => $id,
182 | ];
183 |
184 | try {
185 | $result = $client->delete($doc);
186 | } catch (\Throwable $ex) {
187 | $logger = $this->container->get(StdoutLoggerInterface::class);
188 | $logger->error((string) $ex);
189 | }
190 |
191 | return $result;
192 | }
193 |
194 | /**
195 | * @param array $extra don't remove from arguments, it can easily cut by aop
196 | */
197 | public function rawSearch(array $params, array $extra = []): array
198 | {
199 | return $this->client()->search($params);
200 | }
201 |
202 | public function search(array $body, array $extra = []): array
203 | {
204 | $res = $this->rawSearch(
205 | [
206 | 'index' => $this->index(),
207 | 'body' => $body,
208 | ],
209 | $extra
210 | );
211 |
212 | if (isset($res['hits']['hits']) && $hits = $res['hits']['hits']) {
213 | $ids = [];
214 | foreach ($hits as $item) {
215 | $ids[] = $item['_id'];
216 | }
217 |
218 | return [$res['hits']['total']['value'] ?? 0, $ids];
219 | }
220 |
221 | return [0, []];
222 | }
223 |
224 | public function putIndex(
225 | bool $force = false,
226 | #[ArrayShape(['settings' => ['number_of_replicas' => 'int', 'number_of_shards' => 'int']])]
227 | array $body = []
228 | ): bool {
229 | $client = $this->client();
230 | $indices = $client->indices();
231 |
232 | $params = [
233 | 'index' => $this->index(),
234 | ];
235 | $exist = $indices->exists($params);
236 | if ($exist && $force !== false) {
237 | $indices->delete($params);
238 | $exist = false;
239 | }
240 |
241 | if (! $exist) {
242 | if ($body) {
243 | $params['body'] = $body;
244 | }
245 | $indices->create($params);
246 | return true;
247 | }
248 |
249 | return false;
250 | }
251 |
252 | public function putMapping(): bool
253 | {
254 | $mapping = $this->mapping();
255 | $params = [
256 | 'index' => $this->index(),
257 | 'body' => [
258 | 'properties' => $mapping,
259 | ],
260 | ];
261 |
262 | $indices = $this->client()->indices();
263 | $res = $indices->putMapping($params);
264 | if ($res['acknowledged']) {
265 | return true;
266 | }
267 |
268 | return false;
269 | }
270 |
271 | /**
272 | * 返回搜索引擎实体结构.
273 | */
274 | abstract public function mapping(): array;
275 |
276 | /**
277 | * 搜索引擎索引.
278 | */
279 | abstract public function index(): string;
280 |
281 | /**
282 | * 根据模型获取对应document,如果数据字段不一致,请重写此方法.
283 | */
284 | protected function document(Model $model): array
285 | {
286 | $map = $this->mapping();
287 | $data = [];
288 | foreach ($map as $key => $item) {
289 | $data[$key] = $model->{$key};
290 | }
291 |
292 | if (! $this->check($data)) {
293 | throw new InvalidArgumentException('数据参数与定义不一致');
294 | }
295 |
296 | return $data;
297 | }
298 |
299 | protected function check($data): bool
300 | {
301 | $map = $this->mapping();
302 | foreach ($map as $key => $item) {
303 | if (! isset($data[$key])) {
304 | $logger = $this->container->get(StdoutLoggerInterface::class);
305 | $logger->error(sprintf('[%s] Mapping invalid! Not has [%s]', get_called_class(), $key));
306 | return false;
307 | }
308 | }
309 |
310 | return true;
311 | }
312 | }
313 |
--------------------------------------------------------------------------------