Блог на 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-разработка • База данных • Блог • Пользователь • Практика • Фреймворк







