Магазин на Laravel 7, часть 9. Панель управления сайтом, авторизация администратора

09.10.2020

Теги: LaravelMySQLPHPWeb-разработкаАвторизацияАутентификацияИнтернетМагазинКаталогТоваровМиграцииПанельУправленияПользовательПраваДоступаПрактикаФреймворк

Есть еще один момент, о котором забыл упомянуть в предыдущей части. Если аутентифицированный пользователь попробует перейти на страницу регистрации или на страницу восстановления пароля — он будет перенаправлен на страницу /home. Это логично, потому что на странице регистрации или восстановления пароля ему делать нечего. Страница /home нам не нужна, поэтому мы удалили контроллер HomeController, шаблон views/home.blade.php и маршрут до этой страницы — но редирект остался. Изменить это можно в посреднике (middleware) RedirectIfAuthenticated.

namespace App\Http\Middleware;

use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Support\Facades\Auth;

class RedirectIfAuthenticated {
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string|null  $guard
     * @return mixed
     */
    public function handle($request, Closure $next, $guard = null) {
        if (Auth::guard($guard)->check()) {
            // return redirect(RouteServiceProvider::HOME);
            return redirect()->route('user.index');
        }

        return $next($request);
    }
}

Администратор магазина

Хорошо, с этим разобрались, двигаемся дальше. Теперь нам нужна панель администратора магазина, где можно будет управлять каталогом и обрабатывать заказы. Администратор — тоже пользователь сайта и должен быть зарегистрирован и аутентифицирован. Чтобы различать обычных пользователй и администратора, добавим еще одно поле admin в таблицу базы данных users.

> 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->boolean('admin')->after('email')->default(false);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down() {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('admin');
        });
    }
}
> php artisan migrate

Теперь создадим контроллер, который будет отвечать за показ главной страницы панели управления:

> php artisan make:controller Admin\AdminController --invokable
namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class IndexController extends Controller {
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct() {
        $this->middleware('auth');
        $this->middleware('admin');
    }

    /**
     * Handle the incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function __invoke(Request $request) {
        return view('admin.index');
    }
}

Обратите внимание, что контроллер мы создали в директории app/Http/Controllers/Admin — это значит, что при добавлении новых маршртутов, мы должны указать пространство имен (относительно дефолтного App\Http\Controllers).

// это первый вариант указания пространства имен
Route::name('admin.')->prefix('admin')->group(function () {
    Route::get('index', 'Admin\IndexController')->name('index');
});
// это второй вариант указания пространства имен
Route::namespace('Admin')->name('admin.')->prefix('admin')->group(function () {
    Route::get('index', 'IndexController')->name('index');
});

Теперь нам нужны два шаблона — layout-шаблон admin.blade.php создадим в директории views/layout, а шаблон главной страницы панели управления index.blade.php — в директории views/admin.

<!doctype html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Панель управления</title>
    <link rel="stylesheet" href="{{ asset('css/app.css') }}">
    <link rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css"
          integrity="sha384-AYmEC3Yw5cVb3ZcuHtOA93w35dYTsvhLPVnYs9eStHfGJvOvKxVfELGroGkvsg+p"
          crossorigin="anonymous"/>
    <link rel="stylesheet" href="{{ asset('css/admin.css') }}">
    <script src="{{ asset('js/app.js') }}"></script>
    <script src="{{ asset('js/admin.js') }}"></script>
</head>
<body>
<div class="container">
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
        <!-- Бренд и кнопка «Гамбургер» -->
        <a class="navbar-brand" href="{{ route('admin.index') }}">Панель управления</a>
        <button class="navbar-toggler" type="button" data-toggle="collapse"
                data-target="#navbar-example" aria-controls="navbar-example"
                aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <!-- Основная часть меню (может содержать ссылки, формы и прочее) -->
        <div class="collapse navbar-collapse" id="navbar-example">
            <!-- Этот блок расположен слева -->
            <ul class="navbar-nav mr-auto">
                <li class="nav-item">
                    <a class="nav-link" href="#">Заказы</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="#">Каталог</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="#">Категории</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="#">Бренды</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="#">Товары</a>
                </li>
            </ul>

            <!-- Этот блок расположен справа -->
            <ul class="navbar-nav ml-auto">
                <li class="nav-item">
                    <a onclick="document.getElementById('logout-form').submit(); return false"
                       href="{{ route('user.logout') }}" class="nav-link">Выйти</a>
                </li>
            </ul>
            <form id="logout-form" action="{{ route('user.logout') }}" method="post" class="d-none">
                @csrf
            </form>
        </div>
    </nav>

    <div class="row">
        <div class="col-12">
            @if ($message = Session::get('success'))
                <div class="alert alert-success alert-dismissible mt-0" role="alert">
                    <button type="button" class="close" data-dismiss="alert" aria-label="Закрыть">
                        <span aria-hidden="true">&times;</span>
                    </button>
                    {{ $message }}
                </div>
            @endif
            @yield('content')
        </div>
    </div>
</div>
</body>
</html>
@extends('layout.admin')

@section('content')
    <h1>Панель управления</h1>
    <p>Добро пожаловать, {{ auth()->user()->name }}</p>
    <p>Это панель управления для администратора интернет-магазина.</p>
    <form action="{{ route('user.logout') }}" method="post">
        @csrf
        <button type="submit" class="btn btn-primary">Выйти</button>
    </form>
@endsection

В конструкторе контроллера мы используем два посредника — auth и admin. Первый проверяет, что пользователь аутентифицирован, а второй — что пользователь является администратором. Эти имена должны быть определены в классе Kernel (файл app/Http/Kernel.php). Имя auth уже есть, так что добавим admin.

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel {
    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'admin' => \App\Http\Middleware\Administrator::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
    ];
}

И создадим класс посредника Administrator:

> php artisan make:middleware Administrator
namespace App\Http\Middleware;

use Closure;

class Administrator {
    public function handle($request, Closure $next, $guard = null) {
        // если это не администратор — показываем 404 Not Found
        if ( ! auth()->user()->admin) {
            abort(404);
        }
        return $next($request);
    }
}

Обычный пользователь после входа направляется на страницу /user/index. Но администратора мы должны отправить на страницу /admin/index. Мы можем это сделать в контроллере LoginController.

class LoginController extends Controller {
    /**
     * Сразу после входа выполняем редирект и устанавливаем flash-сообщение
     */
    protected function authenticated(Request $request, $user) {
        $route = 'user.index';
        $message = 'Вы успешно вошли в личный кабинет';
        if ($user->admin) {
            $route = 'admin.index';
            $message = 'Вы успешно вошли в панель управления';
        }
        return redirect()->route($route)
            ->with('success', $message);
    }
}

