Блог на Laravel 7, часть 17. Временная зона для пользователей, деплой на хостинг TimeWeb

27.02.2021

Теги: GitGitHubLaravelMySQLPHPWeb-разработкаБазаДанныхБлогПользовательПрактикаФреймворк

Временна́я зона пользователя

У нас есть еще проблема с временно́й зоной — в базе данных мы храним дату и время в 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
Разработчики GitHub предлагают еще изменить имя ветки с 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.

Исходные коды здесь, демо-сайт здесь.

Поиск: Git • GitHub • Laravel • MySQL • PHP • Web-разработка • База данных • Блог • Пользователь • Практика • Фреймворк

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.