├── hyperf-im.xmind ├── .gitignore ├── .phpstorm.meta.php ├── config ├── autoload │ ├── aspects.php │ ├── commands.php │ ├── dependencies.php │ ├── processes.php │ ├── listeners.php │ ├── middlewares.php │ ├── translation.php │ ├── cache.php │ ├── exceptions.php │ ├── annotations.php │ ├── async_queue.php │ ├── redis.php │ ├── logger.php │ ├── devtool.php │ ├── databases.php │ └── server.php ├── routes.php ├── container.php └── config.php ├── route ├── web.php ├── server.php └── api.php ├── phpstan.neon ├── .env.example ├── src ├── Home │ ├── api │ │ ├── Friend.php │ │ ├── Group.php │ │ ├── User.php │ │ └── Upload.php │ ├── validated │ │ ├── UserLogin.php │ │ ├── GroupJoin.php │ │ ├── GroupCreate.php │ │ └── UserReg.php │ └── service │ │ ├── Group.php │ │ └── Auth.php └── WebSocket │ ├── Enter.php │ ├── controller │ ├── User.php │ ├── Message.php │ ├── Group.php │ ├── Auth.php │ └── Chat.php │ └── Server.php ├── app ├── Model │ ├── UserModel.php │ ├── FriendModel.php │ ├── UserGroupModel.php │ ├── FriendGroupModel.php │ ├── FriendChatLogModel.php │ ├── GroupModel.php │ ├── GroupChatLog.php │ └── Model.php ├── Process │ └── AsyncQueueConsumer.php ├── Request │ └── BaseRequest.php ├── Constants │ └── ErrorCode.php ├── Controller │ ├── IndexController.php │ └── AbstractController.php ├── Exception │ ├── BusinessException.php │ └── Handler │ │ ├── ValidationExceptionHandler.php │ │ └── AppExceptionHandler.php ├── Tools │ ├── Jwt.php │ └── Oss.php ├── Listener │ ├── AppServerStartListener.php │ ├── DbQueryExecutedListener.php │ └── QueueHandleListener.php └── Middleware │ └── AuthMiddleware.php ├── test ├── Cases │ └── ExampleTest.php ├── bootstrap.php └── HttpTestCase.php ├── deploy.test.yml ├── phpunit.xml ├── bin └── hyperf.php ├── .gitlab-ci.yml ├── Dockerfile ├── composer.json ├── .php_cs ├── file └── helper.php ├── README.md ├── hyperf_im.sql └── storage └── languages ├── zh_CN └── validation.php └── en └── validation.php /hyperf-im.xmind: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NiZerin/hyperf-im/HEAD/hyperf-im.xmind -------------------------------------------------------------------------------- /.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 | *.lock 13 | .phpunit* -------------------------------------------------------------------------------- /.phpstorm.meta.php: -------------------------------------------------------------------------------- 1 | [ 15 | \Hyperf\Validation\Middleware\ValidationMiddleware::class 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /config/autoload/translation.php: -------------------------------------------------------------------------------- 1 | 'zh_CN', 14 | 'fallback_locale' => 'en', 15 | 'path' => BASE_PATH . '/storage/languages', 16 | ]; 17 | -------------------------------------------------------------------------------- /config/routes.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'driver' => Hyperf\Cache\Driver\RedisDriver::class, 16 | 'packer' => Hyperf\Utils\Packer\PhpSerializerPacker::class, 17 | 'prefix' => 'c:', 18 | ], 19 | ]; 20 | -------------------------------------------------------------------------------- /app/Model/UserModel.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'http' => [ 16 | App\Exception\Handler\AppExceptionHandler::class, 17 | App\Exception\Handler\ValidationExceptionHandler::class, 18 | ], 19 | ], 20 | ]; 21 | -------------------------------------------------------------------------------- /app/Model/FriendModel.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'paths' => [ 16 | BASE_PATH . '/app', 17 | BASE_PATH . '/src', 18 | ], 19 | 'ignore_annotations' => [ 20 | 'mixin', 21 | ], 22 | ], 23 | ]; 24 | -------------------------------------------------------------------------------- /app/Model/FriendChatLogModel.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'driver' => Hyperf\AsyncQueue\Driver\RedisDriver::class, 16 | 'channel' => 'queue', 17 | 'timeout' => 2, 18 | 'retry_seconds' => 5, 19 | 'handle_timeout' => 10, 20 | 'processes' => 1, 21 | ], 22 | ]; 23 | -------------------------------------------------------------------------------- /app/Constants/ErrorCode.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 26 | $this->assertTrue(is_array($this->get('/'))); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /deploy.test.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | hyperf: 4 | image: $REGISTRY_URL/$PROJECT_NAME:test 5 | environment: 6 | - "APP_PROJECT=hyperf" 7 | - "APP_ENV=test" 8 | ports: 9 | - 9501:9501 10 | deploy: 11 | replicas: 1 12 | restart_policy: 13 | condition: on-failure 14 | delay: 5s 15 | max_attempts: 5 16 | update_config: 17 | parallelism: 2 18 | delay: 5s 19 | order: start-first 20 | networks: 21 | - hyperf_net 22 | configs: 23 | - source: hyperf_v1.0 24 | target: /opt/www/.env 25 | configs: 26 | hyperf_v1.0: 27 | external: true 28 | networks: 29 | hyperf_net: 30 | external: true 31 | -------------------------------------------------------------------------------- /app/Controller/IndexController.php: -------------------------------------------------------------------------------- 1 | request->input('user', 'Hyperf IM'); 20 | $method = $this->request->getMethod(); 21 | 22 | return [ 23 | 'method' => $method, 24 | 'message' => "Hello {$user}.", 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./test 14 | 15 | 16 | 17 | 18 | ./app 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Home/validated/UserLogin.php: -------------------------------------------------------------------------------- 1 | 'required|digits:11|exists:app_users,phone', 36 | 'pwd' => 'required|alpha_dash|min:6|max:16', 37 | ]; 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /src/Home/validated/GroupJoin.php: -------------------------------------------------------------------------------- 1 | 'required|exists:app_group,id' 41 | ]; 42 | } 43 | } -------------------------------------------------------------------------------- /app/Exception/BusinessException.php: -------------------------------------------------------------------------------- 1 | get(\Hyperf\Contract\ApplicationInterface::class); 23 | $application->run(); 24 | })(); -------------------------------------------------------------------------------- /config/container.php: -------------------------------------------------------------------------------- 1 | 'required|min:3|max:16', 36 | 'cover' => 'required|url', 37 | 'notice' => 'required', 38 | 'desc' => 'required' 39 | ]; 40 | } 41 | } -------------------------------------------------------------------------------- /config/autoload/redis.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'host' => env('REDIS_HOST', 'localhost'), 16 | 'auth' => env('REDIS_AUTH', null), 17 | 'port' => (int) env('REDIS_PORT', 6379), 18 | 'db' => (int) env('REDIS_DB', 0), 19 | 'pool' => [ 20 | 'min_connections' => 1, 21 | 'max_connections' => 10, 22 | 'connect_timeout' => 10.0, 23 | 'wait_timeout' => 3.0, 24 | 'heartbeat' => -1, 25 | 'max_idle_time' => (float) env('REDIS_MAX_IDLE_TIME', 60), 26 | ], 27 | ], 28 | ]; 29 | -------------------------------------------------------------------------------- /test/bootstrap.php: -------------------------------------------------------------------------------- 1 | get(Hyperf\Contract\ApplicationInterface::class); 31 | -------------------------------------------------------------------------------- /app/Controller/AbstractController.php: -------------------------------------------------------------------------------- 1 | 'required|alpha_dash|min:3|max:16', 38 | 'pwd' => 'required|alpha_dash|min:6|max:16|confirmed', 39 | 'pwd_confirmation ' => 'alpha_dash|min:6|max:16', 40 | 'phone' => 'required|digits:11|unique:app_users,phone', 41 | ]; 42 | } 43 | } -------------------------------------------------------------------------------- /app/Tools/Jwt.php: -------------------------------------------------------------------------------- 1 | env('APP_NAME', 'Hyperf-im'), 18 | // 生产环境使用 prod 值 19 | 'app_env' => env('APP_ENV', 'dev'), 20 | // 是否使用注解扫描缓存 21 | 'scan_cacheable' => env('SCAN_CACHEABLE', false), 22 | StdoutLoggerInterface::class => [ 23 | 'log_level' => [ 24 | LogLevel::ALERT, 25 | LogLevel::CRITICAL, 26 | LogLevel::DEBUG, 27 | LogLevel::EMERGENCY, 28 | LogLevel::ERROR, 29 | LogLevel::INFO, 30 | LogLevel::NOTICE, 31 | LogLevel::WARNING, 32 | ], 33 | ], 34 | ]; 35 | -------------------------------------------------------------------------------- /config/autoload/logger.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'handler' => [ 16 | 'class' => Monolog\Handler\StreamHandler::class, 17 | 'constructor' => [ 18 | 'stream' => BASE_PATH . '/runtime/logs/hyperf.log', 19 | 'level' => Monolog\Logger::DEBUG, 20 | ], 21 | ], 22 | 'formatter' => [ 23 | 'class' => Monolog\Formatter\LineFormatter::class, 24 | 'constructor' => [ 25 | 'format' => null, 26 | 'dateFormat' => 'Y-m-d H:i:s', 27 | 'allowInlineLineBreaks' => true, 28 | ], 29 | ], 30 | 'processors' => [ 31 | ], 32 | ], 33 | ]; 34 | -------------------------------------------------------------------------------- /route/api.php: -------------------------------------------------------------------------------- 1 | [AuthMiddleware::class] 27 | ]); 28 | 29 | Router::addGroup('/group', function () { 30 | Router::post('/create', 'Src\Home\api\Group@create'); 31 | Router::post('/join', 'Src\Home\api\Group@join'); 32 | }, [ 33 | 'middleware' => [AuthMiddleware::class] 34 | ]); 35 | }); -------------------------------------------------------------------------------- /app/Tools/Oss.php: -------------------------------------------------------------------------------- 1 | accessKeyId = env('OSS_ACCESS_KEY_ID'); 31 | $this->accessKeySecret = env('OSS_ACCESS_KEY_SECRET'); 32 | $this->bucket = env('OSS_BUCKET'); 33 | $this->endpoint = env('OSS_ENDPOINT'); 34 | $this->client = new OssClient($this->accessKeyId, $this->accessKeySecret, $this->endpoint); 35 | } 36 | 37 | public function upload($object, $content) 38 | { 39 | $this->client->putObject($this->bucket, $object, $content); 40 | } 41 | } -------------------------------------------------------------------------------- /src/WebSocket/Enter.php: -------------------------------------------------------------------------------- 1 | client = make(Client::class); 36 | } 37 | 38 | public function __call($name, $arguments) 39 | { 40 | return $this->client->{$name}(...$arguments); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/WebSocket/controller/User.php: -------------------------------------------------------------------------------- 1 | hSet('im_user_online', $fd, (string)$user->id); 34 | redis()->hSet('user_id_to_fd', (string)$user->id, $fd); 35 | } 36 | 37 | /** 38 | * @param \Swoole\WebSocket\Server $server 39 | * @param int $fd 40 | */ 41 | public static function setOutline($server, int $fd): void 42 | { 43 | $fd = (string)$fd; 44 | 45 | $userId = redis()->hGet('im_user_online', $fd); 46 | 47 | redis()->hDel('user_id_to_fd', $userId); 48 | redis()->hDel('im_user_online', $fd); 49 | 50 | $server->close($fd); 51 | } 52 | } -------------------------------------------------------------------------------- /config/autoload/devtool.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'amqp' => [ 16 | 'consumer' => [ 17 | 'namespace' => 'App\\Amqp\\Consumer', 18 | ], 19 | 'producer' => [ 20 | 'namespace' => 'App\\Amqp\\Producer', 21 | ], 22 | ], 23 | 'aspect' => [ 24 | 'namespace' => 'App\\Aspect', 25 | ], 26 | 'command' => [ 27 | 'namespace' => 'App\\Command', 28 | ], 29 | 'controller' => [ 30 | 'namespace' => 'App\\Controller', 31 | ], 32 | 'job' => [ 33 | 'namespace' => 'App\\Job', 34 | ], 35 | 'listener' => [ 36 | 'namespace' => 'App\\Listener', 37 | ], 38 | 'middleware' => [ 39 | 'namespace' => 'App\\Middleware', 40 | ], 41 | 'Process' => [ 42 | 'namespace' => 'App\\Processes', 43 | ], 44 | ], 45 | ]; 46 | -------------------------------------------------------------------------------- /app/Exception/Handler/ValidationExceptionHandler.php: -------------------------------------------------------------------------------- 1 | stopPropagation(); 28 | /** @var \Hyperf\Validation\ValidationException $throwable */ 29 | /** @var $body string */ 30 | $body = $throwable->validator->errors()->first(); 31 | return $response 32 | ->withStatus($throwable->status) 33 | ->withBody(new SwooleStream(error($body))) 34 | ->withHeader('Content-Type', 'application/json'); 35 | } 36 | 37 | public function isValid(Throwable $throwable): bool 38 | { 39 | return $throwable instanceof ValidationException; 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /app/Listener/AppServerStartListener.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 28 | } 29 | 30 | /** 31 | * @return string[] 32 | */ 33 | public function listen(): array 34 | { 35 | return [ 36 | MainWorkerStart::class 37 | ]; 38 | } 39 | 40 | /** 41 | * @param object $event 42 | */ 43 | public function process(object $event) 44 | { 45 | $this->logger->debug("Hyperf-im Starting................"); 46 | 47 | go(function () { 48 | redis()->select((int)env('REDIS_DB')); 49 | redis()->flushDB(); 50 | }); 51 | 52 | $this->logger->debug("Hyperf-im Success................."); 53 | } 54 | } -------------------------------------------------------------------------------- /app/Exception/Handler/AppExceptionHandler.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 31 | } 32 | 33 | public function handle(Throwable $throwable, ResponseInterface $response) 34 | { 35 | $this->logger->error(sprintf('%s[%s] in %s', $throwable->getMessage(), $throwable->getLine(), $throwable->getFile())); 36 | $this->logger->error($throwable->getTraceAsString()); 37 | return $response->withHeader("Server", "Hyperf")->withStatus(500)->withBody(new SwooleStream('Internal Server Error.')); 38 | } 39 | 40 | public function isValid(Throwable $throwable): bool 41 | { 42 | return true; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/WebSocket/Server.php: -------------------------------------------------------------------------------- 1 | data, true); 34 | 35 | if (is_null($message)) { 36 | $server->push($frame->fd, error('message content error')); 37 | return; 38 | } 39 | 40 | if (!array_key_exists('action', $message)) { 41 | $server->push($frame->fd, error('message action error')); 42 | return; 43 | } 44 | 45 | switch ($message['action']) { 46 | case ('send_msg_to_user') : { 47 | Chat::send($frame, $server, $message); 48 | break; 49 | } 50 | case ('send_smg_to_group') : { 51 | Group::send($server, $message); 52 | break; 53 | } 54 | default : { 55 | return; 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /config/autoload/databases.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'driver' => env('DB_DRIVER', 'mysql'), 16 | 'host' => env('DB_HOST', 'localhost'), 17 | 'port' => env('DB_PORT', 3306), 18 | 'database' => env('DB_DATABASE', 'hyperf'), 19 | 'username' => env('DB_USERNAME', 'root'), 20 | 'password' => env('DB_PASSWORD', ''), 21 | 'charset' => env('DB_CHARSET', 'utf8'), 22 | 'collation' => env('DB_COLLATION', 'utf8_unicode_ci'), 23 | 'prefix' => env('DB_PREFIX', ''), 24 | 'pool' => [ 25 | 'min_connections' => 1, 26 | 'max_connections' => 10, 27 | 'connect_timeout' => 10.0, 28 | 'wait_timeout' => 3.0, 29 | 'heartbeat' => -1, 30 | 'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60), 31 | ], 32 | 'cache' => [ 33 | 'handler' => Hyperf\ModelCache\Handler\RedisHandler::class, 34 | 'cache_key' => 'mc:%s:m:%s:%s:%s', 35 | 'prefix' => 'default', 36 | 'ttl' => 3600 * 24, 37 | 'empty_model_ttl' => 600, 38 | 'load_script' => true, 39 | ], 40 | 'commands' => [ 41 | 'gen:model' => [ 42 | 'path' => 'app/Model', 43 | 'force_casts' => true, 44 | 'inheritance' => 'Model', 45 | ], 46 | ], 47 | ], 48 | ]; 49 | -------------------------------------------------------------------------------- /app/Listener/DbQueryExecutedListener.php: -------------------------------------------------------------------------------- 1 | logger = $container->get(LoggerFactory::class)->get('sql'); 37 | } 38 | 39 | public function listen(): array 40 | { 41 | return [ 42 | QueryExecuted::class, 43 | ]; 44 | } 45 | 46 | /** 47 | * @param QueryExecuted $event 48 | */ 49 | public function process(object $event) 50 | { 51 | if ($event instanceof QueryExecuted) { 52 | $sql = $event->sql; 53 | if (! Arr::isAssoc($event->bindings)) { 54 | foreach ($event->bindings as $key => $value) { 55 | $sql = Str::replaceFirst('?', "'{$value}'", $sql); 56 | } 57 | } 58 | 59 | $this->logger->info(sprintf('[%s] %s', $event->time, $sql)); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Home/service/Group.php: -------------------------------------------------------------------------------- 1 | create($data); 37 | UserGroupModel::query()->create(['uid' => $user->id, 'group_id' => $group->id]); 38 | } catch (\Exception $exception) { 39 | throw new \Exception($exception->getMessage()); 40 | } 41 | } 42 | 43 | /** 44 | * @param array $data 45 | * @param object $user 46 | * 47 | * @throws \Exception 48 | */ 49 | public static function join(array $data, object $user) 50 | { 51 | $check = UserGroupModel::query() 52 | ->where('uid', $user['id']) 53 | ->where('group_id', $data['id']) 54 | ->first(); 55 | 56 | if (!is_null($check)) { 57 | throw new \Exception('You are already a member of the group.'); 58 | } 59 | 60 | try { 61 | UserGroupModel::query()->create(['uid' => $user->id, 'group_id' => $data['id']]); 62 | } catch (\Exception $exception) { 63 | throw new \Exception($exception->getMessage()); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /app/Middleware/AuthMiddleware.php: -------------------------------------------------------------------------------- 1 | response = $response; 31 | $this->request = $request; 32 | } 33 | 34 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 35 | { 36 | $token = $request->getHeader('Authorization')[0] ?? ''; 37 | 38 | if (empty($token)) { 39 | return $this->response->json(data('token is lost')); 40 | } 41 | 42 | $check = (new Auth())->checkToken($token); 43 | 44 | if (is_bool($check)) { 45 | return $this->response->json(data('token check failed')); 46 | } else { 47 | $request = Context::get(ServerRequestInterface::class); 48 | 49 | $request = $request->withAttribute('user', $check); 50 | 51 | Context::set(ServerRequestInterface::class, $request); 52 | 53 | return $handler->handle($request); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/Home/api/Group.php: -------------------------------------------------------------------------------- 1 | validated(); 35 | $user = $this->request->getAttribute('user'); 36 | $data['uid'] = $user->id; 37 | 38 | try { 39 | GroupService::create($data, $user); 40 | return $this->response->json(data('create success')); 41 | } catch (\Exception $exception) { 42 | return $this->response->json(data($exception->getMessage())); 43 | } 44 | } 45 | 46 | /** 47 | * @param GroupJoin $groupJoin 48 | * 49 | * @return \Psr\Http\Message\ResponseInterface 50 | */ 51 | public function join(GroupJoin $groupJoin): \Psr\Http\Message\ResponseInterface 52 | { 53 | $data = $groupJoin->validated(); 54 | $user = $this->request->getAttribute('user'); 55 | 56 | try { 57 | GroupService::join($data, $user); 58 | return $this->response->json(data('join success')); 59 | } catch (\Exception $exception) { 60 | return $this->response->json(data($exception->getMessage())); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/WebSocket/controller/Group.php: -------------------------------------------------------------------------------- 1 | select('uid') 34 | ->where('group_id', $message['target_group_id']) 35 | ->get(); 36 | 37 | $msgData = [ 38 | 'action' => 'msg_from_group', 39 | 'message' => [ 40 | 'msg_time' => timestamp(), 41 | 'msg_id' => uuid(), 42 | 'msg_type' => $message['message']['msg_type'], 43 | 'msg_body' => $message['message']['msg_body'], 44 | ], 45 | 'from_user' => [ 46 | 'id' => $message['from_user_id'] 47 | ], 48 | 'target_group' => [ 49 | 'id' => $message['target_group_id'] 50 | ] 51 | ]; 52 | 53 | foreach ($userIds as $k => $v) { 54 | $userFd = redis()->hGet('user_id_to_fd', (string)$v->uid); 55 | 56 | if (is_bool($userFd)) { 57 | continue; 58 | } 59 | 60 | if ($server->isEstablished((int)$userFd)) { 61 | $server->push($userFd, json_encode($msgData, 256)); 62 | } 63 | } 64 | } 65 | 66 | public static function save() 67 | { 68 | 69 | } 70 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Default Dockerfile 2 | # 3 | # @link https://www.hyperf.io 4 | # @document https://doc.hyperf.io 5 | # @contact group@hyperf.io 6 | # @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE 7 | 8 | FROM hyperf/hyperf:7.2-alpine-v3.9-cli 9 | LABEL maintainer="Hyperf Developers " version="1.0" license="MIT" 10 | 11 | ## 12 | # ---------- env settings ---------- 13 | ## 14 | # --build-arg timezone=Asia/Shanghai 15 | ARG timezone 16 | 17 | ENV TIMEZONE=${timezone:-"Asia/Shanghai"} \ 18 | COMPOSER_VERSION=1.9.1 \ 19 | APP_ENV=prod 20 | 21 | # update 22 | RUN set -ex \ 23 | && apk update \ 24 | # install composer 25 | && cd /tmp \ 26 | && wget https://github.com/composer/composer/releases/download/${COMPOSER_VERSION}/composer.phar \ 27 | && chmod u+x composer.phar \ 28 | && mv composer.phar /usr/local/bin/composer \ 29 | # show php version and extensions 30 | && php -v \ 31 | && php -m \ 32 | # ---------- some config ---------- 33 | && cd /etc/php7 \ 34 | # - config PHP 35 | && { \ 36 | echo "upload_max_filesize=100M"; \ 37 | echo "post_max_size=108M"; \ 38 | echo "memory_limit=1024M"; \ 39 | echo "date.timezone=${TIMEZONE}"; \ 40 | } | tee conf.d/99-overrides.ini \ 41 | # - config timezone 42 | && ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \ 43 | && echo "${TIMEZONE}" > /etc/timezone \ 44 | # ---------- clear works ---------- 45 | && rm -rf /var/cache/apk/* /tmp/* /usr/share/man \ 46 | && echo -e "\033[42;37m Build Completed :).\033[0m\n" 47 | 48 | WORKDIR /opt/www 49 | 50 | # Composer Cache 51 | # COPY ./composer.* /opt/www/ 52 | # RUN composer install --no-dev --no-scripts 53 | 54 | COPY . /opt/www 55 | RUN composer install --no-dev -o 56 | 57 | EXPOSE 9501 58 | 59 | ENTRYPOINT ["php", "/opt/www/bin/hyperf.php", "start"] 60 | -------------------------------------------------------------------------------- /src/Home/api/User.php: -------------------------------------------------------------------------------- 1 | validated(); 42 | 43 | $data['pwd'] = sha1($data['pwd']); 44 | 45 | try { 46 | UserModel::query()->create($data); 47 | return $this->response->json(data('register success')); 48 | } catch (\Exception $exception) { 49 | return $this->response->json(data($exception->getMessage())); 50 | } 51 | } 52 | 53 | /** 54 | * 登录 55 | * 56 | * @param \Src\Home\validated\UserLogin $userLogin 57 | * @param \Src\Home\service\Auth $auth 58 | * 59 | * @return \Psr\Http\Message\ResponseInterface 60 | */ 61 | public function login(UserLogin $userLogin, Auth $auth): \Psr\Http\Message\ResponseInterface 62 | { 63 | $data = $userLogin->validated(); 64 | 65 | $userInfo = UserModel::query()->where('phone', $data['phone'])->first(); 66 | 67 | if (sha1($data['pwd']) != $userInfo->pwd) { 68 | $this->response->json(data('password error')); 69 | } 70 | 71 | $token = $auth->makeToken($userInfo); 72 | 73 | return $this->response->json(data('login success', 0, ['token' => $token])); 74 | } 75 | } -------------------------------------------------------------------------------- /app/Model/Model.php: -------------------------------------------------------------------------------- 1 | 'datetime:Y-m-d H:i:s', 53 | 'updated_at' => 'datetime:Y-m-d H:i:s', 54 | ]; 55 | 56 | /** 57 | * @param $value 58 | * 59 | * @return false|string 60 | */ 61 | public function getUpdatedAtAttribute($value) 62 | { 63 | return $this -> getDateTime($value); 64 | } 65 | 66 | /** 67 | * @param $value 68 | * 69 | * @return false|string 70 | */ 71 | public function getCreatedAtAttribute($value) 72 | { 73 | return $this -> getDateTime($value); 74 | } 75 | 76 | /** 77 | * @param $value 78 | * 79 | * @return false|string 80 | */ 81 | protected function getDateTime($value) 82 | { 83 | if (is_string($value)) $value = strtotime($value); 84 | return date("Y-m-d H:i:s", $value); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /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 | SwooleEvent::ON_REQUEST => [Hyperf\HttpServer\Server::class, 'onRequest'], 27 | ], 28 | ], 29 | [ 30 | 'name' => 'ws', 31 | 'type' => Server::SERVER_WEBSOCKET, 32 | 'host' => '0.0.0.0', 33 | 'port' => 9502, 34 | 'sock_type' => SWOOLE_SOCK_TCP, 35 | 'callbacks' => [ 36 | SwooleEvent::ON_HAND_SHAKE => [Hyperf\WebSocketServer\Server::class, 'onHandShake'], 37 | SwooleEvent::ON_MESSAGE => [Hyperf\WebSocketServer\Server::class, 'onMessage'], 38 | SwooleEvent::ON_CLOSE => [Hyperf\WebSocketServer\Server::class, 'onClose'], 39 | ], 40 | ], 41 | ], 42 | 'settings' => [ 43 | 'enable_coroutine' => true, 44 | 'worker_num' => swoole_cpu_num(), 45 | 'pid_file' => BASE_PATH . '/runtime/hyperf.pid', 46 | 'open_tcp_nodelay' => true, 47 | 'max_coroutine' => 100000, 48 | 'open_http2_protocol' => true, 49 | 'max_request' => 100000, 50 | 'socket_buffer_size' => 2 * 1024 * 1024, 51 | 'buffer_output_size' => 2 * 1024 * 1024, 52 | ], 53 | 'callbacks' => [ 54 | SwooleEvent::ON_BEFORE_START => [Hyperf\Framework\Bootstrap\ServerStartCallback::class, 'beforeStart'], 55 | SwooleEvent::ON_WORKER_START => [Hyperf\Framework\Bootstrap\WorkerStartCallback::class, 'onWorkerStart'], 56 | SwooleEvent::ON_PIPE_MESSAGE => [Hyperf\Framework\Bootstrap\PipeMessageCallback::class, 'onPipeMessage'], 57 | ], 58 | ]; 59 | -------------------------------------------------------------------------------- /src/WebSocket/controller/Auth.php: -------------------------------------------------------------------------------- 1 | get ?? []); 33 | 34 | if (is_bool($check)) { 35 | $server->push($request->fd, error('token check failed')); 36 | $server->close($request->fd); 37 | } else { 38 | self::checkOnline($server, $check->id); 39 | User::setOnline($check, $request->fd); 40 | $server->push($request->fd, success('login success')); 41 | } 42 | } 43 | 44 | /** 45 | * 单点登录 46 | * 47 | * @param \Swoole\WebSocket\Server $server 48 | * @param $userId 49 | */ 50 | public static function checkOnline($server, $userId): void 51 | { 52 | $afterFd = (int)redis()->hGet('user_id_to_fd', (string)$userId); 53 | 54 | if (is_bool($afterFd)) { 55 | return; 56 | } 57 | 58 | if ($server->isEstablished($afterFd)) { 59 | $server->push($afterFd, error('you account login other location')); 60 | User::setOutline($server, $afterFd); 61 | } 62 | } 63 | 64 | /** 65 | * 校验token 66 | * @param array $data 67 | * 68 | * @return false|\Hyperf\Database\Model\Builder|\Hyperf\Database\Model\Builder[]|\Hyperf\Database\Model\Collection|\Hyperf\Database\Model\Model 69 | */ 70 | public static function checkToken(array $data) 71 | { 72 | if (!array_key_exists('token', $data)) { 73 | return false; 74 | } 75 | 76 | if (empty($data['token'])) { 77 | return false; 78 | } 79 | 80 | return (new \Src\Home\service\Auth())->checkToken($data['token']); 81 | } 82 | } -------------------------------------------------------------------------------- /src/Home/api/Upload.php: -------------------------------------------------------------------------------- 1 | request->file('image'); 35 | 36 | if (empty($file)) { 37 | return $this->response->json(data('please upload image')); 38 | } 39 | 40 | return $this->saveOss($file, 5); 41 | } 42 | 43 | /** 44 | * @return \Psr\Http\Message\ResponseInterface 45 | */ 46 | public function file(): \Psr\Http\Message\ResponseInterface 47 | { 48 | $file = $this->request->file('file'); 49 | 50 | if (empty($file)) { 51 | return $this->response->json(data('please upload file')); 52 | } 53 | 54 | return $this->saveOss($file, 50); 55 | } 56 | 57 | /** 58 | * @param $file null|UploadedFile|UploadedFile[] 59 | * 60 | * @param int $size 61 | * 62 | * @return \Psr\Http\Message\ResponseInterface 63 | */ 64 | public function saveOss($file, int $size): \Psr\Http\Message\ResponseInterface 65 | { 66 | $fileSize = $file->getSize(); 67 | 68 | if ($fileSize / 1024 / 1024 > $size) { 69 | return $this->response->json(data('file size too max, only'. $size .'mb')); 70 | } 71 | 72 | $content = $file->getStream()->getContents(); 73 | 74 | $extName = $file->getExtension(); 75 | 76 | $fileName = 'image/' . date('Ymd') . '/' . time() . rand(1000, 9999) . '.' .$extName; 77 | 78 | try { 79 | (new Oss())->upload($fileName, $content); 80 | return $this->response->json(data('upload success', 0, ['url' => env('OSS_URL') . $fileName])); 81 | } catch (\Exception $exception) { 82 | return $this->response->json(data('upload failed, please try again')); 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /src/WebSocket/controller/Chat.php: -------------------------------------------------------------------------------- 1 | hGet('im_user_online', (string)$frame->fd); 35 | $targetUserFd = redis()->hGet('user_id_to_fd', (string)$message['target_user_id']); 36 | 37 | $msgData = [ 38 | 'action' => 'msg_from_user', 39 | 'message' => [ 40 | 'msg_time' => timestamp(), 41 | 'msg_id' => uuid(), 42 | 'msg_type' => $message['message']['msg_type'], 43 | 'msg_body' => $message['message']['msg_body'], 44 | ], 45 | 'from_user' => [ 46 | 'id' => $userId 47 | ], 48 | 'target_user' => [ 49 | 'id' => $message['target_user_id'] 50 | ] 51 | ]; 52 | 53 | if ($server->isEstablished((int)$targetUserFd)) { 54 | $server->push($targetUserFd, json_encode($msgData, 256)); 55 | } 56 | 57 | $server->push($frame->fd, json_encode($msgData, 256)); 58 | 59 | self::save($msgData); 60 | } 61 | 62 | /** 63 | * @param array $msgData 64 | */ 65 | public static function save(array $msgData) 66 | { 67 | go(function () use ($msgData) { 68 | FriendChatLogModel::query()->create([ 69 | 'from_uid' => $msgData['from_user']['id'], 70 | 'target_uid' => $msgData['target_user']['id'], 71 | 'msg_id' => $msgData['message']['msg_id'], 72 | 'msg_time' => $msgData['message']['msg_time'], 73 | 'msg_type' => $msgData['message']['msg_type'], 74 | 'msg_body' => json_encode($msgData['message']['msg_body'], 256), 75 | ]); 76 | }); 77 | } 78 | } -------------------------------------------------------------------------------- /src/Home/service/Auth.php: -------------------------------------------------------------------------------- 1 | $user->id, 33 | 'code' => $user->code, 34 | 'exp_time' => time() + 86400 35 | ]; 36 | 37 | $token = Jwt::encode($data); 38 | 39 | $this->tokenToRedis((string)$user->id, $token); 40 | 41 | return 'Bearer ' . $token; 42 | } 43 | 44 | /** 45 | * 检查 token 46 | * 47 | * @param string $bearerToken 48 | * 49 | * @return false|\Hyperf\Database\Model\Builder|\Hyperf\Database\Model\Builder[]|\Hyperf\Database\Model\Collection|\Hyperf\Database\Model\Model 50 | */ 51 | public function checkToken(string $bearerToken) 52 | { 53 | if (is_int(strpos($bearerToken, 'Bearer'))) { 54 | [, $token] = explode(' ', $bearerToken); 55 | } else { 56 | return false; 57 | } 58 | 59 | if (is_null($token)) { 60 | return false; 61 | } 62 | 63 | try { 64 | $data = Jwt::decode($token); 65 | } catch (\Exception $exception) { 66 | return false; 67 | } 68 | 69 | if (time() > $data->exp_time) { 70 | return false; 71 | } 72 | 73 | $user = UserModel::query()->find($data->user_id); 74 | 75 | if (is_null($user)) { 76 | return false; 77 | } else { 78 | return $user; 79 | } 80 | } 81 | 82 | /** 83 | * @param string $userId 84 | * @param string $token 85 | * 86 | * @return void 87 | */ 88 | public function tokenToRedis(string $userId, string $token): void 89 | { 90 | redis()->hSet('user_token', $userId, $token); 91 | } 92 | 93 | /** 94 | * @param int $userId 95 | * 96 | * @return string 97 | */ 98 | public function tokenFromRedis(int $userId): string 99 | { 100 | return redis()->hGet('user_token', $userId); 101 | } 102 | } -------------------------------------------------------------------------------- /app/Listener/QueueHandleListener.php: -------------------------------------------------------------------------------- 1 | logger = $loggerFactory->get('queue'); 43 | $this->formatter = $formatter; 44 | } 45 | 46 | public function listen(): array 47 | { 48 | return [ 49 | AfterHandle::class, 50 | BeforeHandle::class, 51 | FailedHandle::class, 52 | RetryHandle::class, 53 | ]; 54 | } 55 | 56 | public function process(object $event) 57 | { 58 | if ($event instanceof Event && $event->message->job()) { 59 | $job = $event->message->job(); 60 | $jobClass = get_class($job); 61 | $date = date('Y-m-d H:i:s'); 62 | 63 | switch (true) { 64 | case $event instanceof BeforeHandle: 65 | $this->logger->info(sprintf('[%s] Processing %s.', $date, $jobClass)); 66 | break; 67 | case $event instanceof AfterHandle: 68 | $this->logger->info(sprintf('[%s] Processed %s.', $date, $jobClass)); 69 | break; 70 | case $event instanceof FailedHandle: 71 | $this->logger->error(sprintf('[%s] Failed %s.', $date, $jobClass)); 72 | $this->logger->error($this->formatter->format($event->getThrowable())); 73 | break; 74 | case $event instanceof RetryHandle: 75 | $this->logger->warning(sprintf('[%s] Retried %s.', $date, $jobClass)); 76 | break; 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nizerin/hyperf-im", 3 | "type": "project", 4 | "keywords": [ 5 | "php", 6 | "swoole", 7 | "hyperf", 8 | "websocket", 9 | "im", 10 | "room chat" 11 | ], 12 | "description": "websocket service,room chat,im, base of Hyperf", 13 | "license": "Apache-2.0", 14 | "require": { 15 | "php": ">=7.2", 16 | "ext-swoole": ">=4.4", 17 | "hyperf/cache": "~2.0.0", 18 | "hyperf/command": "~2.0.0", 19 | "hyperf/config": "~2.0.0", 20 | "hyperf/db-connection": "~2.0.0", 21 | "hyperf/framework": "~2.0.0", 22 | "hyperf/guzzle": "~2.0.0", 23 | "hyperf/http-server": "~2.0.0", 24 | "hyperf/logger": "~2.0.0", 25 | "hyperf/memory": "~2.0.0", 26 | "hyperf/process": "~2.0.0", 27 | "hyperf/redis": "~2.0.0", 28 | "hyperf/constants": "~2.0.0", 29 | "hyperf/async-queue": "~2.0.0", 30 | "hyperf/model-cache": "~2.0.0", 31 | "hyperf/websocket-server": "~2.0.0", 32 | "ext-json": "*", 33 | "ext-redis": "*", 34 | "firebase/php-jwt": "^5.2", 35 | "hyperf/validation": "~2.0.0", 36 | "ramsey/uuid": "^4.0", 37 | "aliyuncs/oss-sdk-php": "^2.3" 38 | }, 39 | "require-dev": { 40 | "swoft/swoole-ide-helper": "^4.2", 41 | "friendsofphp/php-cs-fixer": "^2.14", 42 | "mockery/mockery": "^1.0", 43 | "doctrine/common": "^2.9", 44 | "phpstan/phpstan": "^0.12", 45 | "hyperf/devtool": "~2.0.0", 46 | "hyperf/testing": "~2.0.0" 47 | }, 48 | "suggest": { 49 | "ext-openssl": "Required to use HTTPS.", 50 | "ext-json": "Required to use JSON.", 51 | "ext-pdo": "Required to use MySQL Client.", 52 | "ext-pdo_mysql": "Required to use MySQL Client.", 53 | "ext-redis": "Required to use Redis Client." 54 | }, 55 | "autoload": { 56 | "psr-4": { 57 | "App\\": "app/", 58 | "Src\\": "src/" 59 | }, 60 | "files": [ 61 | "file/helper.php" 62 | ] 63 | }, 64 | "autoload-dev": { 65 | "psr-4": { 66 | "HyperfTest\\": "./test/" 67 | } 68 | }, 69 | "minimum-stability": "dev", 70 | "prefer-stable": true, 71 | "extra": [], 72 | "scripts": { 73 | "post-root-package-install": [ 74 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 75 | ], 76 | "post-autoload-dump": [ 77 | "rm -rf runtime/container" 78 | ], 79 | "test": "co-phpunit -c phpunit.xml --colors=always", 80 | "cs-fix": "php-cs-fixer fix $1", 81 | "analyse": "phpstan analyse --memory-limit 300M -l 0 -c phpstan.neon ./app ./config", 82 | "start": "php ./bin/hyperf.php start" 83 | }, 84 | "repositories": { 85 | "packagist": { 86 | "type": "composer", 87 | "url": "https://mirrors.aliyun.com/composer" 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 14 | ->setRules([ 15 | '@PSR2' => true, 16 | '@Symfony' => true, 17 | '@DoctrineAnnotation' => true, 18 | '@PhpCsFixer' => true, 19 | 'header_comment' => [ 20 | 'commentType' => '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 | 'class_attributes_separation' => true, 66 | 'combine_consecutive_unsets' => true, 67 | 'declare_strict_types' => true, 68 | 'linebreak_after_opening_tag' => true, 69 | 'lowercase_constants' => true, 70 | 'lowercase_static_reference' => true, 71 | 'no_useless_else' => true, 72 | 'no_unused_imports' => true, 73 | 'not_operator_with_successor_space' => true, 74 | 'not_operator_with_space' => false, 75 | 'ordered_class_elements' => true, 76 | 'php_unit_strict' => false, 77 | 'phpdoc_separation' => false, 78 | 'single_quote' => true, 79 | 'standardize_not_equals' => true, 80 | 'multiline_comment_opening_closing' => true, 81 | ]) 82 | ->setFinder( 83 | PhpCsFixer\Finder::create() 84 | ->exclude('public') 85 | ->exclude('runtime') 86 | ->exclude('vendor') 87 | ->in(__DIR__) 88 | ) 89 | ->setUsingCache(false); 90 | -------------------------------------------------------------------------------- /file/helper.php: -------------------------------------------------------------------------------- 1 | get(Redis::class); 35 | } 36 | } 37 | 38 | if (!function_exists('success')) { 39 | /** 40 | * @param string $msg 41 | * @param int $success_code 42 | * @param array|null $data 43 | * 44 | * @return string 45 | */ 46 | function success(string $msg, int $success_code = 0, array $data = null): string 47 | { 48 | return json($msg, $success_code, $data); 49 | } 50 | } 51 | 52 | if (!function_exists('error')) { 53 | /** 54 | * @param string $msg 55 | * @param int $error_code 56 | * 57 | * @param array|null $data 58 | * 59 | * @return false|string 60 | */ 61 | function error(string $msg, int $error_code = 1, array $data = null): string 62 | { 63 | return json($msg, $error_code, $data); 64 | } 65 | } 66 | 67 | if (!function_exists('json')) { 68 | /** 69 | * @param string $msg 70 | * @param int $code 71 | * @param array|null $data 72 | * 73 | * @return string 74 | */ 75 | function json(string $msg, int $code = 0, array $data = null): string 76 | { 77 | $succeedData = [ 78 | 'msg' => $msg, 79 | 'code' => $code, 80 | 'data' => $data 81 | ]; 82 | 83 | return json_encode($succeedData,256); 84 | } 85 | } 86 | 87 | if (!function_exists('data')) { 88 | /** 89 | * @param string $msg 90 | * @param int $code 91 | * @param array|null $data 92 | * 93 | * @return array 94 | */ 95 | function data(string $msg, int $code = 0, array $data = null) 96 | { 97 | return [ 98 | 'msg' => $msg, 99 | 'code' => $code, 100 | 'data' => $data 101 | ]; 102 | } 103 | } 104 | 105 | if(!function_exists('timestamp')) { 106 | /** 107 | * @return float 108 | */ 109 | function timestamp() 110 | { 111 | [$msec, $sec] = explode(' ', microtime()); 112 | 113 | return (float) sprintf('%.0f', (floatval($msec) + floatval($sec)) * 1000); 114 | } 115 | } 116 | 117 | if (!function_exists('uuid')) { 118 | /** 119 | * @return string 120 | */ 121 | function uuid() 122 | { 123 | return \Ramsey\Uuid\v4(); 124 | } 125 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Hyperf Logo