Для панели управления нам потребуется несколько контроллеров. Неудобно в конструкторе каждого из них прописывать два посредника auth и admin. Давайте уберем конструктор в контроллере Admin\IndexController, а посредники пропишем в маршруте.

// первый способ добавления посредников
Route::namespace('Admin')->name('admin.')->prefix('admin')->middleware('auth', 'admin')->group(function () {
    Route::get('index', 'IndexController')->name('index');
});
// второй способ добавления посредников
Route::group([
    'as' => 'admin.', // имя маршрута, например admin.index
    'prefix' => 'admin', // префикс маршрута, например admin/index
    'namespace' => 'Admin', // пространство имен контроллера
    'middleware' => ['auth', 'admin'] // один или несколько посредников
], function () {
    Route::get('index', 'IndexController')->name('index');
});

Товар в корзине

Неудобно, что корзина в правом верхнем углу всегда одинаковая — когда в ней есть товар(ы) и когда она пустая. Давайте это исправим, чтобы корзина с товаром была другого цвета. Для этого в классе ComposerServiceProvider будем передавать в layout-шаблон site.blade.php переменную $positions.

class ComposerServiceProvider extends ServiceProvider {
    /* ... */
    public function boot() {
        View::composer('layout.part.roots', function($view) {
            $view->with(['items' => Category::roots()]);
        });
        View::composer('layout.part.brands', function($view) {
            $view->with(['items' => Brand::popular()]);
        });
        View::composer('layout.site', function($view) {
            $view->with(['positions' => Basket::getBasket()->products->count()]);
        });
    }
}

Для этого в модели Basket нам потребуется метод getBasket(), который будет возвращать объект корзины:

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\Cookie;

class Basket extends Model {
    /* ... */
    public static function getBasket() {
        $basket_id = request()->cookie('basket_id');
        if (!empty($basket_id)) {
            try {
                $basket = Basket::findOrFail($basket_id);
            } catch (ModelNotFoundException $e) {
                $basket = Basket::create();
            }
        } else {
            $basket = Basket::create();
        }
        Cookie::queue('basket_id', $basket->id, 525600);
        return $basket;
    }
}

А в контроллере BasketController метод getBasket() можно удалить, получая объект корзины в конструкторе:

namespace App\Http\Controllers;

use App\Basket;
use Illuminate\Http\Request;

class BasketController extends Controller {

    private $basket;

    public function __construct() {
        $this->basket = Basket::getBasket();
    }
    /* ... */
}
Подозреваю, что это не слишком правильное решение, но мне еще много чего предстоит узнать о Laravel.

Теперь осталось только в layout-шаблоне показать кол-во позиций в корзине и выделить ее цветом:

<ul class="navbar-nav ml-auto">
    <li class="nav-item">
        <a class="nav-link @if ($positions) text-success @endif" href="{{ route('basket.index') }}">
            Корзина
            @if ($positions) ({{ $positions }}) @endif
        </a>
    </li>
    @guest
        <li class="nav-item">
            <a class="nav-link" href="{{ route('user.login') }}">Войти</a>
        </li>
        @if (Route::has('user.register'))
            <li class="nav-item">
                <a class="nav-link" href="{{ route('user.register') }}">Регистрация</a>
            </li>
        @endif
    @else
        <li class="nav-item">
            <a class="nav-link" href="{{ route('user.index') }}">Личный кабинет</a>
        </li>
    @endif
</ul>

Как-то не слишком удачно у меня получилось с корзиной. Каждый раз, когда новый посетитель приходит на сайт — создается новая запись в таблице БД baskets. Хотя в реальности 90% посетителей не станут покупателями. Так что добавил еще один метод getCount() в модель Basket.

class Basket extends Model {
    /**
     * Возвращает количество позиций в корзине
     */
    public static function getCount() {
        $basket_id = request()->cookie('basket_id');
        if (empty($basket_id)) {
            return 0;
        }
        return self::getBasket()->products->count();
    }
}
class ComposerServiceProvider extends ServiceProvider {
    /* ... */
    public function boot() {
        /* ... */
        View::composer('layout.site', function($view) {
            $view->with(['positions' => Basket::getCount()]);
        });
    }
}

Поиск: 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.