├── .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 | --------------------------------------------------------------------------------