Блог на Laravel 7, часть 17. Временная зона для пользователей, деплой на хостинг TimeWeb
27.02.2021
Теги: Git • GitHub • Laravel • MySQL • PHP • Web-разработка • БазаДанных • Блог • Пользователь • Практика • Фреймворк
Временна́я зона пользователя
У нас есть еще проблема с временно́й зоной — в базе данных мы храним дату и время в UTC. Будем показывать московское время для всех не аутентифицированных пользователй, а для аутентифицированных добавим возможность задать часовой пояс в личном кабинете. Мы добавим поле timezone
в таблицу базы данных users
и будем хранить в нем часовые пояса — Europe/Moscow
, Asia/Irkutsk
, Asia/Magadan
.
> php artisan make:migration alter_users_table --table=users
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class AlterUsersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table('users', function (Blueprint $table) { $table->string('timezone')->after('email')->nullable(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('users', function (Blueprint $table) { $table->dropColumn('timezone'); }); } }
> php artisan migrate Migrating: 2021_02_26_074945_alter_users_table Migrated: 2021_02_26_074945_alter_users_table (0.08 seconds)
Теперь в модели User
, Post
и Comment
добавим аксессоры, которые будут изменять значения свойств моделей в момент доступа.
class User extends Authenticatable { /** * Преобразует дату и время регистрации пользователя из UTC в Europe/Moscow * * @param $value * @return \Carbon\Carbon|false */ public function getCreatedAtAttribute($value) { $timezone = 'Europe/Moscow'; if ($this->timezone) { $timezone = $this->timezone; } return Carbon::createFromFormat('Y-m-d H:i:s', $value) ->timezone($timezone)->format('d.m.Y H:i'); } /** * Преобразует дату и время обновления пользователя из UTC в Europe/Moscow * * @param $value * @return \Carbon\Carbon|false */ public function getUpdatedAtAttribute($value) { $timezone = 'Europe/Moscow'; if ($this->timezone) { $timezone = $this->timezone; } return Carbon::createFromFormat('Y-m-d H:i:s', $value) ->timezone($timezone)->format('d.m.Y H:i'); } }
class Post extends Model { /** * Преобразует дату и время создания поста из UTC в Europe/Moscow * * @param $value * @return \Carbon\Carbon|false */ public function getCreatedAtAttribute($value) { $timezone = 'Europe/Moscow'; if (auth()->check() && auth()->user()->timezone) { $timezone = auth()->user()->timezone; } return Carbon::createFromFormat('Y-m-d H:i:s', $value) ->timezone($timezone)->format('d.m.Y H:i'); } /** * Преобразует дату и время обновления поста из UTC в Europe/Moscow * * @param $value * @return \Carbon\Carbon|false */ public function getUpdatedAtAttribute($value) { $timezone = 'Europe/Moscow'; if (auth()->check() && auth()->user()->timezone) { $timezone = auth()->user()->timezone; } return Carbon::createFromFormat('Y-m-d H:i:s', $value) ->timezone($timezone)->format('d.m.Y H:i'); } /** * Преобразует дату и время удаления поста из UTC в Europe/Moscow * * @param $value * @return \Carbon\Carbon|false */ public function getDeletedAtAttribute($value) { $timezone = 'Europe/Moscow'; if (auth()->check() && auth()->user()->timezone) { $timezone = auth()->user()->timezone; } return Carbon::createFromFormat('Y-m-d H:i:s', $value) ->timezone($timezone)->format('d.m.Y H:i'); } }
class Comment extends Model { /** * Преобразует дату и время создания комментария из UTC в Europe/Moscow * * @param $value * @return \Carbon\Carbon|false */ public function getCreatedAtAttribute($value) { $timezone = 'Europe/Moscow'; if (auth()->check() && auth()->user()->timezone) { $timezone = auth()->user()->timezone; } return Carbon::createFromFormat('Y-m-d H:i:s', $value) ->timezone($timezone)->format('d.m.Y H:i'); } /** * Преобразует дату и время обновления комментария из UTC в Europe/Moscow * * @param $value * @return \Carbon\Carbon|false */ public function getUpdatedAtAttribute($value) { $timezone = 'Europe/Moscow'; if (auth()->check() && auth()->user()->timezone) { $timezone = auth()->user()->timezone; } return Carbon::createFromFormat('Y-m-d H:i:s', $value) ->timezone($timezone)->format('d.m.Y H:i'); } /** * Преобразует дату и время удаления комментария из UTC в Europe/Moscow * * @param $value * @return \Carbon\Carbon|false */ public function getDeletedAtAttribute($value) { $timezone = 'Europe/Moscow'; if (auth()->check() && auth()->user()->timezone) { $timezone = auth()->user()->timezone; } return Carbon::createFromFormat('Y-m-d H:i:s', $value) ->timezone($timezone)->format('d.m.Y H:i'); } }
Теперь в личном кабинете добавим возможность для пользователя указать свой часовой пояс. В модель User
добавим константу TIMEZONES
с перечислением всех часовых поясов России. И создадим форму с выпадающим списком, где пользователь сможет выбрать подходящую — и будем хранить зону для каждого пользователя в базе данных.
class User extends Authenticatable { /* ... */ const TIMEZONES = [ 'Europe/Kaliningrad' => 'Калининград, Россия (+02:00)', 'Europe/Moscow' => 'Москва, Россия (+03:00)', 'Europe/Astrakhan' => 'Астрахань, Россия (+04:00)', 'Asia/Yekaterinburg' => 'Екатеринбург, Россия (+05:00)', 'Asia/Omsk' => 'Омск, Россия (+06:00)', 'Asia/Novosibirsk' => 'Новосибирск, Россия (+07:00)', 'Asia/Irkutsk' => 'Иркутск, Россия (+08:00)', 'Asia/Chita' => 'Чита, Россия (+09:00)', 'Asia/Vladivostok' => 'Владивосток, Россия (+10:00)', 'Asia/Magadan' => 'Магадан, Россия (+11:00)', 'Asia/Kamchatka' => 'Петропавловск-Камчатский, Россия (+12:00)' ]; /* ... */ }
Два новых маршрута:
/* * Личный кабинет пользователя */ Route::group([ 'as' => 'user.', // имя маршрута, например user.index 'prefix' => 'user', // префикс маршрута, например user/index 'namespace' => 'User', // пространство имен контроллеров 'middleware' => ['auth'] // один или несколько посредников ], function () { /* ..... */ /* * Редактирование персональных данных */ Route::get('edit/{user}', 'DataController@edit')->name('edit'); Route::put('update/{user}', 'DataController@update')->name('update'); });
Новый контроллер:
> php artisan make:controller User/DataController
namespace App\Http\Controllers\User; use App\Http\Controllers\Controller; use App\User; use Illuminate\Http\Request; class DataController extends Controller { /** * Показывает форму для редактирования данных */ public function edit(User $user) { $timezones = User::TIMEZONES; return view('user.data', compact('user', 'timezones')); } /** * Обновляет данные пользователя в базе данных */ public function update(Request $request, User $user) { /* * Проверяем данные формы */ $request->validate([ 'name' => 'string|required|max:255', 'timezone' => 'nullable|string|max:255' ]); /* * Обновляем пользователя */ $user->update($request->only(['name', 'timezone'])); /* * Возвращаемся на главную */ return redirect() ->route('user.index') ->with('success', 'Данные успешно обновлены'); } }
Шаблон user.data
:
@extends('layout.user', ['title' => 'Личные данные']) @section('content') <h1 class="mb-4">Личные данные</h1> <form method="post" action="{{ route('user.update', ['user' => $user->id]) }}"> @csrf @method('PUT') <div class="form-group"> <input type="text" class="form-control" name="name" placeholder="Имя, Фамилия" required maxlength="255" value="{{ old('name') ?? $user->name }}"> </div> <div class="form-group"> @php $timezone = old('timezone') ?? $user->timezone ?? null; @endphp <select name="timezone" class="form-control" title="Часовой пояс"> <option value="">Выберите</option> @foreach($timezones as $key => $value) <option value="{{ $key }}" @if ($key === $timezone) selected @endif> {{ $value }} </option> @endforeach </select> </div> <div class="form-group"> <button type="submit" class="btn btn-success">Сохранить</button> </div> </form> @endsection
На главной странице личного кабинета разместим ссылку для перехода к редактированию личных данных:
@extends('layout.user', ['title' => 'Личный кабинет']) @section('content') <h1>Личный кабинет</h1> <p>Добрый день {{ auth()->user()->name }}!</p> .......... <a href="{{ route('user.edit', ['user' => auth()->user()->id]) }}" class="btn btn-primary"> Личные данные </a> @endsection
Деплой на хостинг TimeWeb
1. Создаем репозиторий на GitHub
Сначала создадим новый репозиторий на GitHub, у меня это laravel-7-blog
. Сразу после создания получим подсказки, что делать дальше.
На локальном компьютере тоже создаем репозиторой, чтобы выложить на GitHub:
$ cd D:/work/localhost24/www # это директория проекта
$ git init # создаем пустой репозиторий Initialized empty Git repository in D:/work/localhost24/www/.git/ $ git add --all # добавляем все файлы проекта $ git commit -m "Initial сommit" # первый коммит
У нас уже есть подсказка от GitHub, что надо сделать, чтобы выложить проект (не торопитесь выполнять):
$ git remote add origin git@github.com:tokmakov/laravel-7-blog.git $ git branch -M main # изменить имя с master на main $ git push -u origin main
master
на main
. Потому что имя master
им представляется недостаточно толерантным. Но мы не будем следовать сомнительным советам, имя master
вполне подходит.
Добавляем себе ссылку (pointer) на удаленный репозиторий:
$ git remote add origin git@github.com:tokmakov/laravel-7-blog.git
Теперь удаленный репозиторий доступен как origin
:
$ git remote origin
Отправляем текущую ветку master
(которая у нас сейчас всего одна) в удаленную ветку master
репозитория origin
:
$ git push origin master
Настроим текущую ветку master
на отслеживание удаленной ветки master
:
$ git branch -u origin/master Branch master set up to track remote branch master from origin.
2. Клонируем проект на сервере
В панели управления хостинга нужно включить доступ по SSH и подключиться из командной строки к серверу. На хостинге TimeWeb уже установлен Composer, только нужно задать алиасы (см. здесь) для composer
и php
.
$ pwd /home/c/ca12345 $ nano .bash_profile
alias composer='/opt/php72/bin/php -d memory_limit=1024M /usr/local/bin/composer' alias php='/opt/php72/bin/php -d memory_limit=1024M'
Чтобы применить изменения из файла .bash_profile
:
$ source ~/.bash_profile
Проверяем, что Composer нам теперь доступен:
$ composer ______ / ____/___ ____ ___ ____ ____ ________ _____ / / / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/ / /___/ /_/ / / / / / / /_/ / /_/ (__ ) __/ / \____/\____/_/ /_/ /_/ .___/\____/____/\___/_/ /_/ Composer version 1.9.0 2019-08-02 20:55:32 Usage: command [options] [arguments] ..........
Создаем директорию для нашего проекта в панели управления хостингом, у меня это laravel-7-blog
.
Внутри будет автоматически создана директория public_html
, которая является корнем сервера. Нам это директория не нужна, потому что у нас корень сервера — директория public
.
$ rm -r ~/laravel-7-blog/public_html
Клонируем в laravel-7-blog
репозиторий с GitHub:
$ git clone https://github.com/tokmakov/laravel-7-blog.git laravel-7-blog
3. Устанавливаем проект на сервере
Устанавливаем проект с помощью Composer:
$ cd laravel-7-blog $ composer install --no-dev
Проблема в сервис-провайдере IdeHelperServiceProvider
, так что удаляем его из config/app.php
, а вместо этого регистрируем в AppServiceProvider
.
namespace App\Providers; use App\Observers\PostObserver; use App\Post; use Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function register() { if ($this->app->isLocal()) { $this->app->register(IdeHelperServiceProvider::class); } } public function boot() { Post::observe(PostObserver::class); } }
laravel-ide-helper
позволяет создать файл-хелпер, содержащий сгенерированные статические классы фасадов. Классы никак не используются приложением, а нужны только для автодополнения PhpStorm. У меня на локальном сервере этот пакет в секции require-dev
файла composer.json
, но на хостинге пакет не был установлен — потому что устанавливаются пакеты только из секции require
.
Выкладываем изменения на GitHub:
$ git commit -a -m "package laravel-ide-helper" $ git push
На сервере получаем изменения:
$ git pull
Повторяем установку проекта:
$ composer install --no-dev
4. Создаем новую базу данных
В панели управления хостингом создаем новую базу данных, у меня это ca12345_lar7blog
.
Создаем файл конфигурации приложения, указываем имя БД, имя пользователя БД и пароль.
$ cp .env.example .env $ nano .env
APP_NAME="Блог о веб-разработке" APP_ENV=production APP_KEY= APP_DEBUG=false APP_URL=http://laravel-7-blog.tokmakov.msk.ru LOG_CHANNEL=stack DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=ca12345_lar7blog DB_USERNAME=ca12345_lar7blog DB_PASSWORD=5MdafrXs
Создаем ключ приложения Laravel:
$ php artisan key:generate Application key set successfully
5. Создаем таблицы базы данных
Запускаем миграцию, чтобы создать таблицы БД и тестовые данные:
$ php artisan migrate:fresh --seed ************************************** * Application In Production! * ************************************** Do you really wish to run this command? (yes/no) [no]: > yes Dropped all tables successfully. Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table In Connection.php line 671: SQLSTATE[HY000]: General error: 1709 Index column size too large. The maxim um column size is 767 bytes. (SQL: alter table `users` add unique `users_em ail_unique`(`email`)) In Connection.php line 464: SQLSTATE[HY000]: General error: 1709 Index column size too large. The maxim um column size is 767 bytes.
Исправить это легко, изменяем файл AppServiceProvider.php
:
class AppServiceProvider extends ServiceProvider { public function register() { Schema::defaultStringLength(191); /* ... */ } /* ... */ }
Выкладываем изменения на GitHub:
$ git commit -a -m "default string length" $ git push
На сервере получаем изменения:
$ git pull
Снова запускаем миграцию и получаем следующую ошибку:
Для заполнения БД начальными данными нужен класс Faker\Factory
, но пакет fakerphp/faker
в секции require-dev
файла composer.json
— так что вручную перемещаем его в секцию require
и обновляем файл composer.lock
.
> composer update --lock
Выкладываем изменения на GitHub:
$ git commit -a -m "package fakerphp/faker" $ git push
На сервере получаем изменения:
$ git pull
Повторяем установку проекта:
$ composer install --no-dev
Снова запускаем миграцию:
$ php artisan migrate:fresh --seed
Теперь проблемы с отправкой почты — редактируем файл .env
:
$ nano .env
MAIL_MAILER=smtp MAIL_HOST=smtp.timeweb.ru MAIL_PORT=465 MAIL_USERNAME=laravel-7-blog@tokmakov.msk.ru MAIL_PASSWORD=пароль-почтового-ящика MAIL_ENCRYPTION=ssl MAIL_FROM_ADDRESS=laravel-7-blog@tokmakov.msk.ru MAIL_FROM_NAME="${APP_NAME}"
Кроме того, надо создать такой почтовый ящик с таким паролем в панели управления хостинга.
$ php artisan migrate:fresh --seed
На этот раз все прошло успешно.
6. Создаем символические ссылки
Мы удалили директорию public_html
внутри laravel-7-blog
, которая на хостинге является корнем веб-сервера. Теперь вместо нее создадим символическую ссылку public_html
(см. здесь), которая будет указывать на директорию public
.
$ ln -s ~/laravel-7-blog/public ~/laravel-7-blog/public_html
Кроме того, создаем символическую ссылку public/storage
на директорию storage/app/public
:
$ php artisan storage:link The [/home/c/ca12345/laravel-7-blog/public/storage] link has been connected to [/home/c/ca12345/laravel-7-blog/storage/app/public]. The links have been created.
Исходные коды здесь, демо-сайт здесь.
- Блог на Laravel 7, часть 11. Панель управления — назначение ролей и прав для пользователей
- Блог на Laravel 7, часть 10. Личный кабинет — CRUD-операции над постами и комментариями
- Блог на Laravel 7, часть 3. Checkbox «Запомнить меня» и подтверждение адреса почты
- Мини-блог на Laravel, часть 9. Защита маршрутов создания, редактирования и удаления
- Мини-блог на Laravel, часть 8. Регистрация и аутентификация пользователей на сайте
- Блог на Laravel 7, часть 16. Роль нового пользователя, сообщение админу о новом посте
- Блог на Laravel 7, часть 13. Загрузка и ресайз изображений для категорий и постов блога
Поиск: Git • GitHub • Laravel • MySQL • PHP • Web-разработка • База данных • Блог • Пользователь • Практика • Фреймворк