├── .env ├── .gitignore ├── .gitlab-ci.yml ├── .php-cs-fixer.php ├── .phpstorm.meta.php ├── .watcher.php ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── Controller │ ├── AbstractController.php │ ├── IndexController.php │ └── PersonController.php ├── Exception │ ├── Handler │ │ └── AppExceptionHandler.php │ ├── NotFoundException.php │ └── UniqueException.php ├── Job │ └── PersonJob.php ├── Listener │ ├── DbQueryExecutedListener.php │ └── ResumeExitCoordinatorListener.php ├── Model │ ├── Model.php │ └── Person.php ├── Process │ └── PersonConsumer.php ├── Request │ └── PersonRequest.php ├── Resource │ ├── Person.php │ └── PersonResource.php └── Service │ ├── PersonQueueService.php │ └── PersonService.php ├── assets └── php.jpeg ├── bin └── hyperf.php ├── composer.json ├── composer.lock ├── config ├── autoload │ ├── annotations.php │ ├── aspects.php │ ├── async_queue.php │ ├── cache.php │ ├── commands.php │ ├── databases.php │ ├── dependencies.php │ ├── devtool.php │ ├── exceptions.php │ ├── listeners.php │ ├── logger.php │ ├── middlewares.php │ ├── processes.php │ ├── redis.php │ ├── server.php │ └── translation.php ├── config.php ├── container.php └── routes.php ├── deploy.test.yml ├── docker-compose.yml ├── docker ├── nginx │ └── nginx.conf └── postgres │ ├── db.sql │ └── postgres.conf ├── k6 ├── get_test.js ├── insert_test.js └── test_insert_and_get.js ├── phpstan.neon ├── phpunit.xml ├── start-script.sh ├── storage └── languages │ ├── en │ └── validation.php │ └── zh_CN │ └── validation.php └── test ├── Cases └── ExampleTest.php ├── HttpTestCase.php └── bootstrap.php /.env: -------------------------------------------------------------------------------- 1 | APP_NAME=skeleton 2 | APP_ENV=dev 3 | 4 | DB_DRIVER=pgsql 5 | DB_HOST=rinha-backend-db 6 | DB_PORT=5432 7 | DB_DATABASE=rinha-backend-db 8 | DB_USERNAME=rinha 9 | DB_PASSWORD=postgres 10 | DB_CHARSET= 11 | DB_COLLATION= 12 | DB_PREFIX= 13 | 14 | REDIS_HOST=rinha-backend-redis 15 | REDIS_PORT=6379 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .buildpath 2 | .settings/ 3 | .project 4 | *.patch 5 | .idea/ 6 | .git/ 7 | runtime/ 8 | vendor/ 9 | .phpintel/ 10 | env 11 | .DS_Store 12 | .phpunit* 13 | *.cache 14 | .vscode/ 15 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # usermod -aG docker gitlab-runner 2 | 3 | stages: 4 | - build 5 | - deploy 6 | 7 | variables: 8 | PROJECT_NAME: hyperf 9 | REGISTRY_URL: registry-docker.org 10 | 11 | build_test_docker: 12 | stage: build 13 | before_script: 14 | # - git submodule sync --recursive 15 | # - git submodule update --init --recursive 16 | script: 17 | - docker build . -t $PROJECT_NAME 18 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:test 19 | - docker push $REGISTRY_URL/$PROJECT_NAME:test 20 | only: 21 | - test 22 | tags: 23 | - builder 24 | 25 | deploy_test_docker: 26 | stage: deploy 27 | script: 28 | - docker stack deploy -c deploy.test.yml --with-registry-auth $PROJECT_NAME 29 | only: 30 | - test 31 | tags: 32 | - test 33 | 34 | build_docker: 35 | stage: build 36 | before_script: 37 | # - git submodule sync --recursive 38 | # - git submodule update --init --recursive 39 | script: 40 | - docker build . -t $PROJECT_NAME 41 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:$CI_COMMIT_REF_NAME 42 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:latest 43 | - docker push $REGISTRY_URL/$PROJECT_NAME:$CI_COMMIT_REF_NAME 44 | - docker push $REGISTRY_URL/$PROJECT_NAME:latest 45 | only: 46 | - tags 47 | tags: 48 | - builder 49 | 50 | deploy_docker: 51 | stage: deploy 52 | script: 53 | - echo SUCCESS 54 | only: 55 | - tags 56 | tags: 57 | - builder 58 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 23 | ->setRules([ 24 | '@PSR2' => true, 25 | '@Symfony' => true, 26 | '@DoctrineAnnotation' => true, 27 | '@PhpCsFixer' => true, 28 | 'header_comment' => [ 29 | 'comment_type' => 'PHPDoc', 30 | 'header' => $header, 31 | 'separate' => 'none', 32 | 'location' => 'after_declare_strict', 33 | ], 34 | 'array_syntax' => [ 35 | 'syntax' => 'short', 36 | ], 37 | 'list_syntax' => [ 38 | 'syntax' => 'short', 39 | ], 40 | 'concat_space' => [ 41 | 'spacing' => 'one', 42 | ], 43 | 'global_namespace_import' => [ 44 | 'import_classes' => true, 45 | 'import_constants' => true, 46 | 'import_functions' => null, 47 | ], 48 | 'blank_line_before_statement' => [ 49 | 'statements' => [ 50 | 'declare', 51 | ], 52 | ], 53 | 'general_phpdoc_annotation_remove' => [ 54 | 'annotations' => [ 55 | 'author', 56 | ], 57 | ], 58 | 'ordered_imports' => [ 59 | 'imports_order' => [ 60 | 'class', 'function', 'const', 61 | ], 62 | 'sort_algorithm' => 'alpha', 63 | ], 64 | 'single_line_comment_style' => [ 65 | 'comment_types' => [ 66 | ], 67 | ], 68 | 'yoda_style' => [ 69 | 'always_move_variable' => false, 70 | 'equal' => false, 71 | 'identical' => false, 72 | ], 73 | 'phpdoc_align' => [ 74 | 'align' => 'left', 75 | ], 76 | 'multiline_whitespace_before_semicolons' => [ 77 | 'strategy' => 'no_multi_line', 78 | ], 79 | 'constant_case' => [ 80 | 'case' => 'lower', 81 | ], 82 | 'class_attributes_separation' => true, 83 | 'combine_consecutive_unsets' => true, 84 | 'declare_strict_types' => true, 85 | 'linebreak_after_opening_tag' => true, 86 | 'lowercase_static_reference' => true, 87 | 'no_useless_else' => true, 88 | 'no_unused_imports' => true, 89 | 'not_operator_with_successor_space' => true, 90 | 'not_operator_with_space' => false, 91 | 'ordered_class_elements' => true, 92 | 'php_unit_strict' => false, 93 | 'phpdoc_separation' => false, 94 | 'single_quote' => true, 95 | 'standardize_not_equals' => true, 96 | 'multiline_comment_opening_closing' => true, 97 | ]) 98 | ->setFinder( 99 | PhpCsFixer\Finder::create() 100 | ->exclude('public') 101 | ->exclude('runtime') 102 | ->exclude('vendor') 103 | ->in(__DIR__) 104 | ) 105 | ->setUsingCache(false); 106 | -------------------------------------------------------------------------------- /.phpstorm.meta.php: -------------------------------------------------------------------------------- 1 | '@'])); 6 | override(\Hyperf\Context\Context::get(0), map(['' => '@'])); 7 | override(\make(0), map(['' => '@'])); 8 | override(\di(0), map(['' => '@'])); 9 | override(\Hyperf\Support\make(0), map(['' => '@'])); 10 | override(\Hyperf\Support\optional(0), type(0)); 11 | override(\Hyperf\Tappable\tap(0), type(0)); 12 | } 13 | -------------------------------------------------------------------------------- /.watcher.php: -------------------------------------------------------------------------------- 1 | ScanFileDriver::class, 16 | 'bin' => 'php', 17 | 'watch' => [ 18 | 'dir' => ['app', 'config'], 19 | 'file' => ['.env'], 20 | 'scan_interval' => 2000, 21 | ], 22 | ]; 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Default Dockerfile 2 | # 3 | # @link https://www.hyperf.io 4 | # @document https://hyperf.wiki 5 | # @contact group@hyperf.io 6 | # @license https://github.com/hyperf/hyperf/blob/master/LICENSE 7 | 8 | FROM hyperf/hyperf:8.2-alpine-v3.18-swoole 9 | 10 | ## 11 | # ---------- env settings ---------- 12 | ## 13 | # --build-arg timezone=Asia/Shanghai 14 | ARG timezone 15 | 16 | ENV TIMEZONE=${timezone:-"America/Sao_Paulo"} \ 17 | APP_ENV=prod \ 18 | SCAN_CACHEABLE=(true) 19 | 20 | # update 21 | RUN set -ex \ 22 | # show php version and extensions 23 | && php -v \ 24 | && php -m \ 25 | && php --ri swoole \ 26 | # ---------- some config ---------- 27 | && cd /etc/php* \ 28 | # - config PHP 29 | && { \ 30 | echo "upload_max_filesize=128M"; \ 31 | echo "post_max_size=128M"; \ 32 | echo "memory_limit=1G"; \ 33 | echo "date.timezone=${TIMEZONE}"; \ 34 | } | tee conf.d/99_overrides.ini \ 35 | # - config timezone 36 | && ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \ 37 | && echo "${TIMEZONE}" > /etc/timezone \ 38 | # ---------- clear works ---------- 39 | && rm -rf /var/cache/apk/* /tmp/* /usr/share/man \ 40 | && echo -e "\033[42;37m Build Completed :).\033[0m\n" 41 | 42 | 43 | RUN set -ex \ 44 | && apk --no-cache add \ 45 | && apk add php82-pdo_pgsql 46 | 47 | WORKDIR /app 48 | 49 | COPY . /app 50 | 51 | EXPOSE 9501 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Hyperf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rinha de backend 2023 2 | 3 | ![Achou que eu estava brincando?](/assets/php.jpeg) 4 | 5 | Github [lauroappelt](https://github.com/lauroappelt) 6 | 7 | Linkedin [lauroappelt](https://www.linkedin.com/in/lauro-henrique-appelt/) 8 | 9 | Link do projeto [https://github.com/lauroappelt/rinha-de-backend-2023](https://github.com/lauroappelt/rinha-de-backend-2023) 10 | 11 | ## Stack 12 | * PHP Hyperf/Swoole 13 | * Postgres 14 | * Nginx - Load balancer 15 | * Redis - Cache / Async queue -------------------------------------------------------------------------------- /app/Controller/AbstractController.php: -------------------------------------------------------------------------------- 1 | request->input('user', 'Hyperf'); 19 | $method = $this->request->getMethod(); 20 | 21 | return [ 22 | 'method' => $method, 23 | 'message' => "Hello {$user}.", 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Controller/PersonController.php: -------------------------------------------------------------------------------- 1 | personService = $personService; 21 | } 22 | 23 | public function createPerson(PersonRequest $request, ResponseInterface $response) 24 | { 25 | try { 26 | 27 | $data = $request->all(); 28 | if (!is_string($data['apelido']) || !is_string($data['nome'])) { 29 | return $response->withStatus(400); 30 | } 31 | 32 | if (is_array($data['stack'])) { 33 | foreach ($data['stack'] as $stack) { 34 | if (!is_string($stack)){ 35 | return $response->withStatus(400); 36 | } 37 | } 38 | } 39 | 40 | $person = $this->personService->createPerson($data); 41 | return $response->withHeader('Location', '/pessoas/' . $person['id'])->withStatus(201)->json($person); 42 | } catch (UniqueException $exception) { 43 | return $response->withStatus(422); 44 | } 45 | } 46 | 47 | public function getPerson(RequestInterface $request, ResponseInterface $response) 48 | { 49 | try { 50 | $person = $this->personService->getPerson($request->route('id')); 51 | return $response->json($person); 52 | } catch (NotFoundException $exception) { 53 | return $response->withStatus(404); 54 | } 55 | } 56 | 57 | public function searchPerson(RequestInterface $request, ResponseInterface $response) 58 | { 59 | $term = $request->input('t'); 60 | if ($term == null) { 61 | return $response->withStatus(400); 62 | } 63 | 64 | $persons = $this->personService->searchPerson($term); 65 | return $response->json($persons)->withStatus(200); 66 | } 67 | 68 | public function countPerson(RequestInterface $request, ResponseInterface $response) 69 | { 70 | $count = $this->personService->countPerson(); 71 | return $count; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/Exception/Handler/AppExceptionHandler.php: -------------------------------------------------------------------------------- 1 | logger->error(sprintf('%s[%s] in %s', $throwable->getMessage(), $throwable->getLine(), $throwable->getFile())); 29 | $this->logger->error($throwable->getTraceAsString()); 30 | return $response->withHeader('Server', 'Hyperf')->withStatus(500)->withBody(new SwooleStream('Internal Server Error.')); 31 | } 32 | 33 | public function isValid(Throwable $throwable): bool 34 | { 35 | return true; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Exception/NotFoundException.php: -------------------------------------------------------------------------------- 1 | params = $params; 17 | } 18 | 19 | public function handle() 20 | { 21 | $searchable = $this->params['apelido'] . ' ' . $this->params['nome']; 22 | if (is_array($this->params['stack'])) { 23 | $searchable .= implode(' ', $this->params['stack']); 24 | } 25 | 26 | Db::statement("INSERT INTO person(id, apelido, nome, nascimento, stack, searchable) values (?, ?, ?, ?, ?, ?) ON CONFLICT DO NOTHING;", [ 27 | $this->params['id'], 28 | $this->params['apelido'], 29 | $this->params['nome'], 30 | $this->params['nascimento'], 31 | json_encode($this->params['stack']), 32 | $searchable 33 | ]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Listener/DbQueryExecutedListener.php: -------------------------------------------------------------------------------- 1 | logger = $container->get(LoggerFactory::class)->get('sql'); 33 | } 34 | 35 | public function listen(): array 36 | { 37 | return [ 38 | QueryExecuted::class, 39 | ]; 40 | } 41 | 42 | /** 43 | * @param QueryExecuted $event 44 | */ 45 | public function process(object $event): void 46 | { 47 | if ($event instanceof QueryExecuted) { 48 | $sql = $event->sql; 49 | if (! Arr::isAssoc($event->bindings)) { 50 | $position = 0; 51 | foreach ($event->bindings as $value) { 52 | $position = strpos($sql, '?', $position); 53 | if ($position === false) { 54 | break; 55 | } 56 | $value = "'{$value}'"; 57 | $sql = substr_replace($sql, $value, $position, 1); 58 | $position += strlen($value); 59 | } 60 | } 61 | 62 | $this->logger->info(sprintf('[%s] %s', $event->time, $sql)); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/Listener/ResumeExitCoordinatorListener.php: -------------------------------------------------------------------------------- 1 | resume(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Model/Model.php: -------------------------------------------------------------------------------- 1 | 'array']; 31 | 32 | protected String $keyType = 'string'; 33 | } 34 | -------------------------------------------------------------------------------- /app/Process/PersonConsumer.php: -------------------------------------------------------------------------------- 1 | 'required|max:32', 26 | 'nome' => 'required|max:100', 27 | 'nascimento' => 'required|date_format:Y-m-d', 28 | 'stack' => 'nullable|array', 29 | 'stack.*' => 'max:32' 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Resource/Person.php: -------------------------------------------------------------------------------- 1 | $this->id, 18 | 'apelido' => $this->apelido, 19 | 'nome' => $this->nome, 20 | 'stack' => $this->stack, 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Service/PersonQueueService.php: -------------------------------------------------------------------------------- 1 | driver = $driverFactory->get('default'); 22 | } 23 | 24 | /** 25 | * Publish the message. 26 | */ 27 | public function push($params, int $delay = 0): bool 28 | { 29 | // The `ExampleJob` here will be serialized and stored in Redis, so internal variables of the object are best passed only normal data. 30 | // Similarly, if the annotation is used internally, @Value will serialize the corresponding object, causing the message body to become larger. 31 | // So it is NOT recommended to use the `make` method to create a `Job` object. 32 | return $this->driver->push(new PersonJob($params), $delay); 33 | } 34 | } -------------------------------------------------------------------------------- /app/Service/PersonService.php: -------------------------------------------------------------------------------- 1 | redisClient = $container->get(\Redis::class); 20 | 21 | $this->personQueue = $personQueue; 22 | } 23 | 24 | public function createPerson(array $data) 25 | { 26 | $nickCached = $this->redisClient->get($data['apelido']); 27 | if ($nickCached) { 28 | throw new UniqueException("Unique nick violation"); 29 | } 30 | 31 | $this->redisClient->set($data['apelido'], '1'); 32 | 33 | $data['id'] = Uuid::uuid4(); 34 | $personKey = 'person.' . $data['id']; 35 | 36 | //cache 37 | $this->redisClient->set($personKey, json_encode($data)); 38 | 39 | //queue 40 | $this->personQueue->push($data); 41 | 42 | return $data; 43 | } 44 | 45 | public function getPerson(String $id) 46 | { 47 | $personCached =$this->redisClient->get('person.' . $id); 48 | if ($personCached) { 49 | return json_decode($personCached); 50 | } 51 | 52 | throw new NotFoundException("Not Found"); 53 | } 54 | 55 | public function searchPerson(String $term) 56 | { 57 | $result = Db::select("select id, apelido, nascimento, stack from person where searchable like ? limit 50;", ['%' . $term . '%']); 58 | foreach ($result as $person) { 59 | if ($person->stack) { 60 | $person->stack = json_decode($person->stack); 61 | } 62 | } 63 | 64 | return $result; 65 | } 66 | 67 | public function countPerson() 68 | { 69 | $count = Db::table('person')->count(); 70 | return $count; 71 | } 72 | } -------------------------------------------------------------------------------- /assets/php.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akitaonrails/rinhabackend-php-api/0dd5a086dfcdceeed76a7aa3afd4f4375584ddf9/assets/php.jpeg -------------------------------------------------------------------------------- /bin/hyperf.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | get(Hyperf\Contract\ApplicationInterface::class); 23 | $application->run(); 24 | })(); 25 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperf/hyperf-skeleton", 3 | "type": "project", 4 | "keywords": [ 5 | "php", 6 | "swoole", 7 | "framework", 8 | "hyperf", 9 | "microservice", 10 | "middleware" 11 | ], 12 | "description": "A coroutine framework that focuses on hyperspeed and flexible, specifically use for build microservices and middlewares.", 13 | "license": "Apache-2.0", 14 | "require": { 15 | "php": ">=8.0", 16 | "guzzlehttp/guzzle": "^7.7", 17 | "hyperf/async-queue": "^3.0", 18 | "hyperf/cache": "~3.0.0", 19 | "hyperf/command": "~3.0.0", 20 | "hyperf/config": "~3.0.0", 21 | "hyperf/database": "~3.0.0", 22 | "hyperf/database-pgsql": "^3.0", 23 | "hyperf/db-connection": "~3.0.0", 24 | "hyperf/framework": "~3.0.0", 25 | "hyperf/guzzle": "~3.0.0", 26 | "hyperf/http-server": "~3.0.0", 27 | "hyperf/logger": "~3.0.0", 28 | "hyperf/memory": "~3.0.0", 29 | "hyperf/process": "~3.0.0", 30 | "hyperf/redis": "^3.0", 31 | "hyperf/resource": "^3.0", 32 | "hyperf/validation": "^3.0", 33 | "ramsey/uuid": "^4.7" 34 | }, 35 | "require-dev": { 36 | "friendsofphp/php-cs-fixer": "^3.0", 37 | "hyperf/devtool": "~3.0.0", 38 | "hyperf/testing": "~3.0.0", 39 | "hyperf/watcher": "^3.0", 40 | "mockery/mockery": "^1.0", 41 | "phpstan/phpstan": "^1.0", 42 | "swoole/ide-helper": "^5.0", 43 | "symfony/var-dumper": "^6.3" 44 | }, 45 | "suggest": { 46 | "ext-openssl": "Required to use HTTPS.", 47 | "ext-json": "Required to use JSON.", 48 | "ext-pdo": "Required to use MySQL Client.", 49 | "ext-pdo_mysql": "Required to use MySQL Client.", 50 | "ext-redis": "Required to use Redis Client." 51 | }, 52 | "autoload": { 53 | "psr-4": { 54 | "App\\": "app/" 55 | }, 56 | "files": [] 57 | }, 58 | "autoload-dev": { 59 | "psr-4": { 60 | "HyperfTest\\": "./test/" 61 | } 62 | }, 63 | "minimum-stability": "dev", 64 | "prefer-stable": true, 65 | "config": { 66 | "optimize-autoloader": true, 67 | "sort-packages": true 68 | }, 69 | "extra": [], 70 | "scripts": { 71 | "post-root-package-install": [ 72 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 73 | ], 74 | "post-autoload-dump": [ 75 | "rm -rf runtime/container" 76 | ], 77 | "test": "co-phpunit --prepend test/bootstrap.php -c phpunit.xml --colors=always --testdox", 78 | "cs-fix": "php-cs-fixer fix $1", 79 | "analyse": "phpstan analyse --memory-limit 300M -l 0 -c phpstan.neon ./app ./config", 80 | "start": [ 81 | "Composer\\Config::disableProcessTimeout", 82 | "php ./bin/hyperf.php start" 83 | ] 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /config/autoload/annotations.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'paths' => [ 15 | BASE_PATH . '/app', 16 | ], 17 | 'ignore_annotations' => [ 18 | 'mixin', 19 | ], 20 | ], 21 | ]; 22 | -------------------------------------------------------------------------------- /config/autoload/aspects.php: -------------------------------------------------------------------------------- 1 | [ 16 | 'driver' => RedisDriver::class, 17 | 'redis' => [ 18 | 'pool' => 'default', 19 | ], 20 | 'channel' => '{queue}', 21 | 'timeout' => 2, 22 | 'retry_seconds' => 5, 23 | 'handle_timeout' => 10, 24 | 'processes' => 2, 25 | 'concurrent' => [ 26 | 'limit' => 1, 27 | ], 28 | ], 29 | ]; 30 | -------------------------------------------------------------------------------- /config/autoload/cache.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'driver' => Hyperf\Cache\Driver\RedisDriver::class, 15 | 'packer' => Hyperf\Codec\Packer\PhpSerializerPacker::class, 16 | 'prefix' => 'c:', 17 | ], 18 | ]; 19 | -------------------------------------------------------------------------------- /config/autoload/commands.php: -------------------------------------------------------------------------------- 1 | [ 16 | 'driver' => env('DB_DRIVER', 'mysql'), 17 | 'host' => env('DB_HOST', 'localhost'), 18 | 'database' => env('DB_DATABASE', 'hyperf'), 19 | 'port' => env('DB_PORT', 3306), 20 | 'username' => env('DB_USERNAME', 'root'), 21 | 'password' => env('DB_PASSWORD', ''), 22 | //'charset' => env('DB_CHARSET', 'utf8'), 23 | //'collation' => env('DB_COLLATION', 'utf8_unicode_ci'), 24 | 'prefix' => env('DB_PREFIX', ''), 25 | 'pool' => [ 26 | 'min_connections' => 1, 27 | 'max_connections' => 10, 28 | 'connect_timeout' => 10.0, 29 | 'wait_timeout' => 3.0, 30 | 'heartbeat' => -1, 31 | 'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60), 32 | ], 33 | 'commands' => [ 34 | 'gen:model' => [ 35 | 'path' => 'app/Model', 36 | 'force_casts' => true, 37 | 'inheritance' => 'Model', 38 | ], 39 | ], 40 | ], 41 | ]; 42 | -------------------------------------------------------------------------------- /config/autoload/dependencies.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'amqp' => [ 15 | 'consumer' => [ 16 | 'namespace' => 'App\\Amqp\\Consumer', 17 | ], 18 | 'producer' => [ 19 | 'namespace' => 'App\\Amqp\\Producer', 20 | ], 21 | ], 22 | 'aspect' => [ 23 | 'namespace' => 'App\\Aspect', 24 | ], 25 | 'command' => [ 26 | 'namespace' => 'App\\Command', 27 | ], 28 | 'controller' => [ 29 | 'namespace' => 'App\\Controller', 30 | ], 31 | 'job' => [ 32 | 'namespace' => 'App\\Job', 33 | ], 34 | 'listener' => [ 35 | 'namespace' => 'App\\Listener', 36 | ], 37 | 'middleware' => [ 38 | 'namespace' => 'App\\Middleware', 39 | ], 40 | 'Process' => [ 41 | 'namespace' => 'App\\Processes', 42 | ], 43 | ], 44 | ]; 45 | -------------------------------------------------------------------------------- /config/autoload/exceptions.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'http' => [ 15 | Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler::class, 16 | App\Exception\Handler\AppExceptionHandler::class, 17 | \Hyperf\Validation\ValidationExceptionHandler::class, 18 | ], 19 | ], 20 | ]; 21 | -------------------------------------------------------------------------------- /config/autoload/listeners.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'handler' => [ 15 | 'class' => Monolog\Handler\StreamHandler::class, 16 | 'constructor' => [ 17 | 'stream' => BASE_PATH . '/runtime/logs/hyperf.log', 18 | 'level' => Monolog\Logger::ERROR, 19 | ], 20 | ], 21 | 'formatter' => [ 22 | 'class' => Monolog\Formatter\LineFormatter::class, 23 | 'constructor' => [ 24 | 'format' => null, 25 | 'dateFormat' => 'Y-m-d H:i:s', 26 | 'allowInlineLineBreaks' => true, 27 | ], 28 | ], 29 | ], 30 | ]; 31 | -------------------------------------------------------------------------------- /config/autoload/middlewares.php: -------------------------------------------------------------------------------- 1 | [ 14 | \Hyperf\Validation\Middleware\ValidationMiddleware::class, 15 | ], 16 | ]; 17 | -------------------------------------------------------------------------------- /config/autoload/processes.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'host' => env('REDIS_HOST', 'localhost'), 15 | 'auth' => env('REDIS_AUTH', null), 16 | 'port' => (int) env('REDIS_PORT', 6379), 17 | 'db' => (int) env('REDIS_DB', 0), 18 | 'pool' => [ 19 | 'min_connections' => 1, 20 | 'max_connections' => 10, 21 | 'connect_timeout' => 10.0, 22 | 'wait_timeout' => 3.0, 23 | 'heartbeat' => -1, 24 | 'max_idle_time' => (float) env('REDIS_MAX_IDLE_TIME', 60), 25 | ], 26 | ], 27 | ]; 28 | -------------------------------------------------------------------------------- /config/autoload/server.php: -------------------------------------------------------------------------------- 1 | SWOOLE_PROCESS, 18 | 'servers' => [ 19 | [ 20 | 'name' => 'http', 21 | 'type' => Server::SERVER_HTTP, 22 | 'host' => '0.0.0.0', 23 | 'port' => 9501, 24 | 'sock_type' => SWOOLE_SOCK_TCP, 25 | 'callbacks' => [ 26 | Event::ON_REQUEST => [Hyperf\HttpServer\Server::class, 'onRequest'], 27 | ], 28 | ], 29 | ], 30 | 'settings' => [ 31 | Constant::OPTION_ENABLE_COROUTINE => true, 32 | Constant::OPTION_WORKER_NUM => swoole_cpu_num(), 33 | Constant::OPTION_PID_FILE => BASE_PATH . '/runtime/hyperf.pid', 34 | Constant::OPTION_OPEN_TCP_NODELAY => true, 35 | Constant::OPTION_MAX_COROUTINE => 100000, 36 | Constant::OPTION_OPEN_HTTP2_PROTOCOL => true, 37 | Constant::OPTION_MAX_REQUEST => 100000, 38 | Constant::OPTION_SOCKET_BUFFER_SIZE => 2 * 1024 * 1024, 39 | Constant::OPTION_BUFFER_OUTPUT_SIZE => 2 * 1024 * 1024, 40 | ], 41 | 'callbacks' => [ 42 | Event::ON_WORKER_START => [Hyperf\Framework\Bootstrap\WorkerStartCallback::class, 'onWorkerStart'], 43 | Event::ON_PIPE_MESSAGE => [Hyperf\Framework\Bootstrap\PipeMessageCallback::class, 'onPipeMessage'], 44 | Event::ON_WORKER_EXIT => [Hyperf\Framework\Bootstrap\WorkerExitCallback::class, 'onWorkerExit'], 45 | ], 46 | ]; 47 | -------------------------------------------------------------------------------- /config/autoload/translation.php: -------------------------------------------------------------------------------- 1 | 'en', 14 | 'fallback_locale' => 'en', 15 | 'path' => BASE_PATH . '/storage/languages', 16 | ]; 17 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | env('APP_NAME', 'skeleton'), 19 | 'app_env' => env('APP_ENV', 'dev'), 20 | 'scan_cacheable' => env('SCAN_CACHEABLE', false), 21 | StdoutLoggerInterface::class => [ 22 | 'log_level' => [ 23 | // LogLevel::ALERT, 24 | // LogLevel::CRITICAL, 25 | // LogLevel::DEBUG, 26 | // LogLevel::EMERGENCY, 27 | // LogLevel::ERROR, 28 | // LogLevel::INFO, 29 | // LogLevel::NOTICE, 30 | // LogLevel::WARNING, 31 | ], 32 | ], 33 | ]; 34 | -------------------------------------------------------------------------------- /config/container.php: -------------------------------------------------------------------------------- 1 | r.status === 201 || r.status === 404, 21 | }); 22 | } -------------------------------------------------------------------------------- /k6/insert_test.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http'; 2 | import { check, sleep } from 'k6'; 3 | 4 | export let options = { 5 | stages: [ 6 | { duration: '1m', target: 8 }, 7 | ], 8 | }; 9 | 10 | export default function () { 11 | const payload = { 12 | apelido: `josé_${__VU}_${__ITER}_` + Math.random(), 13 | nome: 'José Roberto', 14 | nascimento: new Date().toISOString().split('T')[0], 15 | stack: ['C#', 'Node', 'Oracle'], 16 | }; 17 | 18 | const params = { 19 | headers: { 20 | 'Content-Type': 'application/json', 21 | }, 22 | }; 23 | 24 | let response = http.post('http://192.168.1.4:9999/pessoas', JSON.stringify(payload), params); 25 | 26 | check(response, { 27 | 'is status 201 or 422': (r) => r.status === 201 || r.status === 422, 28 | }); 29 | } -------------------------------------------------------------------------------- /k6/test_insert_and_get.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http'; 2 | import { check, sleep } from 'k6'; 3 | 4 | export let options = { 5 | stages: [ 6 | { duration: '1m', target: 5 }, 7 | ], 8 | }; 9 | 10 | export default function () { 11 | const payload = { 12 | apelido: `josé_${__VU}_${__ITER}_` + Math.random(), 13 | nome: 'José Roberto', 14 | nascimento: new Date().toISOString().split('T')[0], 15 | stack: ['C#', 'Node', 'Oracle'], 16 | }; 17 | 18 | const params = { 19 | headers: { 20 | 'Content-Type': 'application/json', 21 | }, 22 | }; 23 | 24 | let response = http.post('http://192.168.1.4:9999/pessoas', JSON.stringify(payload), params); 25 | 26 | check(response, { 27 | 'is status 201 or 422': (r) => r.status === 201 || r.status === 422, 28 | }); 29 | 30 | if (response.status === 201 && response.headers['Location']) { 31 | const newPath = response.headers['Location']; 32 | 33 | const host = 'http://192.168.1.4:9999'; // Update with your actual host 34 | const newUrl = `${host}${newPath}`; 35 | 36 | let getResponse = http.get(newUrl); 37 | 38 | check(getResponse, { 39 | 'is status 200': (r) => r.status === 200, 40 | }); 41 | } 42 | } -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | # Magic behaviour with __get, __set, __call and __callStatic is not exactly static analyser-friendly :) 2 | # Fortunately, You can ignore it by the following config. 3 | # 4 | # vendor/bin/phpstan analyse app --memory-limit 200M -l 0 5 | # 6 | parameters: 7 | reportUnmatchedIgnoredErrors: false 8 | ignoreErrors: 9 | - '#Static call to instance method Hyperf\\HttpServer\\Router\\Router::[a-zA-Z0-9\\_]+\(\)#' 10 | - '#Static call to instance method Hyperf\\DbConnection\\Db::[a-zA-Z0-9\\_]+\(\)#' 11 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./test 15 | 16 | 17 | 18 | 19 | ./app 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /start-script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "deu certo" 4 | -------------------------------------------------------------------------------- /storage/languages/en/validation.php: -------------------------------------------------------------------------------- 1 | 'The :attribute must be accepted.', 25 | 'active_url' => 'The :attribute is not a valid URL.', 26 | 'after' => 'The :attribute must be a date after :date.', 27 | 'after_or_equal' => 'The :attribute must be a date after or equal to :date.', 28 | 'alpha' => 'The :attribute may only contain letters.', 29 | 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.', 30 | 'alpha_num' => 'The :attribute may only contain letters and numbers.', 31 | 'array' => 'The :attribute must be an array.', 32 | 'before' => 'The :attribute must be a date before :date.', 33 | 'before_or_equal' => 'The :attribute must be a date before or equal to :date.', 34 | 'between' => [ 35 | 'numeric' => 'The :attribute must be between :min and :max.', 36 | 'file' => 'The :attribute must be between :min and :max kilobytes.', 37 | 'string' => 'The :attribute must be between :min and :max characters.', 38 | 'array' => 'The :attribute must have between :min and :max items.', 39 | ], 40 | 'boolean' => 'The :attribute field must be true or false.', 41 | 'confirmed' => 'The :attribute confirmation does not match.', 42 | 'date' => 'The :attribute is not a valid date.', 43 | 'date_format' => 'The :attribute does not match the format :format.', 44 | 'different' => 'The :attribute and :other must be different.', 45 | 'digits' => 'The :attribute must be :digits digits.', 46 | 'digits_between' => 'The :attribute must be between :min and :max digits.', 47 | 'dimensions' => 'The :attribute has invalid image dimensions.', 48 | 'distinct' => 'The :attribute field has a duplicate value.', 49 | 'email' => 'The :attribute must be a valid email address.', 50 | 'exists' => 'The selected :attribute is invalid.', 51 | 'file' => 'The :attribute must be a file.', 52 | 'filled' => 'The :attribute field is required.', 53 | 'gt' => [ 54 | 'numeric' => 'The :attribute must be greater than :value', 55 | 'file' => 'The :attribute must be greater than :value kb', 56 | 'string' => 'The :attribute must be greater than :value characters', 57 | 'array' => 'The :attribute must be greater than :value items', 58 | ], 59 | 'gte' => [ 60 | 'numeric' => 'The :attribute must be great than or equal to :value', 61 | 'file' => 'The :attribute must be great than or equal to :value kb', 62 | 'string' => 'The :attribute must be great than or equal to :value characters', 63 | 'array' => 'The :attribute must be great than or equal to :value items', 64 | ], 65 | 'image' => 'The :attribute must be an image.', 66 | 'in' => 'The selected :attribute is invalid.', 67 | 'in_array' => 'The :attribute field does not exist in :other.', 68 | 'integer' => 'The :attribute must be an integer.', 69 | 'ip' => 'The :attribute must be a valid IP address.', 70 | 'ipv4' => 'The :attribute must be a valid IPv4 address.', 71 | 'ipv6' => 'The :attribute must be a valid IPv6 address.', 72 | 'json' => 'The :attribute must be a valid JSON string.', 73 | 'lt' => [ 74 | 'numeric' => 'The :attribute must be less than :value', 75 | 'file' => 'The :attribute must be less than :value kb', 76 | 'string' => 'The :attribute must be less than :value characters', 77 | 'array' => 'The :attribute must be less than :value items', 78 | ], 79 | 'lte' => [ 80 | 'numeric' => 'The :attribute must be less than or equal to :value', 81 | 'file' => 'The :attribute must be less than or equal to :value kb', 82 | 'string' => 'The :attribute must be less than or equal to :value characters', 83 | 'array' => 'The :attribute must be less than or equal to :value items', 84 | ], 85 | 'max' => [ 86 | 'numeric' => 'The :attribute may not be greater than :max.', 87 | 'file' => 'The :attribute may not be greater than :max kilobytes.', 88 | 'string' => 'The :attribute may not be greater than :max characters.', 89 | 'array' => 'The :attribute may not have more than :max items.', 90 | ], 91 | 'mimes' => 'The :attribute must be a file of type: :values.', 92 | 'mimetypes' => 'The :attribute must be a file of type: :values.', 93 | 'min' => [ 94 | 'numeric' => 'The :attribute must be at least :min.', 95 | 'file' => 'The :attribute must be at least :min kilobytes.', 96 | 'string' => 'The :attribute must be at least :min characters.', 97 | 'array' => 'The :attribute must have at least :min items.', 98 | ], 99 | 'not_in' => 'The selected :attribute is invalid.', 100 | 'not_regex' => 'The :attribute cannot match a given regular rule.', 101 | 'numeric' => 'The :attribute must be a number.', 102 | 'present' => 'The :attribute field must be present.', 103 | 'regex' => 'The :attribute format is invalid.', 104 | 'required' => 'The :attribute field is required.', 105 | 'required_if' => 'The :attribute field is required when :other is :value.', 106 | 'required_unless' => 'The :attribute field is required unless :other is in :values.', 107 | 'required_with' => 'The :attribute field is required when :values is present.', 108 | 'required_with_all' => 'The :attribute field is required when :values is present.', 109 | 'required_without' => 'The :attribute field is required when :values is not present.', 110 | 'required_without_all' => 'The :attribute field is required when none of :values are present.', 111 | 'same' => 'The :attribute and :other must match.', 112 | 'size' => [ 113 | 'numeric' => 'The :attribute must be :size.', 114 | 'file' => 'The :attribute must be :size kilobytes.', 115 | 'string' => 'The :attribute must be :size characters.', 116 | 'array' => 'The :attribute must contain :size items.', 117 | ], 118 | 'starts_with' => 'The :attribute must be start with :values ', 119 | 'string' => 'The :attribute must be a string.', 120 | 'timezone' => 'The :attribute must be a valid zone.', 121 | 'unique' => 'The :attribute has already been taken.', 122 | 'uploaded' => 'The :attribute failed to upload.', 123 | 'url' => 'The :attribute format is invalid.', 124 | 'uuid' => 'The :attribute is invalid UUID.', 125 | 'max_if' => [ 126 | 'numeric' => 'The :attribute may not be greater than :max when :other is :value.', 127 | 'file' => 'The :attribute may not be greater than :max kilobytes when :other is :value.', 128 | 'string' => 'The :attribute may not be greater than :max characters when :other is :value.', 129 | 'array' => 'The :attribute may not have more than :max items when :other is :value.', 130 | ], 131 | 'min_if' => [ 132 | 'numeric' => 'The :attribute must be at least :min when :other is :value.', 133 | 'file' => 'The :attribute must be at least :min kilobytes when :other is :value.', 134 | 'string' => 'The :attribute must be at least :min characters when :other is :value.', 135 | 'array' => 'The :attribute must have at least :min items when :other is :value.', 136 | ], 137 | 'between_if' => [ 138 | 'numeric' => 'The :attribute must be between :min and :max when :other is :value.', 139 | 'file' => 'The :attribute must be between :min and :max kilobytes when :other is :value.', 140 | 'string' => 'The :attribute must be between :min and :max characters when :other is :value.', 141 | 'array' => 'The :attribute must have between :min and :max items when :other is :value.', 142 | ], 143 | /* 144 | |-------------------------------------------------------------------------- 145 | | Custom Validation Language Lines 146 | |-------------------------------------------------------------------------- 147 | | 148 | | Here you may specify custom validation messages for attributes using the 149 | | convention "attribute.rule" to name the lines. This makes it quick to 150 | | specify a specific custom language line for a given attribute rule. 151 | | 152 | */ 153 | 154 | 'custom' => [ 155 | 'attribute-name' => [ 156 | 'rule-name' => 'custom-message', 157 | ], 158 | ], 159 | 160 | /* 161 | |-------------------------------------------------------------------------- 162 | | Custom Validation Attributes 163 | |-------------------------------------------------------------------------- 164 | | 165 | | The following language lines are used to swap attribute place-holders 166 | | with something more reader friendly such as E-Mail Address instead 167 | | of "email". This simply helps us make messages a little cleaner. 168 | | 169 | */ 170 | 171 | 'attributes' => [], 172 | 'phone_number' => 'The :attribute must be a valid phone number', 173 | 'telephone_number' => 'The :attribute must be a valid telephone number', 174 | 175 | 'chinese_word' => 'The :attribute must contain valid characters(chinese/english character, number, underscore)', 176 | 'sequential_array' => 'The :attribute must be sequential array', 177 | ]; 178 | -------------------------------------------------------------------------------- /storage/languages/zh_CN/validation.php: -------------------------------------------------------------------------------- 1 | ':attribute 必须接受', 25 | 'active_url' => ':attribute 必须是一个合法的 URL', 26 | 'after' => ':attribute 必须是 :date 之后的一个日期', 27 | 'after_or_equal' => ':attribute 必须是 :date 之后或相同的一个日期', 28 | 'alpha' => ':attribute 只能包含字母', 29 | 'alpha_dash' => ':attribute 只能包含字母、数字、中划线或下划线', 30 | 'alpha_num' => ':attribute 只能包含字母和数字', 31 | 'array' => ':attribute 必须是一个数组', 32 | 'before' => ':attribute 必须是 :date 之前的一个日期', 33 | 'before_or_equal' => ':attribute 必须是 :date 之前或相同的一个日期', 34 | 'between' => [ 35 | 'numeric' => ':attribute 必须在 :min 到 :max 之间', 36 | 'file' => ':attribute 必须在 :min 到 :max kb 之间', 37 | 'string' => ':attribute 必须在 :min 到 :max 个字符之间', 38 | 'array' => ':attribute 必须在 :min 到 :max 项之间', 39 | ], 40 | 'boolean' => ':attribute 字符必须是 true 或 false, 1 或 0', 41 | 'confirmed' => ':attribute 二次确认不匹配', 42 | 'date' => ':attribute 必须是一个合法的日期', 43 | 'date_format' => ':attribute 与给定的格式 :format 不符合', 44 | 'different' => ':attribute 必须不同于 :other', 45 | 'digits' => ':attribute 必须是 :digits 位', 46 | 'digits_between' => ':attribute 必须在 :min 和 :max 位之间', 47 | 'dimensions' => ':attribute 具有无效的图片尺寸', 48 | 'distinct' => ':attribute 字段具有重复值', 49 | 'email' => ':attribute 必须是一个合法的电子邮件地址', 50 | 'exists' => '选定的 :attribute 是无效的', 51 | 'file' => ':attribute 必须是一个文件', 52 | 'filled' => ':attribute 的字段是必填的', 53 | 'gt' => [ 54 | 'numeric' => ':attribute 必须大于 :value', 55 | 'file' => ':attribute 必须大于 :value kb', 56 | 'string' => ':attribute 必须大于 :value 个字符', 57 | 'array' => ':attribute 必须大于 :value 项', 58 | ], 59 | 'gte' => [ 60 | 'numeric' => ':attribute 必须大于等于 :value', 61 | 'file' => ':attribute 必须大于等于 :value kb', 62 | 'string' => ':attribute 必须大于等于 :value 个字符', 63 | 'array' => ':attribute 必须大于等于 :value 项', 64 | ], 65 | 'image' => ':attribute 必须是 jpg, jpeg, png, bmp 或者 gif 格式的图片', 66 | 'in' => '选定的 :attribute 是无效的', 67 | 'in_array' => ':attribute 字段不存在于 :other', 68 | 'integer' => ':attribute 必须是个整数', 69 | 'ip' => ':attribute 必须是一个合法的 IP 地址', 70 | 'ipv4' => ':attribute 必须是一个合法的 IPv4 地址', 71 | 'ipv6' => ':attribute 必须是一个合法的 IPv6 地址', 72 | 'json' => ':attribute 必须是一个合法的 JSON 字符串', 73 | 'lt' => [ 74 | 'numeric' => ':attribute 必须小于 :value', 75 | 'file' => ':attribute 必须小于 :value kb', 76 | 'string' => ':attribute 必须小于 :value 个字符', 77 | 'array' => ':attribute 必须小于 :value 项', 78 | ], 79 | 'lte' => [ 80 | 'numeric' => ':attribute 必须小于等于 :value', 81 | 'file' => ':attribute 必须小于等于 :value kb', 82 | 'string' => ':attribute 必须小于等于 :value 个字符', 83 | 'array' => ':attribute 必须小于等于 :value 项', 84 | ], 85 | 'max' => [ 86 | 'numeric' => ':attribute 的最大值为 :max', 87 | 'file' => ':attribute 的最大为 :max kb', 88 | 'string' => ':attribute 的最大长度为 :max 字符', 89 | 'array' => ':attribute 至多有 :max 项', 90 | ], 91 | 'mimes' => ':attribute 的文件类型必须是 :values', 92 | 'mimetypes' => ':attribute 的文件MIME必须是 :values', 93 | 'min' => [ 94 | 'numeric' => ':attribute 的最小值为 :min', 95 | 'file' => ':attribute 大小至少为 :min kb', 96 | 'string' => ':attribute 的最小长度为 :min 字符', 97 | 'array' => ':attribute 至少有 :min 项', 98 | ], 99 | 'not_in' => '选定的 :attribute 是无效的', 100 | 'not_regex' => ':attribute 不能匹配给定的正则', 101 | 'numeric' => ':attribute 必须是数字', 102 | 'present' => ':attribute 字段必须存在', 103 | 'regex' => ':attribute 格式是无效的', 104 | 'required' => ':attribute 字段是必须的', 105 | 'required_if' => ':attribute 字段是必须的当 :other 是 :value', 106 | 'required_unless' => ':attribute 字段是必须的,除非 :other 是在 :values 中', 107 | 'required_with' => ':attribute 字段是必须的当 :values 是存在的', 108 | 'required_with_all' => ':attribute 字段是必须的当 :values 是存在的', 109 | 'required_without' => ':attribute 字段是必须的当 :values 是不存在的', 110 | 'required_without_all' => ':attribute 字段是必须的当 没有一个 :values 是存在的', 111 | 'same' => ':attribute 和 :other 必须匹配', 112 | 'size' => [ 113 | 'numeric' => ':attribute 必须是 :size', 114 | 'file' => ':attribute 必须是 :size kb', 115 | 'string' => ':attribute 必须是 :size 个字符', 116 | 'array' => ':attribute 必须包括 :size 项', 117 | ], 118 | 'starts_with' => ':attribute 必须以 :values 为开头', 119 | 'string' => ':attribute 必须是一个字符串', 120 | 'timezone' => ':attribute 必须是个有效的时区', 121 | 'unique' => ':attribute 已存在', 122 | 'uploaded' => ':attribute 上传失败', 123 | 'url' => ':attribute 无效的格式', 124 | 'uuid' => ':attribute 无效的UUID格式', 125 | 'max_if' => [ 126 | 'numeric' => '当 :other 为 :value 时 :attribute 不能大于 :max', 127 | 'file' => '当 :other 为 :value 时 :attribute 不能大于 :max kb', 128 | 'string' => '当 :other 为 :value 时 :attribute 不能大于 :max 个字符', 129 | 'array' => '当 :other 为 :value 时 :attribute 最多只有 :max 个单元', 130 | ], 131 | 'min_if' => [ 132 | 'numeric' => '当 :other 为 :value 时 :attribute 必须大于等于 :min', 133 | 'file' => '当 :other 为 :value 时 :attribute 大小不能小于 :min kb', 134 | 'string' => '当 :other 为 :value 时 :attribute 至少为 :min 个字符', 135 | 'array' => '当 :other 为 :value 时 :attribute 至少有 :min 个单元', 136 | ], 137 | 'between_if' => [ 138 | 'numeric' => '当 :other 为 :value 时 :attribute 必须介于 :min - :max 之间', 139 | 'file' => '当 :other 为 :value 时 :attribute 必须介于 :min - :max kb 之间', 140 | 'string' => '当 :other 为 :value 时 :attribute 必须介于 :min - :max 个字符之间', 141 | 'array' => '当 :other 为 :value 时 :attribute 必须只有 :min - :max 个单元', 142 | ], 143 | /* 144 | |-------------------------------------------------------------------------- 145 | | Custom Validation Language Lines 146 | |-------------------------------------------------------------------------- 147 | | 148 | | Here you may specify custom validation messages for attributes using the 149 | | convention "attribute.rule" to name the lines. This makes it quick to 150 | | specify a specific custom language line for a given attribute rule. 151 | | 152 | */ 153 | 154 | 'custom' => [ 155 | 'attribute-name' => [ 156 | 'rule-name' => 'custom-message', 157 | ], 158 | ], 159 | 160 | /* 161 | |-------------------------------------------------------------------------- 162 | | Custom Validation Attributes 163 | |-------------------------------------------------------------------------- 164 | | 165 | | The following language lines are used to swap attribute place-holders 166 | | with something more reader friendly such as E-Mail Address instead 167 | | of "email". This simply helps us make messages a little cleaner. 168 | | 169 | */ 170 | 171 | 'attributes' => [], 172 | 'phone_number' => ':attribute 必须为一个有效的电话号码', 173 | 'telephone_number' => ':attribute 必须为一个有效的手机号码', 174 | 175 | 'chinese_word' => ':attribute 必须包含以下有效字符 (中文/英文,数字, 下划线)', 176 | 'sequential_array' => ':attribute 必须是一个有序数组', 177 | ]; 178 | -------------------------------------------------------------------------------- /test/Cases/ExampleTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 25 | $this->assertTrue(is_array($this->get('/'))); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/HttpTestCase.php: -------------------------------------------------------------------------------- 1 | client = make(Client::class); 36 | } 37 | 38 | public function __call($name, $arguments) 39 | { 40 | return $this->client->{$name}(...$arguments); 41 | } 42 | } -------------------------------------------------------------------------------- /test/bootstrap.php: -------------------------------------------------------------------------------- 1 | get(Hyperf\Contract\ApplicationInterface::class); 30 | --------------------------------------------------------------------------------