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

php artisan make:migration added_cafes_files --create=cafes_photos

Наша миграция должна выглядеть так:

/**
 * Run the migrations.
 *
 * @return void
 */
public function up()
{
    Schema::create('cafes_photos', function (Blueprint $table) {
        $table->increments('id');
        $table->integer('cafe_id')->unsigned();
        $table->foreign('cafe_id')->references('id')->on('cafes');
        $table->integer('uploaded_by')->unsigned();
        $table->foreign('uploaded_by')->references('id')->on('users');
        $table->text('file_url');
        $table->timestamps();
    });
}

/**
 * Reverse the migrations.
 *
 * @return void
 */
public function down()
{
    Schema::dropIfExists('cafes_photos');
}

В нашей миграции мы разрешили привязать загруженную фотографию к кафе и привязать к пользователю, загружающему файл. Теперь мы можем красноречиво использовать эти отношения на стороне API.

Шаг 2. Добавьте красноречивые отношения

Давайте быстро добавим эти отношения, прежде чем мы перейдем к интерфейсу, который будет составлять основу статьи.

Сначала добавьте следующий файл: app/Models/CafePhoto.php. В файл добавляем код:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class CafePhoto extends Model
{
    protected $table = 'cafes_photos';

  public function cafe(){
    return $this->belongsTo('App\Models\Cafe', 'cafe_id', 'id');
  }

  public function user(){
    return $this->belongsTo('App\Models\User', 'uploaded_by', 'id');
  }
}

Это позволяет нам связать фотографию с кафе и пользователем, который ее загрузил.

Во-вторых, откройте /app/Http/Models/Cafe.php и добавьте следующие отношения:

public function photos(){
  return $this->hasMany( 'App\Models\CafePhoto', 'id', 'cafe_id' );
}

Это говорит о том, что в кафе много фотографий, и мы можем вызвать запись, чтобы получить информацию о фотографии.

В-третьих, откройте /app/Http/Models/User.php и добавьте следующие отношения:

public function cafePhotos(){
  return $this->hasMany( 'App\Models\CafePhoto', 'id', 'cafe_id' );
}

Эта взаимосвязь означает, что у пользователя есть много загруженных ими фотографий кафе.

Шаг 3: Создайте файловую папку

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

А пока просто создайте следующий каталог: app/Photos. Мы будем группировать файлы в папки по идентификатору кафе. Это будет обработано при загрузке.

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

Шаг 4. Настройте вызовы API для добавления и редактирования кафе для обработки файлов

Есть два URL-адреса, которые необходимо настроить прямо сейчас, чтобы обрабатывать загрузку файлов. Это добавление и редактирование маршрутов кафе: POST /api/v1/cafes & PUT /api/v1/cafes/{cafeID}.

Для этого нам нужно открыть /resources/assets/js/api/cafe.js и внести несколько изменений в способ отправки этих запросов. Как и в случае с любым запросом формы, который включает файлы, нам нужно убедиться, что у нас установлены правильные заголовки, и в этом случае мы будем использовать объект FormData, который позволит нам отправлять файлы с правильным заголовком. Для получения дополнительной информации о FormData посетите: FormData - веб-API | МДН .

Во-первых, давайте изменим метод postAddNewCafe, чтобы он использовал FormData. Нам нужно инициализировать пустой объект:

/*
  Initialize the form data
*/
let formData = new FormData();

Здесь мы будем добавлять все наши данные, которые будут передаваться в API. Теперь в нашем запросе вторым параметром, который мы передаем методу axios.post(), был объект, содержащий данные, которые мы отправляем для добавления кафе:

{
  name: name,
  locations: locations,
  website: website,
  description: description,
  roaster: roaster
}

Теперь мы создадим наш объект formData, содержащий следующие поля:

formData.append('name', name);
formData.append('locations', locations);
formData.append('website', website);
formData.append('description', description);
formData.append('roaster', roaster);

а затем передайте объект данных формы в качестве второго параметра запроса axios.post() следующим образом:

return axios.post( ROAST_CONFIG.API_URL + '/cafes',
  formData
);

Теперь нам нужно добавить дополнительный параметр к нашему методу postAddNewCafe(), который позволит передать изображение кафе вместе с остальными данными.

Сигнатура нашего метода должна выглядеть так:

/*
  POST  /api/v1/cafes
*/
postAddNewCafe: function( name, locations, website, description, roaster, picture )

Затем нам нужно добавить изображение к данным формы следующим образом:

formData.append('picture', picture);

Теперь изображение будет добавлено в форму запроса. Нам нужно сделать только одно - убедиться, что у нас есть право header быть переданным. К счастью, axios.post() принимает третий параметр, который является объектом, который может преобразовывать запрос и добавлять дополнительные заголовки. Итак, для третьего параметра мы должны добавить следующий код:

{
  headers: {
      'Content-Type': 'multipart/form-data'
  }
}

