Laravel: разбираем основные понятия. Часть третья: «Заключительная»

    Считанные дни остаются до старта нового курса от OTUS — «Framework Laravel». В преддверии старта курса делимся заключительной частью авторской публикации о основных понятиях в Laravel. Важно: данная серия публикаций не имеет отношения к образовательной программе курса и является небольшим полезным материалом для новичков. С программой курса можно ознакомиться тут.




    В прошлой статье мы с вами начали писать галерею, в которой пользователь может авторизоваться и регистрироваться, создавать альбомы с описанием, обложкой и названием. В созданном приложении есть первичное значение нашего view и теперь необходимо его немного расширить.
    Также, уже имеющийся код стоит немного улучшить:

    У нас уже есть два контроллера — AlbumController и ImageController. Добавим импорт дополнительного класса, который отвечает за работу со строками:

        use Illuminate\Support\Str;   
        

    И поменяем строчку, которая отвечает за генерацию случайного имени изображения в нашей базе:

         $random_name = Str::random(8);
        

    Кроме того, сейчас стоит немного изменить код миграции, чтобы увеличить надежность работы нашего приложения, потому что пользователь должен иметь право оставить пустым поле нашего description (иначе Laravel с динамитом сообщит нам об ошибке). Зайдем в миграцию, связанную с таблицей изображений, и поправим там строчку, связанную с описанием изображения вот на такую:

         $table->string('description')->nullable();
        

    Хорошо, с «рефакторингом» приложения закончено. Далее необходимо доделать до конца внешний вид нашего приложения. Создадим форму, в которой пользователь будет иметь возможность создать свои альбомы:

    файл addimage.blade.php

        @include('includes.header')
          <body>
        @include('includes.nav')
            <div class="container" style="text-align: center;">
              <div class="span4" style="display: inline-block; margin-top:100px;">
                @if (isset($errors) && $errors->has(''))
                  <div class="alert alert-block alert-error fade in"id="error-block">
                   <?php
                    $messages = $errors->all('<li>:message</li>');
                   ?>
        <button type="button" class="close"data-dismiss="alert">×</button>
                    <h4>Warning!</h4>
                    <ul>
                      @foreach($messages as $message)
                        {{$message}}
                      @endforeach
                    </ul>
                  </div>
                @endif
                <form name="createnewalbum" method="POST"action="{{route('create_album')}}" enctype="multipart/form-data">
                  {{ csrf_field() }}
                  <fieldset>
                  <legend>Создайте альбом</legend>
                   <div class="form-group">
                   <label for="name">Имя альбома</label>
         <input name="name" type="text" class="form-control"placeholder="Название альбома" value="{{old('name')}}">
                    </div>
                    <div class="form-group">
                   <label for="description">Описание альбома</label>
                   <textarea name="description" type="text"class="form-control" placeholder="Описание альбома">{{old('descrption')}}</textarea>
                </div>
               <div class="form-group">
            <label for="cover_image">Выберите обложку для альбома</label>
                  {{Form::file('cover_image')}}
                </div>
          <button type="submit" class=«btn btn-default">Создать!</button>
                  </fieldset>
                </form>
              </div>
            </div> 
            <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
            <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0-rc1/js/bootstrap.min.js"></script>
          </body>
        </html>
        

    Напомню, что в includes наверху страницы у нас сложены навигация и используемые библиотеки, которые были описаны в прошлой части.

    Кроме того, нам нужно добавить внешний вид album.blade.php

        @include('includes.header')
          <body>
          @include('includes.nav')
            <div class="container">
           <div class="starter-template">
                <div class="media">
                  <img class="media-object pull-left"alt="{{$album->name}}" src="/albums/{{$album->cover_image}}" width="350px">
                  <div class="media-body">
                    <h2 class="media-heading" style="font-size: 26px;">Название альбома:</h2>
                    <p>{{$album->name}}</p>
                  <div class="media">
                  <h2 class="media-heading" style="font-size: 26px;">Описание альбома :</h2>
                  <p>{{$album->description}}<p>
                  <a href="{{route('add_image',array('id'=>$album->id))}}"><button type="button"class="btn btn-primary btn-large">Добавить новую фотографию в альбом</button></a>
                  <a href="{{route('delete_album',array('id'=>$album->id))}}" onclick="return confirm('Вы уверены?')"><button type="button"class="btn btn-danger btn-large">Удалить альбом</button></a>
                </div>
              </div>
            </div>
            </div>
            <div class="row">
                @foreach($album->Photos as $photo)
                  <div class="col-lg-3">
                    <div class="thumbnail" style="max-height: 350px;min-height: 350px;">
                      <img alt="{{$album->name}}" src="/albums/{{$photo->image}}">
                      <div class="caption">
                        <p>{{$photo->description}}</p>
                      
                        <p>Дата создания:  {{ date("d F Y",strtotime($photo->created_at)) }}at {{ date("g:ha",strtotime($photo->created_at)) }}</p>
                        <a href="{{route('delete_image',array('id'=>$photo->id))}}" onclick="returnconfirm('Вы уверены?')"><button type="button"class="btn btn-danger btn-small">Удалить изображение</button></a>
                        <p>Переместить фотографию в другой альбом :</p>
                        <form name="movephoto" method="POST"action="{{route('move_image')}}">
                            {{ csrf_field() }}
                          <select name="new_album">
                            @foreach($albums as $others)
                              <option value="{{$others->id}}">{{$others->name}}</option>
                            @endforeach
                          </select>
                          <input type="hidden" name="photo"value="{{$photo->id}}" />
                          <button type="submit" class="btn btn-smallbtn-info" onclick="return confirm('Вы уверены?')">Перемещение изображения</button>
                        </form>
                      </div>
                    </div>
                  </div>
              @endforeach
            </div>
            </div>
            <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0-rc1/js/bootstrap.min.js"></script>
            <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
          </body>
        </html>
        

    Кроме того, мы можем немного обновить наш роутинг, например, в nav.blade.php:

         <li><a href="{{URL::route('create_album_form')}}">Создание нового альбома</a></li>
        

    На более новый и модный вариант:

        <li><a href="{{route('create_album_form')}}">Создание нового альбома</a></li>
        

    С другой стороны, на 6 Laravel работала и предыдущая версия. Итак, у нас уже есть форма, в которой пользователь может добавлять фотографии в альбом, и есть сам основной view нашего альбома. Дальше нам потребуется добавить контролер, который будут отвечать за обработку отдельной фотографии.

    Итак, создаем контроллер, отвечающий за работу с отдельными фотографиями:

        php artisan make:controller ImageController
        

    Итак, что у нас должно быть внутри:

        <?php
        
        namespace App\Http\Controllers;
        
        use Illuminate\Http\Request;
        use Illuminate\Support\MessageBag;
        use Validator; // мы будем проводить валидизацию наших фотографий
        use App\Album;
        use App\Image;
        use Illuminate\Support\Str;
        
        class ImageController extends Controller
        {
            public function getForm($id)
          {
            $album = Album::find($id);
            return view('addimage')
            ->with('album',$album);
            //нам нужна форма, в которой пользователь сможеть добавить фотографию в свой альбом
          }
    
          public function postAdd(Request $request)
          {
              //здесь у нас есть правила, по которым у нас проиходит валидизация
            $rules = [
              'album_id' => 'required|numeric|exists:albums,id',
              'image'=>'required|image'
            ];
        
            $input = ['album_id' => null];
        
            $validator = Validator::make($request->all(), $rules);
            if($validator->fails()){
                return redirect()->route('add_image', ['id' => $request->get('album_id')])->withErrors($validator)->withInput();
            }
        
            $file = $request->file('image');
            //произошел рефакторинг кода
            $random_name = Str::random(8);
            //конец рефакторинга
            $destinationPath = 'albums/';
            $extension = $file->getClientOriginalExtension();
            $filename=$random_name.'_album_image.'.$extension;
            $uploadSuccess = $request->file('image')->move($destinationPath, $filename);
            Image::create(array(
              'description' => $request->get('description'), //описание одной фотографии
              'image' => $filename, // отдельное изображение
              'album_id'=> $request->get('album_id')  // к какому именно альбому имеет отношение наша фотография
            ));
        
            return redirect()->route('show_album',['id'=>$request->get('album_id')]);
          }
          public function getDelete($id)
          //так же нам нужна возможность удаления изображения
          {
            $image = Image::find($id);
            $image->delete();
            return redirect()->route('show_album',['id'=>$image->album_id]);
          }
        
          //так же фотографии можно перемещать между альбомами
          public function postMove(Request $request)
        {
          $rules = array(
        
            'new_album' => 'required|numeric|exists:albums,id',
            'photo'=>'required|numeric|exists:images,id'
        
          );
          //здесь у нас валидизация
          $validator = Validator::make($request->all(), $rules);
        
          if($validator->fails()){
        
            return redirect()->route('index');
          }
        
          $image = Image::find($request->get('photo'));
          $image->album_id = $request->get('new_album');
          $image->save();
          return redirect()->route('show_album', ['id'=>$request->get('new_album')]);
        }
        }
        



    Регистрация и авторизация


    На данный момент наше приложение практически готово. Однако нам нужно еще прикрутить возможность регистрироваться в нашем приложении, выходить, и входить в уже существующий аккаунт.

    В прошлой части мы уже показывали, как у нас выглядит наш includes/nav.blade.php. Т.к. мы хотим, что бы у нас появились кнопки Войти и Зарегистрироваться, когда пользователь у нас еще не зарегистрировался, и кнопка Выйти, когда он у нас зарегистрируется.

    Итак, вот так теперь у нас выглядит список в includes/nav.blade.php:

          <ul class="nav navbar-nav">
               @if (Auth::check())
                <li>
           <button type="button" class="btn btn-primary">
                <!-- если пользователь зарегистрирован, тогда ->отобразить его имя -->
                {{{ Auth::user()->name}}}
                </button>
                </li>
                  <li><a href="{{route('create_album_form')}}">Создание нового альбома</a></li>
               @else
               <!-- в другом случае -->
                  <li><a href="{{route('register')}}">Регистрация</a></li>
                  <li><a href="{{route('login')}}">Войти</a></li>
                        @endif
                  @if (Auth::check())
                  <!-- если зарегистрирован, тогда нам нужно показать как выйти -->
                  <!-- это можно сделать через форму -->
                    <li>
                    <a href="{{url('/logout') }}"
                      onclick="event.preventDefault();
                      document.getElementById('logout-form').submit();">
                      Выйти </a>
                    <form id="logout-form" action="{{ url('/logout') }}"
                      method="POST" style="display: none;">
                        {{ csrf_field() }}
                    </form>
                   </li>
                    @endif
                </ul>
        

    Используя конструкцию Auth::check и элементарный if else, мы можем выстроить изменения в нашем view, в зависимости от того, зарегистрирован наш пользователь или нет.

    Однако с изменениями view до создания модели и контроллера мы немного бежим вперед паровоза. Для того, что бы сделать нашу аутентификацию, мы воспользуемся всеми готовыми и быстрыми решениями, которые есть в Laravel 6.

    Итак, по поводу модели User — мы воспользуемся тем, который поставляется в коробке. С одной стороны он явно недостаточен для полноценного приложения, с другой — я не хочу ограничивать себя рамками одного конкретного приложения. Какого именно рода галерею вы хотите? Где пользователь может сам создавать свои приватные альбомы, которые никому не доступны? Или где все выкладывают свои фотографии в общий доступ? У нас не будет ограничений в нашем приложении — я думаю, читатель сможет сам без проблем их доделать на свой вкус и цвет.

    Итак, в Laravel 6 создавать аутентификации стало еще проще. Идем в командную строку нашего приложения:

        composer require laravel/ui —dev
        

    Что дает нам необходимые пакеты. Теперь создаем:

        php artisan ui:auth
        

    Что нам дает эта команда? Во-первых, у нас в resources/views появилась новая папка auth, в которой у нас сложены все views, которые могут понадобиться для авторизации пользователя и его регистрации: login, register и verify. Там же ещё есть подпапка passwords, посвященная сбросу и восстановлению паролей. В папке Http/Controllers у нас появился HomeController, который нужен для редиректа на домашнюю страницу после регистрации пользователя. Так же у нас появится подпапка layouts, в которой будет находится app.blade.php, которым мы сегодня не будем разбирать, но он может стать отличным основанием для приложения, созданного с нуля.

    Сгенерированные blade-шаблоны уже имеют первоначальную верстку, подстроенную под Bootstrap. Он у нас уже подключен в header.blade.php.
    Кроме того, у нас обновился немного роутинг в routes/web.php. Добавились следующие строчки:

        Auth::routes();
        
        Route::get('/home', 'HomeController@index')->name('home');
        
        Route::post('/logout', 'Auth\LoginController@logout')->name('logout');
        

    Путь на home генерируется автоматически, а редирект на logout я сделал сам.

    Что бы у вас заработала верстка на генерируемых шаблонах, подключим к ним наш nav и header:

        @include('includes.header')
          <body>
          @include('includes.nav')
        @extends('layouts.app')
        

    Отлично, теперь все готово. Попробуйте зарегистрироваться. Подтверждение пароля тоже должно работать:





    В дальнейшем пользователь может выбрать на какую страницу сервиса ему хочется перейти (или читатель может самостоятельно сделать редирект, сложного мало). Нам же осталось поправить маленькую деталь: сделать так, что бы только зарегистрированные и авторизированные пользователи могли видеть наши драгоценные альбомы.

    Для этого мы добавляем в наш роутинг условие — что бы пользователь авторизовался для доступа к альбомам.

    Обновляем наш web.php:

        Route::get('/', array('as' => 'index','uses' => 'AlbumsController@getList')) ->middleware('auth');;
        //middleware позволяют нам делать фильтрацию http запросов на странице
        Route::get('/createalbum', array('as' => 'create_album_form','uses' => 'AlbumsController@getForm')) ->middleware('auth');;
        //создание альбома из формы
        Route::post('/createalbum', array('as' => 'create_album','uses' => 'AlbumsController@postCreate')) ->middleware('auth');;
        //само создание альбома
        Route::get('/deletealbum/{id}', array('as' => 'delete_album','uses' => 'AlbumsController@getDelete')) ->middleware('auth');;
        // удаление альбома
        Route::get('/album/{id}', array('as' => 'show_album','uses' => 'AlbumsController@getAlbum')) ->middleware('auth');;
        // удаление альбома
        

    В принципе, на этом этапе мы уже можем заканчивать. Я оставлю на усмотрение читателя, какое приложение он хочет сделать: с приватными альбомами или с публичными. Чисто теоретически приложением уже можно пользоваться, если отпилить возможность регистрации и создавать пользователя отдельно напрямую в базе данных — например, если вы с коллегами собираетесь организовать свои приватные альбомы и почему-то не хотите пользоваться другими сервисами.

    Всем спасибо за внимание! Как всегда, полезные ссылки:

    OTUS. Онлайн-образование
    641,08
    Цифровые навыки от ведущих экспертов
    Поделиться публикацией

    Комментарии 3

      +2
      Почему Вы не использовали запрос с валидацией (php artisan make:request)?
      Ну и код бы причесать — убрать копипасту вида $request->get('album_id'), добавить phpDoc, проверить результат перемещения файла, определиться с неймингом переменных — сейчас есть и camelCase и snake_case ($destinationPath и $random_name).
        +2
        Да, автору бы в первую очередь ознакомиться с PSR по части code style (уж если сам Laravel этим стандартам следует). Сложновато читать кашу из символов и отступов, поставленных непонятно по какому алгоритму :)
        +1
        Что сразу бросается в глаза
        1. @if (Auth::check()) ... @endif можно смело заменить на @auth ... @endauth и @guest ... @endguest

        2. Роуты можно группировать, используя общую middleware
        Route::group([ 'middleware' => 'auth'], function () {
        Route::get('/createalbum', array('as' => 'create_album_form','uses' => 'AlbumsController@getForm'));
        Route::post('/createalbum', array('as' => 'create_album','uses' => 'AlbumsController@postCreate'));
        });


        3. Вы до сих пор используете include в шаблонах, вместо наследования. С наследованием удобнее.

        4. Очень удобная штука Laravel Collective. Кстати, в нее можно передавать модель, а не отдельные поля формы.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое