├── 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 | 
2 |
3 |
4 |
5 |
6 |
7 |
8 |
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 |
--------------------------------------------------------------------------------