├── config ├── autoload │ ├── translation.php │ ├── aspects.php │ ├── dependencies.php │ ├── listeners.php │ ├── commands.php │ ├── crontab.php │ ├── processes.php │ ├── middlewares.php │ ├── annotations.php │ ├── cache.php │ ├── watcher.php │ ├── exceptions.php │ ├── view.php │ ├── redis.php │ ├── logger.php │ ├── devtool.php │ ├── databases.php │ ├── server.php │ └── mail.php ├── container.php ├── config.php └── routes.php ├── .gitignore ├── storage ├── languages │ ├── en │ │ ├── messages.php │ │ └── validation.php │ └── zh_CN │ │ └── validation.php └── view │ └── emails │ └── supplies │ └── expiring.blade.php ├── .phpstorm.meta.php ├── app ├── Exception │ ├── WelcomeException.php │ └── Handler │ │ ├── ValidationExceptionHandler.php │ │ └── AppExceptionHandler.php ├── Service │ ├── RestaurantService.php │ ├── SupplyService.php │ ├── WelcomeService.php │ └── MailService.php ├── Model │ ├── Model.php │ ├── Supply.php │ └── Restaurant.php ├── ValueObject │ ├── Name.php │ └── Age.php ├── Controller │ ├── IndexController.php │ ├── RestaurantController.php │ ├── AbstractController.php │ ├── WelcomeController.php │ └── SupplyController.php ├── Request │ ├── RestaurantRequest.php │ └── SupplyRequest.php ├── Command │ └── MailExpiringCommand.php ├── Mail │ └── ExpiringSupplies.php └── Listener │ └── DbQueryExecutedListener.php ├── .github └── workflows │ ├── build.yml │ ├── release.yml │ └── Dockerfile ├── .env.example ├── phpstan.neon ├── test ├── Cases │ ├── ExampleTest.php │ ├── WelcomeServiceTest.php │ └── RestaurantControllerTest.php ├── bootstrap.php └── HttpTestCase.php ├── deploy.test.yml ├── docker-compose.yml ├── phpunit.xml ├── migrations ├── 2021_04_21_112716_create_restaurants_table.php └── 2021_04_21_135556_create_supplies_table.php ├── bin └── hyperf.php ├── .gitlab-ci.yml ├── Dockerfile ├── README.md ├── .php_cs └── composer.json /config/autoload/translation.php: -------------------------------------------------------------------------------- 1 | 'en', 5 | ]; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .buildpath 2 | .settings/ 3 | .project 4 | *.patch 5 | .idea/ 6 | .git/ 7 | runtime/ 8 | vendor/ 9 | .phpintel/ 10 | .env 11 | .DS_Store 12 | .phpunit* 13 | *.cache 14 | -------------------------------------------------------------------------------- /storage/languages/en/messages.php: -------------------------------------------------------------------------------- 1 | ':resource :id not found', 5 | 'expiring_supplies' => 'Supplies about to expire', 6 | ]; -------------------------------------------------------------------------------- /.phpstorm.meta.php: -------------------------------------------------------------------------------- 1 | Supplies that are about to expire 2 |
3 | @foreach($supplies as $supply) 4 |
{{ $supply->description }}
5 |
6 | Expiration date: {{ $supply->expires_at }}
7 | Responsible: {{ $supply->responsible }} 8 |
9 | @endforeach 10 |
-------------------------------------------------------------------------------- /config/autoload/commands.php: -------------------------------------------------------------------------------- 1 | setName('Expiring supplies') 5 | ->setRule('* * * * *') // Every minute 6 | ->setType('command') 7 | ->setCallback(['command' => 'mail:expiring']); 8 | 9 | return [ 10 | 'enable' => true, 11 | 'crontab' => [$expiring_supplies], 12 | ]; 13 | -------------------------------------------------------------------------------- /config/autoload/processes.php: -------------------------------------------------------------------------------- 1 | [ 14 | \Hyperf\Validation\Middleware\ValidationMiddleware::class, 15 | ], 16 | ]; 17 | -------------------------------------------------------------------------------- /app/Service/RestaurantService.php: -------------------------------------------------------------------------------- 1 | save(); 15 | return $restaurant; 16 | } 17 | } -------------------------------------------------------------------------------- /app/Model/Model.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'paths' => [ 15 | BASE_PATH . '/app', 16 | ], 17 | 'ignore_annotations' => [ 18 | 'mixin', 19 | ], 20 | ], 21 | ]; 22 | -------------------------------------------------------------------------------- /app/Service/SupplyService.php: -------------------------------------------------------------------------------- 1 | save(); 15 | return $supply; 16 | } 17 | 18 | public function find(string $id): ?Supply 19 | { 20 | return Supply::find($id); 21 | } 22 | } -------------------------------------------------------------------------------- /config/autoload/cache.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'driver' => Hyperf\Cache\Driver\RedisDriver::class, 15 | 'packer' => Hyperf\Utils\Packer\PhpSerializerPacker::class, 16 | 'prefix' => 'c:', 17 | ], 18 | ]; 19 | -------------------------------------------------------------------------------- /config/autoload/watcher.php: -------------------------------------------------------------------------------- 1 | ScanFileDriver::class, 16 | 'bin' => 'php', 17 | 'watch' => [ 18 | 'dir' => ['app', 'config'], 19 | 'file' => ['.env'], 20 | 'scan_interval' => 2000, 21 | ], 22 | ]; 23 | -------------------------------------------------------------------------------- /app/ValueObject/Name.php: -------------------------------------------------------------------------------- 1 | value = $value; 17 | } 18 | 19 | public function value(): string 20 | { 21 | return $this->value; 22 | } 23 | 24 | public function __toString() 25 | { 26 | return $this->value(); 27 | } 28 | } -------------------------------------------------------------------------------- /config/autoload/exceptions.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'http' => [ 15 | Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler::class, 16 | App\Exception\Handler\AppExceptionHandler::class, 17 | \App\Exception\Handler\ValidationExceptionHandler::class, 18 | ], 19 | ], 20 | ]; 21 | -------------------------------------------------------------------------------- /config/autoload/view.php: -------------------------------------------------------------------------------- 1 | \Hyperf\View\Engine\BladeEngine::class, 17 | 'mode' => Mode::SYNC, 18 | 'config' => [ 19 | 'view_path' => BASE_PATH . '/storage/view/', 20 | 'cache_path' => BASE_PATH . '/runtime/view/', 21 | ], 22 | ]; 23 | -------------------------------------------------------------------------------- /test/Cases/ExampleTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 25 | $this->assertTrue(is_array($this->get('/'))); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Controller/IndexController.php: -------------------------------------------------------------------------------- 1 | request->input('user', 'Hyperf'); 19 | $method = $this->request->getMethod(); 20 | 21 | return [ 22 | 'method' => $method, 23 | 'message' => "Hello {$user}.", 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Request/RestaurantRequest.php: -------------------------------------------------------------------------------- 1 | 'required|email|unique:restaurants,email', 26 | 'name' => 'required|min:3' 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 6 | 7 | name: Release 8 | 9 | jobs: 10 | release: 11 | name: Release 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | - name: Create Release 17 | id: create_release 18 | uses: actions/create-release@v1 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | with: 22 | tag_name: ${{ github.ref }} 23 | release_name: Release ${{ github.ref }} 24 | draft: false 25 | prerelease: false 26 | -------------------------------------------------------------------------------- /app/ValueObject/Age.php: -------------------------------------------------------------------------------- 1 | value = $value; 19 | } 20 | 21 | public function underage(): bool 22 | { 23 | return $this->value < self::LEGAL_AGE; 24 | } 25 | 26 | public function value(): int 27 | { 28 | return $this->value; 29 | } 30 | 31 | public function __toString() 32 | { 33 | return (string) $this->value(); 34 | } 35 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | services: 3 | db: 4 | container_name: inmana_db 5 | image: mysql:8 6 | command: --default-authentication-plugin=mysql_native_password 7 | environment: 8 | MYSQL_ROOT_PASSWORD: inmana 9 | MYSQL_DATABASE: ${DB_DATABASE} 10 | MYSQL_USER: ${DB_USERNAME} 11 | MYSQL_PASSWORD: ${DB_PASSWORD} 12 | ports: 13 | - 3306:3306 14 | 15 | app: 16 | container_name: inmana 17 | image: hyperf/hyperf:7.4-alpine-v3.11-swoole 18 | entrypoint: /bin/sh 19 | working_dir: /app 20 | environment: 21 | TIMEZONE: America/Sao_Paulo 22 | env_file: 23 | - .env 24 | volumes: 25 | - ./:/app 26 | ports: 27 | - 9501:9501 28 | links: 29 | - db -------------------------------------------------------------------------------- /app/Request/SupplyRequest.php: -------------------------------------------------------------------------------- 1 | 'required', 26 | 'expires_at' => 'required|date', 27 | 'responsible' => 'required|min:3', 28 | 'restaurant_id' => 'required|uuid' 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./test 14 | 15 | 16 | 17 | 18 | ./app 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /migrations/2021_04_21_112716_create_restaurants_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 16 | $table->string('email')->unique(); 17 | $table->string('name'); 18 | $table->timestamps(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | */ 25 | public function down(): void 26 | { 27 | Schema::dropIfExists('restaurants'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /bin/hyperf.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | get(Hyperf\Contract\ApplicationInterface::class); 22 | $application->run(); 23 | })(); 24 | -------------------------------------------------------------------------------- /config/container.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'host' => env('REDIS_HOST', 'localhost'), 15 | 'auth' => env('REDIS_AUTH', null), 16 | 'port' => (int) env('REDIS_PORT', 6379), 17 | 'db' => (int) env('REDIS_DB', 0), 18 | 'pool' => [ 19 | 'min_connections' => 1, 20 | 'max_connections' => 10, 21 | 'connect_timeout' => 10.0, 22 | 'wait_timeout' => 3.0, 23 | 'heartbeat' => -1, 24 | 'max_idle_time' => (float) env('REDIS_MAX_IDLE_TIME', 60), 25 | ], 26 | ], 27 | ]; 28 | -------------------------------------------------------------------------------- /app/Controller/RestaurantController.php: -------------------------------------------------------------------------------- 1 | restaurantService = $restaurantService; 18 | } 19 | 20 | public function create(RestaurantRequest $request, ResponseInterface $response): \Psr\Http\Message\ResponseInterface 21 | { 22 | $restaurant = $this->restaurantService->create($request->validated()); 23 | return $response->json($restaurant)->withStatus(Status::CREATED); 24 | } 25 | } -------------------------------------------------------------------------------- /test/bootstrap.php: -------------------------------------------------------------------------------- 1 | get(Hyperf\Contract\ApplicationInterface::class); 30 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | env('APP_NAME', 'skeleton'), 17 | 'app_env' => env('APP_ENV', 'dev'), 18 | 'scan_cacheable' => env('SCAN_CACHEABLE', false), 19 | StdoutLoggerInterface::class => [ 20 | 'log_level' => [ 21 | LogLevel::ALERT, 22 | LogLevel::CRITICAL, 23 | LogLevel::DEBUG, 24 | LogLevel::EMERGENCY, 25 | LogLevel::ERROR, 26 | LogLevel::INFO, 27 | LogLevel::NOTICE, 28 | LogLevel::WARNING, 29 | ], 30 | ], 31 | ]; 32 | -------------------------------------------------------------------------------- /app/Controller/AbstractController.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'handler' => [ 15 | 'class' => Monolog\Handler\StreamHandler::class, 16 | 'constructor' => [ 17 | 'stream' => BASE_PATH . '/runtime/logs/hyperf.log', 18 | 'level' => Monolog\Logger::DEBUG, 19 | ], 20 | ], 21 | 'formatter' => [ 22 | 'class' => Monolog\Formatter\LineFormatter::class, 23 | 'constructor' => [ 24 | 'format' => null, 25 | 'dateFormat' => 'Y-m-d H:i:s', 26 | 'allowInlineLineBreaks' => true, 27 | ], 28 | ], 29 | ], 30 | ]; 31 | -------------------------------------------------------------------------------- /app/Service/WelcomeService.php: -------------------------------------------------------------------------------- 1 | special($name, $age)) { 23 | return 'You are special'; 24 | } 25 | 26 | if (!$age->underage()) { 27 | return 'You are ok'; 28 | } 29 | 30 | throw WelcomeException::underage(); 31 | } 32 | 33 | private function special(Name $name, Age $age): bool 34 | { 35 | return $name->value() === self::SPECIAL_NAME && $age->value() === self::SPECIAL_AGE; 36 | } 37 | } -------------------------------------------------------------------------------- /app/Command/MailExpiringCommand.php: -------------------------------------------------------------------------------- 1 | container = $container; 22 | parent::__construct('mail:expiring'); 23 | } 24 | 25 | public function configure() 26 | { 27 | parent::configure(); 28 | $this->setDescription('Mail restaurants with expiring supplies'); 29 | } 30 | 31 | public function handle(): void 32 | { 33 | $this->output->info('Mailing expired supplies'); 34 | $this->container->get(MailService::class)->expiring(); 35 | $this->output->success('Done'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/Cases/WelcomeServiceTest.php: -------------------------------------------------------------------------------- 1 | welcome(['name' => 'banana', 'age' => '42']); 15 | 16 | self::assertSame('You are special', $actual); 17 | } 18 | 19 | public function testRegularWelcome() 20 | { 21 | $svc = new WelcomeService(); 22 | $actual = $svc->welcome(['name' => 'Leo', 'age' => '42']); 23 | 24 | self::assertSame('You are ok', $actual); 25 | } 26 | 27 | public function testUnderageWelcome() 28 | { 29 | $this->expectException(WelcomeException::class); 30 | $this->expectExceptionMessage('You shall not pass'); 31 | 32 | $svc = new WelcomeService(); 33 | $actual = $svc->welcome(['name' => 'Leo', 'age' => '17']); 34 | } 35 | } -------------------------------------------------------------------------------- /app/Service/MailService.php: -------------------------------------------------------------------------------- 1 | count()], 17 | ]; 18 | 19 | /** @var Restaurant $restaurant */ 20 | foreach ($restaurants as $restaurant) { 21 | Coroutine::create(static function() use ($restaurant, &$results): void { 22 | $supplies = $restaurant->expiringSupplies(); 23 | 24 | if ($supplies->isNotEmpty()) { 25 | $results[] = [$restaurant->email, Mail::to($restaurant->email)->send(new ExpiringSupplies($supplies))]; 26 | } else { 27 | $results[] = [$restaurant->email, 'Without expiring']; 28 | } 29 | }); 30 | } 31 | 32 | return $results; 33 | } 34 | } -------------------------------------------------------------------------------- /app/Mail/ExpiringSupplies.php: -------------------------------------------------------------------------------- 1 | */ 20 | private Collection $supplies; 21 | 22 | /** 23 | * Create a new message instance. 24 | * @param Collection $supplies 25 | */ 26 | public function __construct(Collection $supplies) 27 | { 28 | $this->supplies = $supplies; 29 | } 30 | 31 | /** 32 | * Build the message. 33 | */ 34 | public function build(): void 35 | { 36 | $this->subject(trans('messages.expiring_supplies')); 37 | $this->htmlView('emails.supplies.expiring') 38 | ->with('supplies', $this->supplies); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /migrations/2021_04_21_135556_create_supplies_table.php: -------------------------------------------------------------------------------- 1 | uuid('id')->primary(); 16 | $table->uuid('restaurant_id'); 17 | $table->text('description'); 18 | $table->date('expires_at'); 19 | $table->string('responsible'); 20 | $table->timestamps(); 21 | 22 | $table 23 | ->foreign('restaurant_id') 24 | ->references('id') 25 | ->on('restaurants') 26 | ->onUpdate('cascade') 27 | ->onDelete('cascade'); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | */ 34 | public function down(): void 35 | { 36 | Schema::dropIfExists('supplies'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Controller/WelcomeController.php: -------------------------------------------------------------------------------- 1 | welcomeService = $welcomeService; 19 | } 20 | 21 | public function index(RequestInterface $request, ResponseInterface $response): \Psr\Http\Message\ResponseInterface 22 | { 23 | try { 24 | $message = $this->welcomeService->welcome($request->getQueryParams()); 25 | return $response->json(['error' => false, 'message' => $message]); 26 | } catch (WelcomeException $err) { 27 | return $response->json(['error' => true, 'message' => $err->getMessage()]) 28 | ->withStatus(Status::BAD_REQUEST); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /test/HttpTestCase.php: -------------------------------------------------------------------------------- 1 | client = make(Client::class); 36 | } 37 | 38 | public function __call($name, $arguments) 39 | { 40 | return $this->client->{$name}(...$arguments); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /config/autoload/devtool.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'amqp' => [ 15 | 'consumer' => [ 16 | 'namespace' => 'App\\Amqp\\Consumer', 17 | ], 18 | 'producer' => [ 19 | 'namespace' => 'App\\Amqp\\Producer', 20 | ], 21 | ], 22 | 'aspect' => [ 23 | 'namespace' => 'App\\Aspect', 24 | ], 25 | 'command' => [ 26 | 'namespace' => 'App\\Command', 27 | ], 28 | 'controller' => [ 29 | 'namespace' => 'App\\Controller', 30 | ], 31 | 'job' => [ 32 | 'namespace' => 'App\\Job', 33 | ], 34 | 'listener' => [ 35 | 'namespace' => 'App\\Listener', 36 | ], 37 | 'middleware' => [ 38 | 'namespace' => 'App\\Middleware', 39 | ], 40 | 'Process' => [ 41 | 'namespace' => 'App\\Processes', 42 | ], 43 | ], 44 | ]; 45 | -------------------------------------------------------------------------------- /app/Exception/Handler/ValidationExceptionHandler.php: -------------------------------------------------------------------------------- 1 | stopPropagation(); 20 | /** @var ValidationException $throwable */ 21 | $body = $throwable->validator->errors()->first(); 22 | if (!$response->hasHeader('content-type')) { 23 | $response = $response->withAddedHeader('content-type', 'application/json; charset=utf-8'); 24 | } 25 | return $response->withStatus($throwable->status)->withBody(new SwooleStream(json_encode([ 26 | 'error' => true, 27 | 'message' => $body, 28 | ], JSON_THROW_ON_ERROR))); 29 | } 30 | 31 | public function isValid(Throwable $throwable): bool 32 | { 33 | return $throwable instanceof ValidationException; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Model/Supply.php: -------------------------------------------------------------------------------- 1 | 'datetime', 'updated_at' => 'datetime', 'expires_at' => 'date']; 40 | 41 | public function restaurant(): BelongsTo 42 | { 43 | return $this->belongsTo(Restaurant::class, 'restaurant_id', 'id'); 44 | } 45 | } -------------------------------------------------------------------------------- /config/autoload/databases.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'driver' => env('DB_DRIVER', 'mysql'), 15 | 'host' => env('DB_HOST', 'localhost'), 16 | 'database' => env('DB_DATABASE', 'hyperf'), 17 | 'port' => env('DB_PORT', 3306), 18 | 'username' => env('DB_USERNAME', 'root'), 19 | 'password' => env('DB_PASSWORD', ''), 20 | 'charset' => env('DB_CHARSET', 'utf8'), 21 | 'collation' => env('DB_COLLATION', 'utf8_unicode_ci'), 22 | 'prefix' => env('DB_PREFIX', ''), 23 | 'pool' => [ 24 | 'min_connections' => 1, 25 | 'max_connections' => 10, 26 | 'connect_timeout' => 10.0, 27 | 'wait_timeout' => 3.0, 28 | 'heartbeat' => -1, 29 | 'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60), 30 | ], 31 | 'commands' => [ 32 | 'gen:model' => [ 33 | 'path' => 'app/Model', 34 | 'force_casts' => true, 35 | 'inheritance' => 'Model', 36 | ], 37 | ], 38 | ], 39 | ]; 40 | -------------------------------------------------------------------------------- /app/Exception/Handler/AppExceptionHandler.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 30 | } 31 | 32 | public function handle(Throwable $throwable, ResponseInterface $response) 33 | { 34 | $this->logger->error(sprintf('%s[%s] in %s', $throwable->getMessage(), $throwable->getLine(), $throwable->getFile())); 35 | $this->logger->error($throwable->getTraceAsString()); 36 | return $response->withHeader('Server', 'Hyperf')->withStatus(500)->withBody(new SwooleStream('Internal Server Error.')); 37 | } 38 | 39 | public function isValid(Throwable $throwable): bool 40 | { 41 | return true; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/Controller/SupplyController.php: -------------------------------------------------------------------------------- 1 | supplyService = $supplyService; 18 | } 19 | 20 | public function create(SupplyRequest $request, ResponseInterface $response): \Psr\Http\Message\ResponseInterface 21 | { 22 | $supply = $this->supplyService->create($request->validated()); 23 | return $response->json($supply)->withStatus(Status::CREATED); 24 | } 25 | 26 | public function show(string $id, ResponseInterface $response): \Psr\Http\Message\ResponseInterface 27 | { 28 | $supply = $this->supplyService->find($id); 29 | 30 | if ($supply === null) { 31 | return $response 32 | ->json(['error' => true, 'message' => trans('messages.not_found', [ 33 | 'resource' => 'Supply', 34 | 'id' => $id, 35 | ]) 36 | ]) 37 | ->withStatus(Status::NOT_FOUND); 38 | } 39 | 40 | return $response->json($supply); 41 | } 42 | } -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # usermod -aG docker gitlab-runner 2 | 3 | stages: 4 | - build 5 | - deploy 6 | 7 | variables: 8 | PROJECT_NAME: hyperf 9 | REGISTRY_URL: registry-docker.org 10 | 11 | build_test_docker: 12 | stage: build 13 | before_script: 14 | # - git submodule sync --recursive 15 | # - git submodule update --init --recursive 16 | script: 17 | - docker build . -t $PROJECT_NAME 18 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:test 19 | - docker push $REGISTRY_URL/$PROJECT_NAME:test 20 | only: 21 | - test 22 | tags: 23 | - builder 24 | 25 | deploy_test_docker: 26 | stage: deploy 27 | script: 28 | - docker stack deploy -c deploy.test.yml --with-registry-auth $PROJECT_NAME 29 | only: 30 | - test 31 | tags: 32 | - test 33 | 34 | build_docker: 35 | stage: build 36 | before_script: 37 | # - git submodule sync --recursive 38 | # - git submodule update --init --recursive 39 | script: 40 | - docker build . -t $PROJECT_NAME 41 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:$CI_COMMIT_REF_NAME 42 | - docker tag $PROJECT_NAME $REGISTRY_URL/$PROJECT_NAME:latest 43 | - docker push $REGISTRY_URL/$PROJECT_NAME:$CI_COMMIT_REF_NAME 44 | - docker push $REGISTRY_URL/$PROJECT_NAME:latest 45 | only: 46 | - tags 47 | tags: 48 | - builder 49 | 50 | deploy_docker: 51 | stage: deploy 52 | script: 53 | - echo SUCCESS 54 | only: 55 | - tags 56 | tags: 57 | - builder 58 | -------------------------------------------------------------------------------- /app/Model/Restaurant.php: -------------------------------------------------------------------------------- 1 | 'datetime', 'updated_at' => 'datetime']; 40 | 41 | public function supplies(): HasMany 42 | { 43 | return $this->hasMany(Supply::class, 'restaurant_id', 'id'); 44 | } 45 | 46 | /** 47 | * @return Collection 48 | */ 49 | public function expiringSupplies(): Collection 50 | { 51 | $today = CarbonImmutable::now(); 52 | return $this->supplies()->whereBetween('expires_at', [$today->startOfWeek(), $today->endOfWeek()]) 53 | ->orderBy('expires_at') 54 | ->get(); 55 | } 56 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Default Dockerfile 2 | # 3 | # @link https://www.hyperf.io 4 | # @document https://hyperf.wiki 5 | # @contact group@hyperf.io 6 | # @license https://github.com/hyperf/hyperf/blob/master/LICENSE 7 | 8 | FROM hyperf/hyperf:7.4-alpine-v3.11-swoole 9 | LABEL maintainer="Hyperf Developers " version="1.0" license="MIT" app.name="Hyperf" 10 | 11 | ## 12 | # ---------- env settings ---------- 13 | ## 14 | # --build-arg timezone=Asia/Shanghai 15 | ARG timezone 16 | 17 | ENV TIMEZONE=${timezone:-"Asia/Shanghai"} \ 18 | APP_ENV=prod \ 19 | SCAN_CACHEABLE=(true) 20 | 21 | # update 22 | RUN set -ex \ 23 | # show php version and extensions 24 | && php -v \ 25 | && php -m \ 26 | && php --ri swoole \ 27 | # ---------- some config ---------- 28 | && cd /etc/php7 \ 29 | # - config PHP 30 | && { \ 31 | echo "upload_max_filesize=128M"; \ 32 | echo "post_max_size=128M"; \ 33 | echo "memory_limit=1G"; \ 34 | echo "date.timezone=${TIMEZONE}"; \ 35 | } | tee conf.d/99_overrides.ini \ 36 | # - config timezone 37 | && ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \ 38 | && echo "${TIMEZONE}" > /etc/timezone \ 39 | # ---------- clear works ---------- 40 | && rm -rf /var/cache/apk/* /tmp/* /usr/share/man \ 41 | && echo -e "\033[42;37m Build Completed :).\033[0m\n" 42 | 43 | WORKDIR /opt/www 44 | 45 | # Composer Cache 46 | # COPY ./composer.* /opt/www/ 47 | # RUN composer install --no-dev --no-scripts 48 | 49 | COPY . /opt/www 50 | RUN composer install --no-dev -o && php bin/hyperf.php 51 | 52 | EXPOSE 9501 53 | 54 | ENTRYPOINT ["php", "/opt/www/bin/hyperf.php", "start"] 55 | -------------------------------------------------------------------------------- /.github/workflows/Dockerfile: -------------------------------------------------------------------------------- 1 | # Default Dockerfile 2 | # 3 | # @link https://www.hyperf.io 4 | # @document https://hyperf.wiki 5 | # @contact group@hyperf.io 6 | # @license https://github.com/hyperf/hyperf/blob/master/LICENSE 7 | 8 | FROM hyperf/hyperf:7.4-alpine-v3.11-swoole 9 | LABEL maintainer="Hyperf Developers " version="1.0" license="MIT" app.name="Hyperf" 10 | 11 | ## 12 | # ---------- env settings ---------- 13 | ## 14 | # --build-arg timezone=Asia/Shanghai 15 | ARG timezone 16 | 17 | ENV TIMEZONE=${timezone:-"Asia/Shanghai"} \ 18 | APP_ENV=prod \ 19 | SCAN_CACHEABLE=(true) 20 | 21 | # update 22 | RUN set -ex \ 23 | # show php version and extensions 24 | && php -v \ 25 | && php -m \ 26 | && php --ri swoole \ 27 | # ---------- some config ---------- 28 | && cd /etc/php7 \ 29 | # - config PHP 30 | && { \ 31 | echo "upload_max_filesize=128M"; \ 32 | echo "post_max_size=128M"; \ 33 | echo "memory_limit=1G"; \ 34 | echo "date.timezone=${TIMEZONE}"; \ 35 | } | tee conf.d/99_overrides.ini \ 36 | # - config timezone 37 | && ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \ 38 | && echo "${TIMEZONE}" > /etc/timezone \ 39 | # ---------- clear works ---------- 40 | && rm -rf /var/cache/apk/* /tmp/* /usr/share/man \ 41 | && echo -e "\033[42;37m Build Completed :).\033[0m\n" 42 | 43 | WORKDIR /opt/www 44 | 45 | # Composer Cache 46 | # COPY ./composer.* /opt/www/ 47 | # RUN composer install --no-dev --no-scripts 48 | 49 | COPY . /opt/www 50 | RUN print "\n" | composer install -o && php bin/hyperf.php 51 | 52 | EXPOSE 9501 53 | 54 | ENTRYPOINT ["php", "/opt/www/bin/hyperf.php", "start"] 55 | -------------------------------------------------------------------------------- /app/Listener/DbQueryExecutedListener.php: -------------------------------------------------------------------------------- 1 | logger = $container->get(LoggerFactory::class)->get('sql'); 36 | } 37 | 38 | public function listen(): array 39 | { 40 | return [ 41 | QueryExecuted::class, 42 | ]; 43 | } 44 | 45 | /** 46 | * @param QueryExecuted $event 47 | */ 48 | public function process(object $event) 49 | { 50 | if ($event instanceof QueryExecuted) { 51 | $sql = $event->sql; 52 | if (! Arr::isAssoc($event->bindings)) { 53 | foreach ($event->bindings as $key => $value) { 54 | $sql = Str::replaceFirst('?', "'{$value}'", $sql); 55 | } 56 | } 57 | 58 | $this->logger->info(sprintf('[%s] %s', $event->time, $sql)); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /config/autoload/server.php: -------------------------------------------------------------------------------- 1 | SWOOLE_PROCESS, 18 | 'servers' => [ 19 | [ 20 | 'name' => 'http', 21 | 'type' => Server::SERVER_HTTP, 22 | 'host' => '0.0.0.0', 23 | 'port' => 9501, 24 | 'sock_type' => SWOOLE_SOCK_TCP, 25 | 'callbacks' => [ 26 | Event::ON_REQUEST => [Hyperf\HttpServer\Server::class, 'onRequest'], 27 | ], 28 | ], 29 | ], 30 | 'settings' => [ 31 | Constant::OPTION_ENABLE_COROUTINE => true, 32 | Constant::OPTION_WORKER_NUM => swoole_cpu_num(), 33 | Constant::OPTION_PID_FILE => BASE_PATH . '/runtime/hyperf.pid', 34 | Constant::OPTION_OPEN_TCP_NODELAY => true, 35 | Constant::OPTION_MAX_COROUTINE => 100000, 36 | Constant::OPTION_OPEN_HTTP2_PROTOCOL => true, 37 | Constant::OPTION_MAX_REQUEST => 100000, 38 | Constant::OPTION_SOCKET_BUFFER_SIZE => 2 * 1024 * 1024, 39 | Constant::OPTION_BUFFER_OUTPUT_SIZE => 2 * 1024 * 1024, 40 | ], 41 | 'callbacks' => [ 42 | Event::ON_WORKER_START => [Hyperf\Framework\Bootstrap\WorkerStartCallback::class, 'onWorkerStart'], 43 | Event::ON_PIPE_MESSAGE => [Hyperf\Framework\Bootstrap\PipeMessageCallback::class, 'onPipeMessage'], 44 | Event::ON_WORKER_EXIT => [Hyperf\Framework\Bootstrap\WorkerExitCallback::class, 'onWorkerExit'], 45 | ], 46 | ]; 47 | -------------------------------------------------------------------------------- /test/Cases/RestaurantControllerTest.php: -------------------------------------------------------------------------------- 1 | stub = [ 16 | 'name' => 'Restaurant', 17 | 'email' => 'restaurant@email.tld', 18 | ]; 19 | 20 | $this->wrongEmailStub = [ 21 | 'name' => 'Restaurant', 22 | 'email' => 'invalid', 23 | ]; 24 | 25 | $this->wrongNameStub = [ 26 | 'name' => 'Re', 27 | 'email' => 'restaurant@email.tld', 28 | ]; 29 | } 30 | 31 | public function testCreate() 32 | { 33 | $actual = $this->client->post('/api/restaurants', $this->stub); 34 | self::assertSame($this->stub['name'], $actual['name']); 35 | self::assertSame($this->stub['email'], $actual['email']); 36 | } 37 | 38 | public function testUniqueEmail() 39 | { 40 | $this->client->post('/api/restaurants', $this->stub); 41 | $actual = $this->client->post('/api/restaurants', $this->stub); 42 | self::assertSame([ 43 | 'error' => true, 44 | 'message' => 'The email has already been taken.', 45 | ], $actual); 46 | } 47 | 48 | public function testInvalidEmail() 49 | { 50 | $actual = $this->client->post('/api/restaurants', $this->wrongEmailStub); 51 | self::assertSame([ 52 | 'error' => true, 53 | 'message' => 'The email must be a valid email address.', 54 | ], $actual); 55 | } 56 | 57 | public function testInvalidName() 58 | { 59 | $actual = $this->client->post('/api/restaurants', $this->wrongNameStub); 60 | self::assertSame([ 61 | 'error' => true, 62 | 'message' => 'The name must be at least 3 characters.', 63 | ], $actual); 64 | } 65 | 66 | protected function tearDown(): void 67 | { 68 | Restaurant::where($this->stub)->delete(); 69 | } 70 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inmana PHP 2 | 3 | 🚀 Developing [Rocketseat](https://rocketseat.com.br/) 's [Next Level Week](https://nextlevelweek.com/) (NLW#05) Application using [Swoole](https://www.swoole.co.uk/) + [Hyperf](https://hyperf.io/). 4 | 5 | > This is the app of the [Elixir track](https://nextlevelweek.com/episodios/elixir/3/edicao/5). 6 | 7 | | I know PHP/Swoole | I know NLW#5 | 8 | | --- | --- | 9 | | The Next Level Week is a sprint promoted by the education company Rocketseat to help in the learning of new technologies. Unfortunately they don't officially support a PHP track, but we love PHP specially when it comes with Swoole. | This is the following of the Elixir track, but using Swoole (instead of regular PHP-FPM), a high-performance server for PHP applications inspired on Erlang, and Hyper, a MVC framework analog to Phoenix, that supports Coroutines as first-class. | 10 | 11 | ## Comparison table 12 | 13 | | | Language | Runtime | Framework | ORM | 14 | | --- | --- | --- | --- | --- | 15 | | **From** | Elixir | Processes (Tasks/GenServer) | Phoenix | Ecto | 16 | | **To** | **PHP** | **Swoole Coroutines/Fibers** | **Hyperf** | **Hyperf-Eloquent** | 17 | 18 | For the REPL (iex) you can use [gokure/hyperf-tinker](https://packagist.org/packages/gokure/hyperf-tinker). 19 | 20 | The mail component for Elixir was based on the third-party [Bamboo](https://github.com/thoughtbot/bamboo) project, **same here, it uses [hyperf-ext/mail](https://github.com/hyperf-ext/mail)** (both running locally with logs, but with a wide range of transports/adapters available). 21 | 22 | > Check `/runtime/logs/hyperf.log` for sent emails. 23 | 24 | ## Trying out 25 | 26 | ### Up development environment 27 | 28 | ```shell 29 | docker compose run --rm --service-ports app 30 | ``` 31 | 32 | This will start an interactive shell inside a container-based development environment.
33 | **The following commands should be executed inside this shell session.** 34 | 35 | > If you don't want to use Docker you can skip this step, just make sure you have a running MySQL server, PHP 7.4 and Swoole 4.6. 36 | 37 | ### Install dependencies 38 | ```shell 39 | composer install 40 | ``` 41 | 42 | ### Configure 43 | Make sure to change `.env` for proper values (like setting `DB_HOST` to `db` if in Docker) and adding passwords. 44 | 45 | ### Run migrations 46 | ```shell 47 | php bin/hyperf.php migrate 48 | ``` 49 | 50 | ### Start the server (and watch for changes) 51 | ```shell 52 | php bin/hyperf.php server:watch 53 | ``` 54 | 55 | ### Trigger expiring emails 56 | ```shell 57 | php bin/hyperf.php mail:expiring 58 | ``` 59 | > After that, see `runtime/logs/hyperf.log` -------------------------------------------------------------------------------- /.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 | 'constant_case' => [ 66 | 'case' => 'lower', 67 | ], 68 | 'class_attributes_separation' => true, 69 | 'combine_consecutive_unsets' => true, 70 | 'declare_strict_types' => true, 71 | 'linebreak_after_opening_tag' => true, 72 | 'lowercase_static_reference' => true, 73 | 'no_useless_else' => true, 74 | 'no_unused_imports' => true, 75 | 'not_operator_with_successor_space' => true, 76 | 'not_operator_with_space' => false, 77 | 'ordered_class_elements' => true, 78 | 'php_unit_strict' => false, 79 | 'phpdoc_separation' => false, 80 | 'single_quote' => true, 81 | 'standardize_not_equals' => true, 82 | 'multiline_comment_opening_closing' => true, 83 | ]) 84 | ->setFinder( 85 | PhpCsFixer\Finder::create() 86 | ->exclude('public') 87 | ->exclude('runtime') 88 | ->exclude('vendor') 89 | ->in(__DIR__) 90 | ) 91 | ->setUsingCache(false); 92 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperf/hyperf-skeleton", 3 | "type": "project", 4 | "keywords": [ 5 | "php", 6 | "swoole", 7 | "framework", 8 | "hyperf", 9 | "microservice", 10 | "middleware" 11 | ], 12 | "description": "A coroutine framework that focuses on hyperspeed and flexible, specifically use for build microservices and middlewares.", 13 | "license": "Apache-2.0", 14 | "require": { 15 | "php": ">=7.4", 16 | "ext-json": "*", 17 | "ext-swoole": "^4.6", 18 | "duncan3dc/blade": "^4.10", 19 | "hyperf-ext/mail": "~2.1.0", 20 | "hyperf/async-queue": "^2.1", 21 | "hyperf/cache": "~2.1.0", 22 | "hyperf/command": "^2.1", 23 | "hyperf/config": "~2.1.0", 24 | "hyperf/crontab": "^2.1", 25 | "hyperf/database": "~2.1.0", 26 | "hyperf/db-connection": "~2.1.0", 27 | "hyperf/framework": "~2.1.0", 28 | "hyperf/guzzle": "~2.1.0", 29 | "hyperf/http-server": "~2.1.0", 30 | "hyperf/logger": "~2.1.0", 31 | "hyperf/memory": "~2.1.0", 32 | "hyperf/process": "~2.1.0", 33 | "hyperf/redis": "~2.1.0", 34 | "hyperf/validation": "~2.1.0", 35 | "hyperf/view": "~2.1.0", 36 | "hyperf/view-engine": "~2.1.0", 37 | "ramsey/uuid": "^4.1" 38 | }, 39 | "require-dev": { 40 | "friendsofphp/php-cs-fixer": "^2.14", 41 | "hyperf/devtool": "~2.1.0", 42 | "hyperf/testing": "~2.1.0", 43 | "hyperf/watcher": "^2.1", 44 | "mockery/mockery": "^1.0", 45 | "phpstan/phpstan": "^0.12", 46 | "swoole/ide-helper": "^4.5" 47 | }, 48 | "suggest": { 49 | "ext-openssl": "Required to use HTTPS.", 50 | "ext-pdo": "Required to use MySQL Client.", 51 | "ext-pdo_mysql": "Required to use MySQL Client.", 52 | "ext-redis": "Required to use Redis Client." 53 | }, 54 | "autoload": { 55 | "psr-4": { 56 | "App\\": "app/" 57 | }, 58 | "files": [] 59 | }, 60 | "autoload-dev": { 61 | "psr-4": { 62 | "HyperfTest\\": "./test/" 63 | } 64 | }, 65 | "minimum-stability": "dev", 66 | "prefer-stable": true, 67 | "config": { 68 | "optimize-autoloader": true, 69 | "sort-packages": true 70 | }, 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": [ 83 | "Composer\\Config::disableProcessTimeout", 84 | "php ./bin/hyperf.php start" 85 | ] 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /config/autoload/mail.php: -------------------------------------------------------------------------------- 1 | env('MAIL_MAILER', 'smtp'), 24 | 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | Mailer Configurations 28 | |-------------------------------------------------------------------------- 29 | | 30 | | Here you may configure all of the mailers used by your application plus 31 | | their respective settings. Several examples have been configured for 32 | | you and you are free to add your own as your application requires. 33 | | 34 | | Supports a variety of mail "transport" drivers to be used while 35 | | sending an e-mail. You will specify which one you are using for your 36 | | mailers below. You are free to add additional mailers as required. 37 | | 38 | */ 39 | 40 | 'mailers' => [ 41 | 'smtp' => [ 42 | 'transport' => \HyperfExt\Mail\Transport\SmtpTransport::class, 43 | 'options' => [ 44 | 'host' => env('MAIL_SMTP_HOST', 'smtp.mailgun.org'), 45 | 'port' => env('MAIL_SMTP_PORT', 587), 46 | 'encryption' => env('MAIL_SMTP_ENCRYPTION', 'tls'), 47 | 'username' => env('MAIL_SMTP_USERNAME'), 48 | 'password' => env('MAIL_SMTP_PASSWORD'), 49 | 'timeout' => env('MAIL_SMTP_TIMEOUT'), 50 | 'auth_mode' => env('MAIL_SMTP_AUTH_MODE'), 51 | ], 52 | ], 53 | 54 | 'aws_ses' => [ 55 | 'transport' => \HyperfExt\Mail\Transport\AwsSesTransport::class, 56 | 'options' => [ 57 | 'credentials' => [ 58 | 'key' => env('MAIL_AWS_SES_ACCESS_KEY_ID'), 59 | 'secret' => env('MAIL_AWS_SES_SECRET_ACCESS_KEY'), 60 | ], 61 | 'region' => env('MAIL_AWS_SES_REGION'), 62 | ], 63 | ], 64 | 65 | 'aliyun_dm' => [ 66 | 'transport' => \HyperfExt\Mail\Transport\AliyunDmTransport::class, 67 | 'options' => [ 68 | 'access_key_id' => env('MAIL_ALIYUN_DM_ACCESS_KEY_ID'), 69 | 'access_secret' => env('MAIL_ALIYUN_DM_ACCESS_SECRET'), 70 | 'region_id' => env('MAIL_ALIYUN_DM_REGION_ID'), 71 | 'click_trace' => env('MAIL_ALIYUN_DM_CLICK_TRACE', '0'), 72 | ], 73 | ], 74 | 75 | 'mailgun' => [ 76 | 'transport' => \HyperfExt\Mail\Transport\MailgunTransport::class, 77 | 'options' => [ 78 | 'domain' => env('MAIL_MAILGUN_DOMAIN'), 79 | 'key' => env('MAIL_MAILGUN_KEY'), 80 | 'endpoint' => env('MAIL_MAILGUN_ENDPOINT', 'api.mailgun.net'), 81 | ], 82 | ], 83 | 84 | 'postmark' => [ 85 | 'transport' => \HyperfExt\Mail\Transport\PostmarkTransport::class, 86 | 'options' => [ 87 | 'token' => env('MAIL_POSTMARK_TOKEN'), 88 | ], 89 | ], 90 | 91 | 'sendmail' => [ 92 | 'transport' => \HyperfExt\Mail\Transport\SendmailTransport::class, 93 | 'options' => [ 94 | 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs'), 95 | ], 96 | ], 97 | 98 | 'log' => [ 99 | 'transport' => \HyperfExt\Mail\Transport\LogTransport::class, 100 | 'options' => [ 101 | 'name' => 'mail.local', 102 | 'group' => 'default', 103 | ], 104 | ], 105 | ], 106 | 107 | /* 108 | |-------------------------------------------------------------------------- 109 | | Global "From" Address 110 | |-------------------------------------------------------------------------- 111 | | 112 | | You may wish for all e-mails sent by your application to be sent from 113 | | the same address. Here, you may specify a name and address that is 114 | | used globally for all e-mails that are sent by your application. 115 | | 116 | */ 117 | 118 | 'from' => [ 119 | 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 120 | 'name' => env('MAIL_FROM_NAME', 'Example'), 121 | ], 122 | 123 | /* 124 | |-------------------------------------------------------------------------- 125 | | Logger Options 126 | |-------------------------------------------------------------------------- 127 | | 128 | | The `hyperf/logger` component is required if enabled. 129 | */ 130 | 131 | 'logger' => [ 132 | 'enabled' => false, 133 | 'name' => 'mail', 134 | 'group' => 'default', 135 | ], 136 | ]; 137 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------