Блог на Laravel 7, часть 12. Доп.страницы сайта в панели управления и в публичной части
22.01.2021
Теги: Laravel • MySQL • PHP • Web-разработка • БазаДанных • Блог • ПанельУправления • Практика • СтраницаСайта • Фреймворк • ШаблонСайта
Страницы в панели управления
Хотя у нас простой блог, но может возникнуть необходимость создания страниц сайта — что-то типа «Об этом сайте» или «Размещение рекламы». И у администратора должна быть возможность такие страницы создавать, редактировать и удалять. Давайте создадим еще одну таблицу базы данных pages
и ресурсный контроллер для CRUD-операций над страницами.
> php artisan make:model Page -m
namespace App; use Illuminate\Database\Eloquent\Model; class Page extends Model { protected $fillable = [ 'name', 'slug', 'content', 'parent_id', ]; /** * Связь «один ко многим» таблицы `pages` с таблицей `pages` * * @return \Illuminate\Database\Eloquent\Relations\HasMany */ public function children() { return $this->hasMany(Page::class, 'parent_id'); } /** * Связь «страница принадлежит» таблицы `pages` с таблицей `pages` * * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function parent() { return $this->belongsTo(Page::class); } }
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreatePagesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('pages', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('parent_id')->nullable(); $table->string('name', 100)->nullable(false); $table->text('content')->nullable(false); $table->string('slug', 100)->unique()->nullable(false); $table->timestamps(); // внешний ключ, ссылается на поле id таблицы pages $table->foreign('parent_id') ->references('id') ->on('pages') ->nullOnDelete();; }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('pages'); } }
> php artisan migrate
Создаем ресурсный контроллер:
> php artisan make:controller Admin/PageController --resource --model=Page
namespace App\Http\Controllers\Admin; use App\Helpers\ImageUploader; use App\Http\Controllers\Controller; use App\Page; use Illuminate\Http\Request; class PageController extends Controller { public function __construct() { $this->middleware('perm:manage-pages')->only('index'); $this->middleware('perm:create-page')->only(['create', 'store']); $this->middleware('perm:edit-page')->only(['edit', 'update']); $this->middleware('perm:delete-page')->only('destroy'); } /** * Показывает список всех страниц */ public function index() { $roots = Page::whereNull('parent_id')->with('children')->get(); return view('admin.page.index', compact('roots')); } /** * Показывает форму для создания страницы */ public function create() { $parents = Page::whereNull('parent_id')->get(); return view('admin.page.create', compact('parents')); } /** * Сохраняет новую страницу в базу данных */ public function store(Request $request) { $this->validate($request, [ 'name' => 'required|max:100', 'parent_id' => 'numeric|nullable', 'slug' => 'required|max:100|unique:pages|regex:~^[-_a-z0-9]+$~i', 'content' => 'required', ]); Page::create($request->all()); return redirect() ->route('admin.page.index') ->with('success', 'Новая страница успешно создана'); } /** * Показывает форму для редактирования страницы */ public function edit(Page $page) { $parents = Page::whereNull('parent_id')->where('id', '<>', $page->id)->get(); return view('admin.page.edit', compact('page', 'parents')); } /** * Обновляет страницу (запись в таблице БД) */ public function update(Request $request, Page $page) { $this->validate($request, [ 'name' => 'required|max:100', 'parent_id' => 'numeric|not_in:'.$page->id.'|nullable', // обязательное, не больше 100 символов, уникальное (без учета slug этой // записи таблицы pages) и содержать буквы, цифры, дефис и подчеркивание 'slug' => 'required|max:100|unique:pages,slug,'.$page->id.',id|regex:~^[-_a-z0-9]+$~i', 'content' => 'required', ]); $page->update($request->all()); return redirect() ->route('admin.page.index') ->with('success', 'Страница была успешно отредактирована'); } /** * Удаляет страницу (запись в таблице БД) */ public function destroy(Page $page, ImageUploader $imageUploader) { if ($page->children->count()) { return back()->withErrors('Нельзя удалить страницу, у которой есть дочерние'); } $imageUploader->destroy($page->content); $page->delete(); return redirect() ->route('admin.page.index') ->with('success', 'Страница сайта успешно удалена'); } }
Добавляем маршруты в файл web.php
:
/* * Панель управления: CRUD-операции над постами, категориями, тегами */ Route::group([ 'as' => 'admin.', // имя маршрута, например admin.index 'prefix' => 'admin', // префикс маршрута, например admin/index 'namespace' => 'Admin', // пространство имен контроллеров 'middleware' => ['auth'] // один или несколько посредников ], function () { /* * CRUD-операции над страницами */ Route::resource('page', 'PageController', ['except' => 'show']); });
Добавляем свойство $fillable
в модель, чтобы использовать «mass assignment» и настраиваем связи между таблицами:
namespace App; use Illuminate\Database\Eloquent\Model; class Page extends Model { protected $fillable = [ 'name', 'slug', 'content', 'parent_id', ]; /** * Связь «один ко многим» таблицы `pages` с таблицей `pages` * * @return \Illuminate\Database\Eloquent\Relations\HasMany */ public function children() { return $this->hasMany(Page::class, 'parent_id'); } /** * Связь «страница принадлежит» таблицы `pages` с таблицей `pages` * * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function parent() { return $this->belongsTo(Page::class); } }
Шаблон views/admin/page/index.blade.php
для просмотра списка страниц:
@extends('layout.admin', ['title' => 'Все страницы']) @section('content') <h1 class="mb-3">Все страницы</h1> @perm('create-page') <a href="{{ route('admin.page.create') }}" class="btn btn-success mb-4"> Создать страницу </a> @endperm @if ($roots->count()) <table class="table table-bordered"> <tr> <th>#</th> <th width="45%">Наименование</th> <th width="45%">ЧПУ (англ.)</th> <th><i class="fas fa-edit"></i></th> <th><i class="fas fa-trash-alt"></i></th> </tr> @foreach ($roots as $root) <tr> <td>{{ $root->id }}</td> <td><strong>{{ $root->name }}</strong></td> <td>{{ $root->slug }}</td> <td> @perm('edit-page') <a href="{{ route('admin.page.edit', ['page' => $root->id]) }}"> <i class="far fa-edit"></i> </a> @endperm </td> <td> @perm('delete-page') <form action="{{ route('admin.page.destroy', ['page' => $root->id]) }}" method="post" onsubmit="return confirm('Удалить эту страницу?')"> @csrf @method('DELETE') <button type="submit" class="m-0 p-0 border-0 bg-transparent"> <i class="far fa-trash-alt text-danger"></i> </button> </form> @endperm </td> </tr> @foreach ($root->children as $child) <tr> <td>{{ $child->id }}</td> <td>— {{ $child->name }}</td> <td>{{ $child->slug }}</td> <td> @perm('edit-page') <a href="{{ route('admin.page.edit', ['page' => $child->id]) }}"> <i class="far fa-edit"></i> </a> @endperm </td> <td> @perm('delete-page') <form action="{{ route('admin.page.destroy', ['page' => $child->id]) }}" method="post" onsubmit="return confirm('Удалить эту страницу?')"> @csrf @method('DELETE') <button type="submit" class="m-0 p-0 border-0 bg-transparent"> <i class="far fa-trash-alt text-danger"></i> </button> </form> @endperm </td> </tr> @endforeach @endforeach </table> @endif @endsection
Шаблон views/admin/page/create.blade.php
для создания новой страницы:
@extends('layout.admin', ['title' => 'Создание новой страницы']) @section('content') <h1>Создание новой страницы</h1> <form method="post" action="{{ route('admin.page.store') }}"> @include('admin.page.form') </form> @endsection
Шаблон views/admin/page/edit.blade.php
для редактирования страницы:
@extends('layout.admin', ['title' => 'Редактирование страницы']) @section('content') <h1>Редактирование страницы</h1> <form method="post" action="{{ route('admin.page.update', ['page' => $page->id]) }}"> @method('PUT') @include('admin.page.form') </form> @endsection
Шаблон views/admin/page/form.blade.php
формы создания-редактирования:
@csrf <div class="form-group"> <input type="text" class="form-control" name="name" placeholder="Наименование" required maxlength="100" value="{{ old('name') ?? $page->name ?? '' }}"> </div> <div class="form-group"> <input type="text" class="form-control" name="slug" placeholder="ЧПУ (на англ.)" required maxlength="100" value="{{ old('slug') ?? $page->slug ?? '' }}"> </div> <div class="form-group"> @php $parent_id = old('parent_id') ?? $page->parent_id ?? null; @endphp <select name="parent_id" class="form-control" title="Родитель"> <option value="0">Без родителя</option> @foreach($parents as $parent) <option value="{{ $parent->id }}" @if ($parent->id === $parent_id) selected @endif> {{ $parent->name }} </option> @endforeach </select> </div> <div class="form-group"> <textarea class="form-control" name="content" placeholder="Контент (html)" required rows="10">{{ old('content') ?? $page->content ?? '' }}</textarea> </div> <div class="form-group"> <button type="submit" class="btn btn-primary">Сохранить</button> </div>
При выборе родителя для страницы нам приходится работать с null
-значениями. У страницы может не быть родителя, в этом случае parent_id
равно null
. По умолчанию Laravel содержит посредники TrimStrings
и ConvertEmptyStringsToNull
, они перечислены в классе App\Http\Kernel
. Чтобы валидатор не считал значение null
как некорректное, мы добавляем правило валидации nullable
.
Заполнение БД данными
Надо наполнить таблицу базы данных pages
данными, то есть создать несколько страниц сайта.
> php artisan make:seeder PageTableSeeder Seeder created successfully.
use Illuminate\Database\Seeder; class PageTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { // создать 5 страниц factory(App\Page::class, 5)->create(); } }
> php artisan make:factory PageFactory --model=Page Factory created successfully.
use App\Page; use Faker\Generator as Faker; use Illuminate\Support\Str; $factory->define(Page::class, function (Faker $faker) { $name = $faker->realText(rand(20, 30)); $content = '<p>' . $faker->realText(rand(400, 500)) . '</p>' . '<p>' . $faker->realText(rand(400, 500)) . '</p>' . '<p>' . $faker->realText(rand(400, 500)) . '</p>'; return [ 'name' => $name, 'content' => $content, 'slug' => Str::slug($name) . '-' . rand(100, 999), ]; });
И вносим изменения в класс DatabaseSeeder
:
use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { /** * Seed the application's database. * * @return void */ public function run() { $this->call(UserTableSeeder::class); $this->command->info('Таблица пользователей загружена данными!'); /* ..... */ $this->call(PageTableSeeder::class); $this->command->info('Таблица страниц загружена данными!'); } }
Кроме того, нам еще нужно добавить права доступа для управления страницами сайта:
use Illuminate\Database\Seeder; class PermissionTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { $permissions = [ ['slug' => 'manage-users', 'name' => 'Управление пользователями'], ['slug' => 'create-user', 'name' => 'Создание пользователя'], ['slug' => 'edit-user', 'name' => 'Редактирование пользователя'], ['slug' => 'delete-user', 'name' => 'Удаление пользователя'], /* ...все прочие права... */ ['slug' => 'manage-pages', 'name' => 'Управление страницами сайта'], ['slug' => 'create-page', 'name' => 'Создание страницы сайта'], ['slug' => 'edit-page', 'name' => 'Редактирование страницы сайта'], ['slug' => 'delete-page', 'name' => 'Удаление страницы сайта'], ]; foreach ($permissions as $item) { $permission = new App\Permission(); $permission->name = $item['name']; $permission->slug = $item['slug']; $permission->save(); } } }
Теперь можно заново заполнить базу данных тестовыми данными:
> php artisan migrate:fresh --seed
Страницы в публичной части
В панели управления для работы со страницами все готово, теперь нужно организовать просмотр этих страниц в публичной части. И нужно добавить в левую колонку список всех страниц сайта для возможности навигации.
Создаем контроллер для показа страницы:
> php artisan make:controller PageController --invokable
namespace App\Http\Controllers; use App\Page; class PageController extends Controller { public function __invoke(Page $page) { return view('page.show', compact('page')); } }
Добавляем новый маршрут в файл routes/web.php
:
/* * Страницы сайта с доп.информацией */ Route::get('page/{page:slug}', 'PageController')->name('page');
Создаем шаблон resource/views/page/show.blade.php
для показа страницы:
@extends('layout.site', ['title' => $page->name]) @section('content') <h1>{{ $page->name }}</h1> {!! $page->content !!} @endsection
Создадем шаблон resource/views/layout/part/all-pages.blade.php
для показа списка страниц в левой колонке:
<ul> @foreach($pages as $page) <li> <a href="{{ route('page', ['page' => $page->slug]) }}">{{ $page->name }}</a> @if ($page->children->count()) <ul> @foreach($page->children as $child) <li> <a href="{{ route('page', ['page' => $child->slug]) }}">{{ $child->name }}</a> </li> @endforeach </ul> @endif </li> @endforeach </ul>
Передадим в это шаблон коллекцию всех страниц верхнего уровня. Поскольку нам надо показывать и дочерние страницы — используем жадную загрузку, чтобы уменьшить кол-во SQL-запросов к базе данных.
class ComposerServiceProvider extends ServiceProvider { /* ... */ public function register() { /* ... */ View::composer('layout.part.all-pages', function($view) { $view->with(['pages' => Page::whereNull('parent_id')->with('children')->get()]); }); } /* ... */ }
В layout-шаблоне resource/views/layout/site.blade.php
подключим этот шаблон в левой колонке:
<div class="row"> <div class="col-md-3"> <h4>Категории блога</h4> @include('layout.part.categories', ['parent' => 0]) <h4>Популярные теги</h4> @include('layout.part.popular-tags') <h4>Доп.информация</h4> @include('layout.part.all-pages') </div> <!-- ..... --> </div>
Небольшое отступление
Прежде, чем двигаться дальше, давайте посмотрим, как в Laravel организовано хранение и загрузка файлов. Это нам потребуется, чтобы организовать загрузку, хранение и удаление изображений для постов и категорий.
Файловое хранилище
Сразу после установки Laravel доступны диски local
и public
, использующие драйвер local
. Для диска local
место хранения — директория storage/app
, для диска public
место хранения — директория storage/app/public
. Диск local
является диском по умолчанию, это задается в файле настроек config/filesystems.php
.
return [ /* ... */ 'default' => env('FILESYSTEM_DRIVER', 'local'), /* ... */ 'disks' => [ 'local' => [ 'driver' => 'local', 'root' => storage_path('app'), ], 'public' => [ 'driver' => 'local', 'root' => storage_path('app/public'), 'url' => env('APP_URL').'/storage', 'visibility' => 'public', ], ], /* ... */ 'links' => [ public_path('storage') => storage_path('app/public'), ], /* ... */ ];
Чтобы сделать файлы диска public
доступными через веб, надо создать символьную ссылку из public/storage
на storage/app/public
. Директория public
проекта Laravel — является корневой директорией сервера, поэтому файл storage/app/public/image.jpg
будет доступен через веб как http://server.com/storage/image.jpg
.
> php artisan storage:link
Какие символьные ссылки создавать — задается в файле конфигурации, см. выше. Когда файл сохранён на диске и создана символьная ссылка, можно создать URL к файлу с помощью хелпера asset()
или метода url()
фасада Storage
:
<img src="{{ asset('storage/images/image.jpg') }}" alt="" />
<img src="{{ Storage::disk('public')->url('images/image.jpg') }}" alt="" />
При использовании диска local
при вызове метода Storage::url()
будет возвращен URL вида /storage/images/image.jpg
.
При использовании драйвера local
все файловые операции выполняются относительно директории root
, определенной в конфигурационном файле. Для диска local
директория root
— это storage/app
, для диска public
директория root
— это storage/app/public
.
// файл будет сохранен в storage/app/data/file.txt Storage::disk('local')->put('data/file.txt', 'Some file content');
Загрузка файлов
В Laravel очень просто сохранять загружаемые файлы методом store()
на экземпляре загружаемого файла:
namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Http\Controllers\Controller; class UserAvatarController extends Controller { /** * Обновление аватара пользователя. */ public function update(Request $request) { // будет сохранен как storage/app/avatars/L6ceL...xzXFw.jpeg $path = $request->file('avatar')->store('avatars'); return $path; } }
Мы указываем только директорию avatars
, а имя файла будет сформировано автоматически. Метод вернёт путь к файлу, поэтому можно сохранить в БД весь путь, включая сгенерированное имя. Файл будет сохранен на диск по умолчанию (и не будет доступен из веб), но можно указать диск вторым аргументом метода store()
.
// будет сохранен как storage/app/public/avatars/L6ceL...xzXFw.jpeg и будет // доступен из веб как http://server.com/storage/avatars/L6ceL...xzXFw.jpeg $path = $request->file('avatar')->store('avatars', 'public');
Чтобы задать свое имя файла и (опционально) диск для сохранения, можно использовать метод storeAs()
:
// будет использован диск по умолчанию $path = $request->file('avatar')->storeAs( 'avatars', // директория, куда сохранять $request->user()->id // имя файла );
// явное указание диска для сохранения $path = $request->file('avatar')->storeAs( 'avatars', // директория, куда сохранять $request->user()->id, // имя файла 'public' // диск, куда сохранять );
- Блог на Laravel 7, часть 10. Личный кабинет — CRUD-операции над постами и комментариями
- Блог на Laravel 7, часть 9. Панель управления — создание, публикация, удаление комментариев
- Блог на Laravel 7, часть 8. Панель управления — CRUD для категорий, тегов и пользователей
- Блог на Laravel 7, часть 7. Панель управления — создание, публикация, удаление постов
- Блог на Laravel 7, часть 16. Роль нового пользователя, сообщение админу о новом посте
- Блог на Laravel 7, часть 11. Панель управления — назначение ролей и прав для пользователей
- Блог на Laravel 7, часть 6. Публичная часть — все посты, посты категории, посты автора
Поиск: Laravel • MySQL • PHP • Web-разработка • База данных • Блог • Панель управления • Практика • Страница сайта • Фреймворк • Шаблон сайта