Наш новый метод postAddNewCafe() должен выглядеть так:

/*
  POST  /api/v1/cafes
*/
postAddNewCafe: function( name, locations, website, description, roaster, picture ){
  /*
    Initialize the form data
  */
  let formData = new FormData();

  /*
    Add the form data we need to submit
  */
  formData.append('name', name);
  formData.append('locations', JSON.stringify( locations ) );
  formData.append('website', website);
  formData.append('description', description);
  formData.append('roaster', roaster);
  formData.append('picture', picture);

  return axios.post( ROAST_CONFIG.API_URL + '/cafes',
    formData,
    {
      headers: {
          'Content-Type': 'multipart/form-data'
      }
    }
  );
},

Теперь мы используем formData() для передачи данных в запрос API и устанавливаем заголовки для приема изображения для кафе. Обратите внимание, что мы сохранили те же ключи и имена значений, что и раньше, поэтому наш API будет правильно их принимать. Единственная разница в том, что мы сделали JSON.stringify() на locations. Это сделано для того, чтобы он передавался как JSON, а не как объект javascript, иначе наш API запутается.

Мы обновим наш putEditCafe(), чтобы он работал аналогично:

/*
  PUT   /api/v1/cafes/{id}
*/
putEditCafe: function( id, name, locations, website, description, roaster, picture ){
  /*
    Initialize the form data
  */
  let formData = new FormData();

  /*
    Add the form data we need to submit
  */
  formData.append('name', name);
  formData.append('locations', JSON.stringify( locations ) );
  formData.append('website', website);
  formData.append('description', description);
  formData.append('roaster', roaster);
  formData.append('picture', picture);

  return axios.put( ROAST_CONFIG.API_URL + '/cafes/'+id,
    formData,
    {
      headers: {
          'Content-Type': 'multipart/form-data'
      }
    }
  );
},

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

Шаг 5. Обновите действия Vuex для передачи изображения

Все, что нам нужно сделать для обновления действий, - это добавить параметр изображения к вызову в действии. Если мы откроем наш /resources/assets/js/modules/cafes.js файл и найдем действия addCafe() и editCafe(), мы увидим наш вызов API в каждом методе.

В конце вызова метода добавьте следующий параметр:
data.picture

наше addCafe() действие должно выглядеть так:

/*
  Adds a cafe
*/
addCafe( { commit, state, dispatch }, data ){
  commit( 'setCafeAddedStatus', 1 );

  CafeAPI.postAddNewCafe( data.name, data.locations, data.website, data.description, data.roaster, data.picture )
      .then( function( response ){
        commit( 'setCafeAddedStatus', 2 );
        dispatch( 'loadCafes' );
      })
      .catch( function(){
        commit( 'setCafeAddedStatus', 3 );
      });
},

действие editCafe() должно выглядеть так:

/*
  Edits a cafe
*/
editCafe( { commit, state, dispatch }, data ){
  commit( 'setCafeEditStatus', 1 );

  CafeAPI.putEditCafe( data.id, data.name, data.locations, data.website, data.description, data.roaster, data.picture )
      .then( function( response ){
        commit( 'setCafeEditStatus', 2 );
        dispatch( 'loadCafes' );
      })
      .catch( function(){
        commit( 'setCafeEditStatus', 3 );
      });
},

теперь наши действия будут соответствовать выбранной нами картинке. Наши последние правки должны быть в формах как для редактирования, так и для добавления кафе.

Шаг 6. Обновите формы, чтобы разрешить загрузку изображений

Теперь, когда у нас настроен наш API, и наши запросы готовы обрабатывать загрузку изображения, пришло время разрешить пользователю загружать изображения в кафе. Сделаем это через формы редактирования и добавления для кафе.

Сначала сделаем это на странице /resources/assets/js/pages/NewCafe.vue.

На этой странице мы добавим следующий код шаблона:

<div class="large-12 medium-12 small-12 cell">
  <label>Photo
    <input type="file" id="cafe-photo" ref="photo" v-on:change="handleFileUpload()"/>
  </label>
</div>

Это позволяет нам загружать файл и слушать, когда ввод изменился. Теперь нам просто нужно добавить метод handleFileUpload():

handleFileUpload(){
  this.picture = this.$refs.photo.files[0];
}

Итак, когда наша фотография выбрана, мы устанавливаем локальные данные в файл в первом массиве элемента фотографии. Мы добавили ref=“photo" в наш файл, чтобы мы могли получить к нему доступ из $refs global в нашем элементе VueJS. Теперь мы готовы отправить его, когда добавим кафе:

submitNewCafe(){
  if( this.validateNewCafe() ){
    this.$store.dispatch( 'addCafe', {
      name: this.name,
      locations: this.locations,
      website: this.website,
      description: this.description,
      roaster: this.roaster,
      picture: this.picture
    });
  }
},

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

