diff --git a/.env.example b/.env.example index 71b86cde..3e3af021 100644 --- a/.env.example +++ b/.env.example @@ -26,6 +26,7 @@ QUEUE_CONNECTION=redis SESSION_DRIVER=redis SESSION_LIFETIME=120 FILESYSTEM_DISK=local +CACHE_QUERY_STORE=redis MAIL_HOST=toby-mailpit-dev MAIL_PORT=1025 diff --git a/app/Domain/DailySummaryRetriever.php b/app/Domain/DailySummaryRetriever.php index d5f6b024..edd1d244 100644 --- a/app/Domain/DailySummaryRetriever.php +++ b/app/Domain/DailySummaryRetriever.php @@ -23,7 +23,7 @@ public function __construct( public function getAbsences(Carbon $date): Collection { return VacationRequest::query() - ->with(["user", "vacations"]) + ->with(["user.profile", "vacations"]) ->whereDate("from", "<=", $date) ->whereDate("to", ">=", $date) ->states(VacationRequestStatesRetriever::notFailedStates()) @@ -41,7 +41,7 @@ public function getAbsences(Carbon $date): Collection public function getRemoteDays(Carbon $date): Collection { return VacationRequest::query() - ->with(["user", "vacations"]) + ->with(["user.profile", "vacations"]) ->whereDate("from", "<=", $date) ->whereDate("to", ">=", $date) ->states(VacationRequestStatesRetriever::notFailedStates()) @@ -77,7 +77,7 @@ public function getUpcomingAbsences(Carbon $date): Collection public function getUpcomingRemoteDays(Carbon $date): Collection { return VacationRequest::query() - ->with(["user", "vacations"]) + ->with(["user.profile", "vacations"]) ->whereDate("from", ">", $date) ->states(VacationRequestStatesRetriever::notFailedStates()) ->whereIn( diff --git a/app/Domain/DashboardAggregator.php b/app/Domain/DashboardAggregator.php index b3c3e595..b402f4e7 100644 --- a/app/Domain/DashboardAggregator.php +++ b/app/Domain/DashboardAggregator.php @@ -49,25 +49,27 @@ public function aggregateCalendarData(User $user, YearPeriod $yearPeriod): array { $approvedVacations = $user ->vacations() - ->with("vacationRequest.vacations") + ->with(["vacationRequest.vacations", "vacationRequest.user.profile"]) ->whereBelongsTo($yearPeriod) + ->cache() ->approved() ->get() ->mapWithKeys( fn(Vacation $vacation): array => [ - $vacation->date->toDateString() => new DashboardVacationRequestResource($vacation->vacationRequest->load(["user", "vacations"])), + $vacation->date->toDateString() => new DashboardVacationRequestResource($vacation->vacationRequest), ], ); $pendingVacations = $user ->vacations() - ->with("vacationRequest.vacations") + ->with(["vacationRequest.vacations", "vacationRequest.user.profile"]) ->whereBelongsTo($yearPeriod) + ->cache() ->pending() ->get() ->mapWithKeys( fn(Vacation $vacation): array => [ - $vacation->date->toDateString() => new DashboardVacationRequestResource($vacation->vacationRequest->load(["user", "vacations"])), + $vacation->date->toDateString() => new DashboardVacationRequestResource($vacation->vacationRequest), ], ); @@ -86,17 +88,19 @@ public function aggregateVacationRequests(User $user, YearPeriod $yearPeriod): J { if ($user->can("listAllRequests")) { $vacationRequests = $yearPeriod->vacationRequests() - ->with(["user", "vacations"]) + ->with(["user", "vacations", "vacations.user", "vacations.user.profile", "user.permissions", "user.profile"]) ->states(VacationRequestStatesRetriever::waitingForUserActionStates($user)) ->latest("updated_at") ->limit(3) + ->cache() ->get(); } else { $vacationRequests = $user->vacationRequests() - ->with(["user", "vacations"]) + ->with(["user", "vacations", "vacations.user", "vacations.user.profile", "user.permissions", "user.profile"]) ->whereBelongsTo($yearPeriod) ->latest("updated_at") ->limit(3) + ->cache() ->get(); } diff --git a/app/Domain/UserVacationStatsRetriever.php b/app/Domain/UserVacationStatsRetriever.php index 117175dc..8aa53eaf 100644 --- a/app/Domain/UserVacationStatsRetriever.php +++ b/app/Domain/UserVacationStatsRetriever.php @@ -103,6 +103,7 @@ public function getVacationDaysLimit(User $user, YearPeriod $yearPeriod): int { return $user->vacationLimits() ->whereBelongsTo($yearPeriod) + ->cache() ->first()?->limit ?? 0; } @@ -110,6 +111,7 @@ public function hasVacationDaysLimit(User $user, YearPeriod $yearPeriod): bool { return $user->vacationLimits() ->whereBelongsTo($yearPeriod) + ->cache() ->first()?->hasVacation() ?? false; } diff --git a/app/Eloquent/Helpers/YearPeriodRetriever.php b/app/Eloquent/Helpers/YearPeriodRetriever.php index ff464967..a3771093 100644 --- a/app/Eloquent/Helpers/YearPeriodRetriever.php +++ b/app/Eloquent/Helpers/YearPeriodRetriever.php @@ -4,6 +4,7 @@ namespace Toby\Eloquent\Helpers; +use Illuminate\Cache\CacheManager; use Illuminate\Contracts\Session\Session; use Toby\Eloquent\Models\YearPeriod; @@ -13,14 +14,17 @@ class YearPeriodRetriever public function __construct( protected Session $session, + protected CacheManager $cacheManager, ) {} public function selected(): YearPeriod { - /** @var YearPeriod $yearPeriod */ - $yearPeriod = YearPeriod::query()->find($this->session->get(static::SESSION_KEY)); + return $this->cacheManager->remember("selected_year_period", 60, function () { + /** @var YearPeriod $yearPeriod */ + $yearPeriod = YearPeriod::query()->find($this->session->get(static::SESSION_KEY)); - return $yearPeriod !== null ? $yearPeriod : $this->current(); + return $yearPeriod !== null ? $yearPeriod : $this->current(); + }); } public function current(): YearPeriod @@ -33,7 +37,7 @@ public function links(): array $selected = $this->selected(); $current = $this->current(); - $years = YearPeriod::all(); + $years = YearPeriod::query()->cache()->get(); $navigation = $years->map(fn(YearPeriod $yearPeriod): array => $this->toNavigation($yearPeriod)); diff --git a/app/Infrastructure/Http/Controllers/SelectYearPeriodController.php b/app/Infrastructure/Http/Controllers/SelectYearPeriodController.php index c9ab368c..82ddd761 100644 --- a/app/Infrastructure/Http/Controllers/SelectYearPeriodController.php +++ b/app/Infrastructure/Http/Controllers/SelectYearPeriodController.php @@ -6,6 +6,7 @@ use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Cache; use Toby\Eloquent\Helpers\YearPeriodRetriever; use Toby\Eloquent\Models\YearPeriod; @@ -14,6 +15,7 @@ class SelectYearPeriodController extends Controller public function __invoke(Request $request, YearPeriod $yearPeriod): RedirectResponse { $request->session()->put(YearPeriodRetriever::SESSION_KEY, $yearPeriod->id); + Cache::forget("selected_year_period"); return redirect() ->back() diff --git a/app/Infrastructure/Http/Controllers/VacationRequestController.php b/app/Infrastructure/Http/Controllers/VacationRequestController.php index cff96ee7..4f7736b7 100644 --- a/app/Infrastructure/Http/Controllers/VacationRequestController.php +++ b/app/Infrastructure/Http/Controllers/VacationRequestController.php @@ -43,7 +43,7 @@ public function index(Request $request, YearPeriodRetriever $yearPeriodRetriever $vacationRequests = $request->user() ->vacationRequests() - ->with(["user", "vacations"]) + ->with(["vacations.user.profile", "user.permissions", "user.profile"]) ->whereBelongsTo($yearPeriodRetriever->selected()) ->latest() ->states(VacationRequestStatesRetriever::filterByStatusGroup($status, $request->user())) @@ -99,7 +99,7 @@ public function indexForApprovers( $type = $request->get("type"); $vacationRequests = VacationRequest::query() - ->with(["user", "vacations"]) + ->with(["vacations.user.profile", "user.permissions", "user.profile"]) ->whereBelongsTo($yearPeriod) ->when($user !== null, fn(Builder $query): Builder => $query->where("user_id", $user)) ->when($type !== null, fn(Builder $query): Builder => $query->where("type", $type)) @@ -131,7 +131,7 @@ public function show(VacationRequest $vacationRequest, UserVacationStatsRetrieve { $this->authorize("show", $vacationRequest); - $vacationRequest->load(["user", "vacations", "activities", "activities.user.profile"]); + $vacationRequest->load(["vacations.user.profile", "user.permissions", "user.profile", "activities.user.profile"]); $limit = $statsRetriever->getVacationDaysLimit($vacationRequest->user, $vacationRequest->yearPeriod); $used = $statsRetriever->getUsedVacationDays($vacationRequest->user, $vacationRequest->yearPeriod); $pending = $statsRetriever->getPendingVacationDays($vacationRequest->user, $vacationRequest->yearPeriod); diff --git a/app/Infrastructure/Http/Middleware/HandleInertiaRequests.php b/app/Infrastructure/Http/Middleware/HandleInertiaRequests.php index 616747b4..a3ba9cd3 100644 --- a/app/Infrastructure/Http/Middleware/HandleInertiaRequests.php +++ b/app/Infrastructure/Http/Middleware/HandleInertiaRequests.php @@ -32,7 +32,7 @@ public function share(Request $request): array protected function getAuthData(Request $request): Closure { - $user = $request->user(); + $user = $request->user()?->load("profile"); return fn(): array => [ "user" => $user ? new UserResource($user) : null, diff --git a/app/Infrastructure/Http/Resources/VacationRequestResource.php b/app/Infrastructure/Http/Resources/VacationRequestResource.php index e564ca56..a1414b52 100644 --- a/app/Infrastructure/Http/Resources/VacationRequestResource.php +++ b/app/Infrastructure/Http/Resources/VacationRequestResource.php @@ -30,7 +30,7 @@ public function toArray($request): array return [ "id" => $this->id, "name" => $this->name, - "user" => new SimpleUserResource($this->user), + "user" => new SimpleUserResource($user), "type" => $this->type, "isVacation" => $this->configRetriever->isVacation($this->type), "state" => $this->state, @@ -38,7 +38,7 @@ public function toArray($request): array "to" => $this->to->toDisplayString(), "displayDate" => $this->getDate($this->from->toDisplayString(), $this->to->toDisplayString()), "comment" => $this->comment, - "days" => VacationResource::collection($this->vacations->load("user")), + "days" => VacationResource::collection($this->vacations), "can" => [ "acceptAsTechnical" => $this->resource->state->canTransitionTo(AcceptedByTechnical::class) && $user->can("acceptAsTechApprover", $this->resource), diff --git a/composer.json b/composer.json index 2aa2d6f9..14de3845 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,7 @@ "fakerphp/faker": "^1.22.0", "guzzlehttp/guzzle": "^7.7.0", "inertiajs/inertia-laravel": "^0.6.11", + "laragear/cache-query": "^4.0", "laravel/framework": "^10.33.0", "laravel/sanctum": "^3.3.2", "laravel/socialite": "^5.10.0", @@ -30,6 +31,7 @@ "spatie/laravel-slack-slash-command": "^1.11.4" }, "require-dev": { + "barryvdh/laravel-debugbar": "^3.12", "blumilksoftware/codestyle": "^2.8.0", "laravel/dusk": "^7.11.4", "mockery/mockery": "^1.5.1", diff --git a/composer.lock b/composer.lock index ac9f941c..67566c96 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c8d8df84c5fefa8639edb8deeb5f42c1", + "content-hash": "c8440fbff00cfa7d3aff1ce231602b3f", "packages": [ { "name": "azuyalabs/yasumi", @@ -2454,6 +2454,73 @@ ], "time": "2023-10-10T08:35:13+00:00" }, + { + "name": "laragear/cache-query", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/Laragear/CacheQuery.git", + "reference": "377a8706c8433f171fb2883a644efb01470ae7fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Laragear/CacheQuery/zipball/377a8706c8433f171fb2883a644efb01470ae7fa", + "reference": "377a8706c8433f171fb2883a644efb01470ae7fa", + "shasum": "" + }, + "require": { + "illuminate/cache": "10.*|11.*", + "illuminate/config": "10.*|11.*", + "illuminate/container": "10.*|11.*", + "illuminate/contracts": "10.*|11.*", + "illuminate/database": "10.*|11.*", + "illuminate/support": "10.*|11.*", + "php": "^8.1" + }, + "require-dev": { + "orchestra/testbench": "8.*|9.*" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laragear\\CacheQuery\\CacheQueryServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laragear\\CacheQuery\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Italo Baeza C.", + "email": "DarkGhostHunter@Gmail.com", + "homepage": "https://github.com/sponsors/DarkGhostHunter" + } + ], + "description": "Remember your query results using only one method. Yes, only one.", + "support": { + "issues": "https://github.com/laragear/cache-query/issues", + "source": "https://github.com/laragear/cache-query" + }, + "funding": [ + { + "url": "https://github.com/sponsors/DarkGhostHunter", + "type": "Github Sponsorship" + }, + { + "url": "https://paypal.me/darkghosthunter", + "type": "Paypal" + } + ], + "time": "2024-03-04T04:27:23+00:00" + }, { "name": "laravel/framework", "version": "v10.48.3", @@ -6612,16 +6679,16 @@ }, { "name": "spatie/laravel-package-tools", - "version": "1.16.3", + "version": "1.16.4", "source": { "type": "git", "url": "https://github.com/spatie/laravel-package-tools.git", - "reference": "59db18c2e20d49a0b6d447bb1c654f6c123beb9e" + "reference": "ddf678e78d7f8b17e5cdd99c0c3413a4a6592e53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/59db18c2e20d49a0b6d447bb1c654f6c123beb9e", - "reference": "59db18c2e20d49a0b6d447bb1c654f6c123beb9e", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/ddf678e78d7f8b17e5cdd99c0c3413a4a6592e53", + "reference": "ddf678e78d7f8b17e5cdd99c0c3413a4a6592e53", "shasum": "" }, "require": { @@ -6660,7 +6727,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-package-tools/issues", - "source": "https://github.com/spatie/laravel-package-tools/tree/1.16.3" + "source": "https://github.com/spatie/laravel-package-tools/tree/1.16.4" }, "funding": [ { @@ -6668,7 +6735,7 @@ "type": "github" } ], - "time": "2024-03-07T07:35:57+00:00" + "time": "2024-03-20T07:29:11+00:00" }, { "name": "spatie/laravel-permission", @@ -6754,31 +6821,31 @@ }, { "name": "spatie/laravel-slack-slash-command", - "version": "1.11.5", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-slack-slash-command.git", - "reference": "1fad21d1713b8945200a028362d214eb73f35ccd" + "reference": "ba3ee5603f8bde562eb93cd7822a0ffee543889d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-slack-slash-command/zipball/1fad21d1713b8945200a028362d214eb73f35ccd", - "reference": "1fad21d1713b8945200a028362d214eb73f35ccd", + "url": "https://api.github.com/repos/spatie/laravel-slack-slash-command/zipball/ba3ee5603f8bde562eb93cd7822a0ffee543889d", + "reference": "ba3ee5603f8bde562eb93cd7822a0ffee543889d", "shasum": "" }, "require": { "guzzlehttp/guzzle": "^7.0", - "illuminate/config": "~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0", - "illuminate/queue": "~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0", - "illuminate/routing": "~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0", - "illuminate/support": "~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0", + "illuminate/config": "~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/queue": "~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/routing": "~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/support": "~5.8.0|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", "php": "^7.3|^8.0" }, "require-dev": { "mockery/mockery": "^1.0", - "orchestra/testbench": "~3.8.0|^4.0|^5.0|^6.0|^7.0|^8.0", - "pestphp/pest": "^1.22", - "phpunit/phpunit": "^9.2" + "orchestra/testbench": "~3.8.0|^4.0|^5.0|^6.0|^7.0|^8.0|^9.0", + "pestphp/pest": "^1.22|^2.34", + "phpunit/phpunit": "^9.2|^10.5" }, "type": "library", "extra": { @@ -6813,9 +6880,9 @@ ], "support": { "issues": "https://github.com/spatie/laravel-slack-slash-command/issues", - "source": "https://github.com/spatie/laravel-slack-slash-command/tree/1.11.5" + "source": "https://github.com/spatie/laravel-slack-slash-command/tree/1.12.0" }, - "time": "2024-01-18T15:46:42+00:00" + "time": "2024-03-20T07:31:06+00:00" }, { "name": "symfony/console", @@ -9520,16 +9587,16 @@ }, { "name": "composer/pcre", - "version": "3.1.2", + "version": "3.1.3", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "4775f35b2d70865807c89d32c8e7385b86eb0ace" + "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/4775f35b2d70865807c89d32c8e7385b86eb0ace", - "reference": "4775f35b2d70865807c89d32c8e7385b86eb0ace", + "url": "https://api.github.com/repos/composer/pcre/zipball/5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", + "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", "shasum": "" }, "require": { @@ -9571,7 +9638,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.1.2" + "source": "https://github.com/composer/pcre/tree/3.1.3" }, "funding": [ { @@ -9587,7 +9654,7 @@ "type": "tidelift" } ], - "time": "2024-03-07T15:38:35+00:00" + "time": "2024-03-19T10:26:25+00:00" }, { "name": "composer/xdebug-handler", @@ -9728,16 +9795,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.52.0", + "version": "v3.52.1", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "a3564bd66f4bce9bc871ef18b690e2dc67a7f969" + "reference": "6e77207f0d851862ceeb6da63e6e22c01b1587bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/a3564bd66f4bce9bc871ef18b690e2dc67a7f969", - "reference": "a3564bd66f4bce9bc871ef18b690e2dc67a7f969", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/6e77207f0d851862ceeb6da63e6e22c01b1587bc", + "reference": "6e77207f0d851862ceeb6da63e6e22c01b1587bc", "shasum": "" }, "require": { @@ -9808,7 +9875,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.52.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.52.1" }, "funding": [ { @@ -9816,7 +9883,7 @@ "type": "github" } ], - "time": "2024-03-18T18:40:11+00:00" + "time": "2024-03-19T21:02:43+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -9993,16 +10060,16 @@ }, { "name": "mockery/mockery", - "version": "1.6.9", + "version": "1.6.10", "source": { "type": "git", "url": "https://github.com/mockery/mockery.git", - "reference": "0cc058854b3195ba21dc6b1f7b1f60f4ef3a9c06" + "reference": "47065d1be1fa05def58dc14c03cf831d3884ef0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/0cc058854b3195ba21dc6b1f7b1f60f4ef3a9c06", - "reference": "0cc058854b3195ba21dc6b1f7b1f60f4ef3a9c06", + "url": "https://api.github.com/repos/mockery/mockery/zipball/47065d1be1fa05def58dc14c03cf831d3884ef0b", + "reference": "47065d1be1fa05def58dc14c03cf831d3884ef0b", "shasum": "" }, "require": { @@ -10014,8 +10081,8 @@ "phpunit/phpunit": "<8.0" }, "require-dev": { - "phpunit/phpunit": "^8.5 || ^9.6.10", - "symplify/easy-coding-standard": "^12.0.8" + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" }, "type": "library", "autoload": { @@ -10072,7 +10139,7 @@ "security": "https://github.com/mockery/mockery/security/advisories", "source": "https://github.com/mockery/mockery" }, - "time": "2023-12-10T02:24:34+00:00" + "time": "2024-03-19T16:15:45+00:00" }, { "name": "myclabs/deep-copy", diff --git a/config/cache-query.php b/config/cache-query.php new file mode 100644 index 00000000..381a2a40 --- /dev/null +++ b/config/cache-query.php @@ -0,0 +1,8 @@ + env("CACHE_QUERY_STORE"), + "prefix" => "cache-query", +]; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index f182e303..2e26d406 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -70,7 +70,7 @@ public function run(): void foreach ($users as $user) { VacationRequest::factory() - ->count(10) + ->count(50) ->for($user) ->for($user, "creator") ->sequence(fn() => [ diff --git a/environment/prod/deployment/beta/.env.beta b/environment/prod/deployment/beta/.env.beta index ab94f2ab..6e8230d6 100644 --- a/environment/prod/deployment/beta/.env.beta +++ b/environment/prod/deployment/beta/.env.beta @@ -15,6 +15,7 @@ QUEUE_CONNECTION=redis SESSION_DRIVER=redis SESSION_LIFETIME=120 FILESYSTEM_DISK=local +CACHE_QUERY_STORE=redis MAIL_MAILER=log diff --git a/environment/prod/deployment/prod/.env.prod b/environment/prod/deployment/prod/.env.prod index 9946486c..40ca8524 100644 --- a/environment/prod/deployment/prod/.env.prod +++ b/environment/prod/deployment/prod/.env.prod @@ -15,6 +15,7 @@ QUEUE_CONNECTION=redis SESSION_DRIVER=redis SESSION_LIFETIME=120 FILESYSTEM_DISK=local +CACHE_QUERY_STORE=redis MAIL_MAILER=smtp MAIL_PORT=465