2 | 3 |

4 | Build Status 5 | Financial Contributors on Open Collective 6 | Php Version 7 | Swoole Version 8 | Hyperf License 9 |

10 | 11 | # 介绍 12 | 13 | Hyperf 是基于 `Swoole 4.4+` 实现的高性能、高灵活性的 PHP 协程框架,内置协程服务器及大量常用的组件,性能较传统基于 `PHP-FPM` 的框架有质的提升,提供超高性能的同时,也保持着极其灵活的可扩展性,标准组件均基于 [PSR 标准](https://www.php-fig.org/psr) 实现,基于强大的依赖注入设计,保证了绝大部分组件或类都是 `可替换` 与 `可复用` 的。 14 | 15 | 框架组件库除了常见的协程版的 `MySQL 客户端`、`Redis 客户端`,还为您准备了协程版的 `Eloquent ORM`、`WebSocket 服务端及客户端`、`JSON RPC 服务端及客户端`、`GRPC 服务端及客户端`、`Zipkin/Jaeger (OpenTracing) 客户端`、`Guzzle HTTP 客户端`、`Elasticsearch 客户端`、`Consul 客户端`、`ETCD 客户端`、`AMQP 组件`、`NSQ 组件`、`Nats 组件`、`Apollo 配置中心`、`阿里云 ACM 应用配置管理`、`ETCD 配置中心`、`基于令牌桶算法的限流器`、`通用连接池`、`熔断器`、`Swagger 文档生成`、`Swoole Tracker`、`视图引擎`、`Snowflake 全局 ID 生成器` 等组件,省去了自己实现对应协程版本的麻烦。 16 | 17 | Hyperf 还提供了 `基于 PSR-11 的依赖注入容器`、`注解`、`AOP 面向切面编程`、`基于 PSR-15 的中间件`、`自定义进程`、`基于 PSR-14 的事件管理器`、`Redis/RabbitMQ/NSQ/Nats 消息队列`、`自动模型缓存`、`基于 PSR-16 的缓存`、`Crontab 秒级定时任务`、`Translation 国际化`、`Validation 验证器` 等非常便捷的功能,满足丰富的技术场景和业务场景,开箱即用。 18 | 19 | # 框架初衷 20 | 21 | 尽管现在基于 PHP 语言开发的框架处于一个百家争鸣的时代,但仍旧未能看到一个优雅的设计与超高性能的共存的完美框架,亦没有看到一个真正为 PHP 微服务铺路的框架,此为 Hyperf 及其团队成员的初衷,我们将持续投入并为此付出努力,也欢迎你加入我们参与开源建设。 22 | 23 | # 设计理念 24 | 25 | `Hyperspeed + Flexibility = Hyperf`,从名字上我们就将 `超高速` 和 `灵活性` 作为 Hyperf 的基因。 26 | 27 | - 对于超高速,我们基于 Swoole 协程并在框架设计上进行大量的优化以确保超高性能的输出。 28 | - 对于灵活性,我们基于 Hyperf 强大的依赖注入组件,组件均基于 [PSR 标准](https://www.php-fig.org/psr) 的契约和由 Hyperf 定义的契约实现,达到框架内的绝大部分的组件或类都是可替换的。 29 | 30 | 基于以上的特点,Hyperf 将存在丰富的可能性,如实现 Web 服务,网关服务,分布式中间件,微服务架构,游戏服务器,物联网(IOT)等。 31 | 32 | # 生产可用 33 | 34 | 我们为组件进行了大量的单元测试以保证逻辑的正确,目前存在 `1120` 个单测共 `3369` 个断言条件,同时维护了高质量的文档,在 Hyperf 正式对外开放(2019年6月20日)之前,便已经过了严酷的生产环境的考验,我们才正式的对外开放该项目,现在已有很多的大型互联网企业将 Hyperf 部署到了自己的生产环境上并稳定运行。 35 | 36 | # 运行环境 37 | 38 | - Linux, OS X or Cygwin, WSL 39 | - PHP 7.2+ 40 | - Swoole 4.4+ 41 | 42 | # 安全漏洞 43 | 44 | 如果您发现 Hyperf 中存在安全漏洞,请发送电子邮件至 Hyperf 官方团队,电子邮件地址为 group@hyperf.io ,所有安全漏洞都会被及时的解决。 45 | 46 | # 官网及文档 47 | 48 | 官网 [https://hyperf.io](https://hyperf.io) 49 | 文档 [https://hyperf.wiki](https://hyperf.wiki) 50 | 51 | # 代码贡献者 52 | 53 | 感谢所有参与 Hyperf 开发的代码贡献者。 [[contributors](https://github.com/hyperf/hyperf/graphs/contributors)] 54 | 55 | 56 | # 性能 57 | 58 | ### 阿里云 8 核 16G 59 | 命令: `wrk -c 1024 -t 8 http://127.0.0.1:9501/` 60 | ```bash 61 | Running 10s test @ http://127.0.0.1:9501/ 62 | 8 threads and 1024 connections 63 | Thread Stats Avg Stdev Max +/- Stdev 64 | Latency 10.08ms 6.82ms 56.66ms 70.19% 65 | Req/Sec 13.17k 5.94k 33.06k 84.12% 66 | 1049478 requests in 10.10s, 190.16MB read 67 | Requests/sec: 103921.49 68 | Transfer/sec: 18.83MB 69 | ``` 70 | 71 | # 开源协议 72 | 73 | Hyperf 是一个基于 [MIT 协议](https://github.com/hyperf/hyperf/blob/master/LICENSE) 开源的软件。 -------------------------------------------------------------------------------- /hyperf_im.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat Premium Data Transfer 3 | 4 | Source Server : 5 | Source Server Type : MySQL 6 | Source Server Version : 50729 7 | Source Host : 8 | Source Schema : hyperf_im 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 50729 12 | File Encoding : 65001 13 | 14 | Date: 17/07/2020 10:49:52 15 | */ 16 | 17 | SET NAMES utf8mb4; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for app_friend 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `app_friend`; 24 | CREATE TABLE `app_friend` ( 25 | `id` int(11) NOT NULL AUTO_INCREMENT, 26 | `uid` int(11) NULL DEFAULT NULL COMMENT '用户id', 27 | `friend_id` int(11) NULL DEFAULT NULL COMMENT '好友id', 28 | `friend_group_id` int(11) NULL DEFAULT NULL COMMENT '分组id', 29 | `created_at` int(11) NULL DEFAULT NULL, 30 | `updated_at` int(11) NULL DEFAULT NULL, 31 | `deleted_at` int(11) NULL DEFAULT NULL, 32 | PRIMARY KEY (`id`) USING BTREE 33 | ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic; 34 | 35 | -- ---------------------------- 36 | -- Table structure for app_friend_chat_log 37 | -- ---------------------------- 38 | DROP TABLE IF EXISTS `app_friend_chat_log`; 39 | CREATE TABLE `app_friend_chat_log` ( 40 | `id` int(11) NOT NULL AUTO_INCREMENT, 41 | `from_uid` int(11) NULL DEFAULT NULL COMMENT '来自ID', 42 | `target_uid` int(11) NULL DEFAULT NULL COMMENT '目标ID', 43 | `msg_id` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '消息ID', 44 | `msg_time` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '消息时间', 45 | `msg_type` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '消息类型', 46 | `msg_body` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '消息内容', 47 | `created_at` int(11) NULL DEFAULT NULL, 48 | `updated_at` int(11) NULL DEFAULT NULL, 49 | `deleted_at` int(11) NULL DEFAULT NULL, 50 | PRIMARY KEY (`id`) USING BTREE 51 | ) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; 52 | 53 | -- ---------------------------- 54 | -- Table structure for app_friend_group 55 | -- ---------------------------- 56 | DROP TABLE IF EXISTS `app_friend_group`; 57 | CREATE TABLE `app_friend_group` ( 58 | `id` int(11) NOT NULL AUTO_INCREMENT, 59 | `uid` int(11) NULL DEFAULT NULL COMMENT '用户id', 60 | `name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '分组名', 61 | `type` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '类型', 62 | `created_at` int(11) NULL DEFAULT NULL, 63 | `updated_at` int(11) NULL DEFAULT NULL, 64 | `deleted_at` int(11) NULL DEFAULT NULL, 65 | PRIMARY KEY (`id`) USING BTREE 66 | ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic; 67 | 68 | -- ---------------------------- 69 | -- Table structure for app_group 70 | -- ---------------------------- 71 | DROP TABLE IF EXISTS `app_group`; 72 | CREATE TABLE `app_group` ( 73 | `id` int(11) NOT NULL AUTO_INCREMENT, 74 | `uid` int(11) NULL DEFAULT NULL COMMENT '所有者id', 75 | `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '群昵称', 76 | `cover` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '封面', 77 | `notice` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '公告', 78 | `validation` tinyint(2) NULL DEFAULT 1 COMMENT '加入是否需要验证', 79 | `size` enum('100','500','1000') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '100' COMMENT '群容量', 80 | `desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '描述详情', 81 | `created_at` int(11) NULL DEFAULT NULL, 82 | `updated_at` int(11) NULL DEFAULT NULL, 83 | `deleted_at` int(11) NULL DEFAULT NULL, 84 | PRIMARY KEY (`id`) USING BTREE 85 | ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic; 86 | 87 | -- ---------------------------- 88 | -- Table structure for app_user_in_group 89 | -- ---------------------------- 90 | DROP TABLE IF EXISTS `app_user_in_group`; 91 | CREATE TABLE `app_user_in_group` ( 92 | `id` int(11) NOT NULL AUTO_INCREMENT, 93 | `uid` int(11) NULL DEFAULT NULL, 94 | `group_id` int(11) NULL DEFAULT NULL, 95 | `created_at` int(11) NULL DEFAULT NULL, 96 | `updated_at` int(11) NULL DEFAULT NULL, 97 | `deleted_at` int(11) NULL DEFAULT NULL, 98 | PRIMARY KEY (`id`) USING BTREE 99 | ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic; 100 | 101 | -- ---------------------------- 102 | -- Table structure for app_users 103 | -- ---------------------------- 104 | DROP TABLE IF EXISTS `app_users`; 105 | CREATE TABLE `app_users` ( 106 | `id` int(11) NOT NULL AUTO_INCREMENT, 107 | `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '姓名', 108 | `pwd` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '密码', 109 | `code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'im号', 110 | `sex` int(2) NULL DEFAULT NULL COMMENT '性别', 111 | `addr` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '地区', 112 | `intro` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '个性签名', 113 | `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '手机', 114 | `cover` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '头像', 115 | `status` int(5) NULL DEFAULT 0 COMMENT '状态', 116 | `created_at` int(11) NULL DEFAULT NULL, 117 | `updated_at` int(11) NULL DEFAULT NULL, 118 | `deleted_at` int(11) NULL DEFAULT NULL, 119 | PRIMARY KEY (`id`) USING BTREE 120 | ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic; 121 | 122 | SET FOREIGN_KEY_CHECKS = 1; 123 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------