/*
  Clears the form.
*/
clearForm(){
  this.name = '';
  this.locations = [];
  this.website = '';
  this.description = '';
  this.roaster = false;
  this.picture = '';
  this.$refs.photo.value = '';

  this.validations = {
    name: {
      is_valid: true,
      text: ''
    },
    locations: [],
    oneLocation: {
      is_valid: true,
      text: ''
    },
    website: {
      is_valid: true,
      text: ''
    }
  };

  EventBus.$emit('clear-tags');

  this.addLocation();
},

Теперь мы отправляем файлы в наш API через VueJS и ось! Давайте быстро внесем эти обновления на стороне редактирования и вернемся к API, чтобы обработать загрузку этих файлов.

Шаг 7. Обработка загрузки файлов через API

Итак, мы снова вернемся к стороне API Laravel. Нам нужно обрабатывать каждую загрузку файла через API. Для этого мы внесем несколько изменений, чтобы принять новое форматирование из FormData() и принять файл. Однако, прежде чем что-либо делать, мы будем работать с фасадом File, поэтому добавьте:

use File;

в верхнюю часть контроллера.

Сначала откройте файл app/Http/Controllers/API/CafesController.php и перейдите к методу postNewCafe().

В верхней части метода загрузите местоположения в массив. Поскольку мы используем данные формы, теперь они передаются как JSON, которые мы должны декодировать. Итак, сначала измените загрузку, чтобы она выглядела так:

$locations = json_decode( $request->get('locations') );

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

$address            = $locations[0]->address;
$city               = $locations[0]->city;
$state              = $locations[0]->state;
$zip                    = $locations[0]->zip;
$locationName           = $locations[0]->name;
$brewMethods            = $locations[0]->methodsAvailable;
$tags               = $locations[0]->tags;

Теперь мы готовы приступить к загрузке файлов. Итак, ПОСЛЕ вызова метода $parentCafe->save() мы обработаем загрузку. Прежде чем мы сможем прикрепить к нему картинку, нам необходимо наличие родительского кафе.

Во-первых, давайте возьмем нашу картинку:

$photo = Request::file('picture');

Это захватывает файл с именем picture из нашего входящего запроса. Теперь давайте проверим, есть ли у нас что-то и действительный ли это файл:

if( count( $photo ) > 0 ){
  if( $photo != null && $photo->isValid() ){

    }
}

Далее мы проверяем, есть ли каталог для кафе. Нам нужен идентификатор кафе, поэтому мы делаем это после того, как кафе было сохранено:

/*
  Creates the cafe directory if needed
*/
if( !File::exists( app_path().'/Photos/'.$parentCafe->id.'/' ) ){
  File::makeDirectory( app_path() .'/Photos/'.$parentCafe->id.'/' );
}

Это создает каталог для кафе, поэтому в будущем, когда мы добавим логотипы и изображения местоположения, мы сможем иметь красивую структуру.

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

/*
  Sets the destination path and moves the file there.
*/
$destinationPath = app_path().'/Photos/'.$parentCafe->id;

/*
  Grabs the filename and file type
*/
$filename = time().'-'.$photo->getClientOriginalName();

/*
  Moves to the directory
*/
$photo->move( $destinationPath, $filename );

/*
  Creates a new record in the database.
*/
$cafePhoto = new CafePhoto();

$cafePhoto->cafe_id = $parentCafe->id;
$cafePhoto->uploaded_by = Auth::user()->id;
$cafePhoto->file_url = app_path() .'/Photos/'.$parentCafe->id.'/';

$cafePhoto->save();

Это все, что нам нужно сделать, чтобы сделать простой загрузчик файлов с помощью VueJS и Axios и обработать загрузку файлов с помощью Laravel. Не забудьте настроить свой API для обработки нескольких местоположений как объекта, а также обновить маршрут редактирования! Я отправлю весь этот код на GitHub здесь: GitHub - serverideup / roastandbrew: Помогаем любителям кофе найти свою следующую чашку кофе. Кроме того, помощь начинающим разработчикам веб-приложений и мобильных приложений в создании одностраничного приложения и преобразовании его в мобильный гибрид. Все руководства можно найти на https://serversideup.net , поэтому обязательно ознакомьтесь с ними!

Заключение

Это был простой обзор того, как загружать фотографии для кафе. Я буду обсуждать еще несколько мелких деталей по ходу проекта, и мы будем больше работать с Axios и управлением файлами VueJS. Мы будем работать над удалением файлов, загрузкой сразу нескольких изображений и, конечно же, отображением фотографий на странице. Эти уроки скоро появятся! А пока обязательно загляните на GitHub: GitHub - serverideup / roastandbrew: Помогаем любителям кофе найти свою следующую чашку кофе. Кроме того, помощь начинающим разработчикам веб-приложений и мобильных приложений в создании одностраничного приложения и преобразовании его в мобильный гибрид. Все руководства можно найти на https://serversideup.net »и задать любые вопросы в комментариях ниже!