Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

STEP07:Laravel5.7 + Vue2.5 でユーザ一覧をCSVでダウンロード

Laravelのユーザテーブル情報をVueの画面からダウンロードします
Vue側のダウンロードの処理一切はダウンロードボタンのコンポーネントに閉じ込めてみます
CSVはWindowsで開けるように BOM つけときます(macは動作確認してません..)

環境設定他関連記事はこちら
Laravel + Vue + Vuetify で業務サイト作ってみる

作業はこちらを参考に
LaravelでCSVダウンロード。 - Qiita
[Laravel5.4] 大容量CSVのダウンロード - Qiita
Laravel 5.3でcsvのダウンロード機能を実装
【Laravel 5.4】ファサード の作り方 - Qiita

1.LaravelでCSVダウンロードするサービスを作成

app/Services/Csv.php
<?php

namespace App\Services;

use Illuminate\Support\Facades\Log;
use Response;

class Csv
{
    /**
     * CSVダウンロード
     * @param array $csv_data
     * @param array $csv_header
     * @param string $csv_filename
     * @return \Illuminate\Http\Response
     */
    public function download($csv_data, $csv_header, $csv_filename)
    {
        Log::Debug(__CLASS__.':'.__FUNCTION__);

        // ヘッダー指定あれば1行目にヘッダーをセット
        if (count($csv_header) > 0) {
            array_unshift($csv_data, $csv_header);
        }

        // ストリームでレスポンス ::
        //      vendor/laravel/framework/src/Illuminate/Routing/ResponseFactory.php
        //          streamDownload($callback, $name = null, array $headers = [], $disposition = 'attachment')
        return response() -> streamDownload(
            function () use($csv_data) {
                $file = new \SplFileObject('php://output', 'w');
                foreach ($csv_data as $row) {
                    $file->fputcsv($row);
                }
            },
            $csv_filename,
            array('Content-Type' => 'application/octet-stream')
        );
    }
}

今回、BOMは Vue側で付与するので、Laravelでは素直に出力するだけにしてます
また、ファイル名指定をつけてますが、実際のファイル名はVue側で設定するので、ここのファイル名指定は利用してないです(手抜き。。)
本来は Vue 側で attachment の ファイル名を取得して利用するべきですが。。。手抜きです。。

あまりネット上で記述がなかった streamDownload (https://readouble.com/laravel/5.7/ja/responses.html#file-downloads) を利用した手順で書いてみました。

2.作成したサービスをファサード化

参考記事のままです。
【Laravel 5.4】ファサード の作り方 - Qiita
助かりました。

2-1.サービスプロバイダ作成

artisnコマンドでひな形作って

$ php artisan make:provider  CsvServiceProvider

レジスタ部分にバインドを記述

app/Providers/CsvServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class CsvServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }

    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(
            'Csv', 'App\Services\Csv'
        );
    }
}

2-2.ファサードクラスを作成

ディレクトリを作成して

$ mkdir  app/Facades  

クラスを作成

app/Facades/Csv.php
<?php
namespace App\Facades;

use Illuminate\Support\Facades\Facade;

class Csv extends Facade {
    protected static function getFacadeAccessor() {
        return 'Csv';
    }
}

2-3.サービスプロバイダと別名の設定を追加

config/app.php
~~~
    'providers' => [
   ~~~
        App\Providers\CsvServiceProvider::class,
    ],
  ~~~
    'aliases' => [
   ~~~
        'Csv' => App\Facades\Csv::class,
    ],
~~~

3.Laravel のUserコントローラに download を追加

ユーザテーブルから指定カラムのみを取り出してCSVダウンロードのファサードに渡してます
お手軽ですね

app/Http/Controllers/UserController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Hash;
use App\User;
use Validator;
use App\Facades\Csv;    //★これを追加

class UserController extends Controller
{
  public function index()
  {
    Log::Debug(__CLASS__.':'.__FUNCTION__);

    $users = User::all();
    return ['users' => $users];
  }


  public function download(Request $request)     //★このfunctionも追加
  {
    Log::Debug(__CLASS__.':'.__FUNCTION__);

    $csv_data = User::get(['loginid', 'name', 'role'])->toArray();
    $csv_header = ['loginid', 'name', 'role'];
    return Csv::download($csv_data, $csv_header, 'test.csv');    // ここでファサード呼んでる
  }

~~~

4.Laravelのルーティング設定

CSVダウンロードは管理者以上の権限でないと利用できないように制限かけときます

routes/web.php
<?php
Route::get('/', function () {
    return view('home');
})->middleware('auth');

// Authentication Routes...
Route::get('/login', 'Auth\LoginController@showLoginForm')->name('login');
Route::post('/login', 'Auth\LoginController@login');
Route::post('/logout', 'Auth\LoginController@logout')->name('logout');

// Admin
Route::group( ['middleware' => ['auth', 'can:admin']], function() {

  // USER
  Route::post('/api/admin/user', 'UserController@index')->name('admin/user');
  Route::post('/api/admin/user/store', 'UserController@store')->name('admin/user/store');
  Route::post('/api/admin/user/destroy', 'UserController@destroy')->name('admin/user/destroy');
  Route::post('/api/admin/user/download', 'UserController@download')->name('admin/user/download');
});

// Other
Route::get('/{any}', function () {
  return view('home');
})->middleware('auth')->where('any', '.*');

★ /api/admin/user/download で Vueから UserコントローラのCSVダウンロードを呼び出します

設定確認

php artisan route:list
+--------+----------+-------------------------+---------------------+---------------------------------------------------------+--------------------+
| Domain | Method   | URI                     | Name                | Action                                                  | Middleware         |
+--------+----------+-------------------------+---------------------+---------------------------------------------------------+--------------------+
|        | GET|HEAD | /                       |                     | Closure                                                 | web,auth           |
|        | POST     | api/admin/user          | admin/user          | App\Http\Controllers\UserController@index               | web,auth,can:admin |
|        | POST     | api/admin/user/destroy  | admin/user/destroy  | App\Http\Controllers\UserController@destroy             | web,auth,can:admin |
|        | POST     | api/admin/user/download | admin/user/download | App\Http\Controllers\UserController@download            | web,auth,can:admin |
|        | POST     | api/admin/user/store    | admin/user/store    | App\Http\Controllers\UserController@store               | web,auth,can:admin |
|        | GET|HEAD | api/user                |                     | Closure                                                 | api,auth:api       |
|        | GET|HEAD | login                   | login               | App\Http\Controllers\Auth\LoginController@showLoginForm | web,guest          |
|        | POST     | login                   |                     | App\Http\Controllers\Auth\LoginController@login         | web,guest          |
|        | POST     | logout                  | logout              | App\Http\Controllers\Auth\LoginController@logout        | web                |
|        | GET|HEAD | {any}                   |                     | Closure                                                 | web,auth           |
+--------+----------+-------------------------+---------------------+---------------------------------------------------------+--------------------+

5.VueでCSVダウンロード専用のコンポーネントを作成

このボタンの中でサーバ通信からファイル保存までCSVダウンロード処理のすべてをまかないます
ダウンロード処理中はボタンを非活性にして処理中を表す「ぐるぐる」を表示
 ※ ぐるぐるを見るには Chromeで通信帯域を絞ったりすると出ると思います

ダウンロードボタンの色やアイコンや文言などはコンポーネント呼び出し時に指定可能にしときます(props)
サーバのURLやCSVファイル名も指定可能にしておきます(props)

resources/js/components/Admin/CsvDownload.vue
<template>
  <v-btn block flat
    :color="color ? color : 'primary'"
    :loading="csvdownloading"
    :disabled="csvdownloading"
    @click="csvdownload(filename, url)"
  >
    <v-icon dark class="mr-1">{{icon ? icon : 'cloud_download' }}</v-icon> {{title ? title : 'CSV ダウンロード'}}
    <v-progress-circular slot="csvdownload" indeterminate color="primary" dark></v-progress-circular>
  </v-btn>
</template>

<script>
  export default {
    name: 'CSVDownload',

    props: {
      color: String,
      icon: String,
      title: String,
      url: String,
      filename: String,
    },

    data: () => ({
      csvdownloading: false,
    }),

    created() {
      if (process.env.MIX_DEBUG) console.log('CSV Download Btn created.')
    },

    methods: {
      csvdownload(filename, url) {
        if (process.env.MIX_DEBUG) console.log("CSV Download func csvdownload")
        var config = {
          responseType: 'blob',
          headers: {'Content-Type': 'application/x-www-form-urlencoded'},
        }

        this.csvdownloading = true
        axios.post(this.url, config)

        .then( function (response) {
          this.csvdownloading = false
          this.saveCsvFile(response)
        }.bind(this))

        .catch(function (error) {
          if (process.env.MIX_DEBUG) console.log("CSV Download csvdownload error")
          this.csvdownloading = false
          console.log(error)
          if (error.response && [401, 419].includes(error.response.status)) {
            this.$emit('axios-logout')
          }
        }.bind(this))
      },

      saveCsvFile(res) {
        // CSVデータ取得 - BOM 付与
        var blob = new Blob(['\ufeff' + res.data], { type: 'text/csv' })

        // ファイル名設定 - ファイル名には日時をつけて拡張子 csv を設定
        //                - ボタンの引数で指定された名前があれば尊重
        //                - 指定なしならルーティングのページ名をつけておく
        //                - (サーバから指定されたファイル名は無視してます)
        var filename = this.filename
        if (! filename) {
          filename = this.$route.meta.name
        }
        filename += '_' + moment(Date.now()).format("YYYYMMDD_HHmmss") + '.csv'

        // IE11 ( msSaveBlog が有効なら)
        if (window.navigator.msSaveBlob) {
          window.navigator.msSaveBlob(blob, filename)
          window.navigator.msSaveOrOpenBlob(blob, filename)
        }

        // IE11 以外なら( Chrome, Firefox, Android, etc...)
        else {
          const url = window.URL.createObjectURL(blob)
          const link = document.createElement('a')
          link.href = url
          link.setAttribute('download', filename)
          link.click()
        }
      },
    },
  }
</script>

6.Vueのユーザ一覧画面にダウンロードボタンを追加

CSVダウンロードボタンのコンポーネントを組み込みます

resources/js/components/Admin/UserComponent.vue
<template>
  <v-flex>
    <v-card xs12 class="m-3 px-3">

      <v-card-title class="title">
        <v-icon class="pr-2">{{ $route.meta.icon }}</v-icon> {{ $route.meta.name }} {{ /* 社員管理 */ }}
        <user-dialog ref="userDialog" @reload="reload" @setsearch="setsearch"></user-dialog>
        <v-spacer></v-spacer>
        <v-spacer></v-spacer>
        <v-text-field
          v-model="search"
          prepend-icon="search"
          label="Search"
          single-line
          hide-details
          clearable
        ></v-text-field>
      </v-card-title>

      <v-data-table
        :headers="headers"
        :items="tabledata"
        :pagination.sync="pagination"
        :rows-per-page-items='[10,25,50,{"text":"All","value":-1}]'
        :loading="loading"
        :search="search"
        class="elevation-0 p-1"
      >
        <v-progress-linear slot="progress" color="blue" indeterminate></v-progress-linear>

        <template slot="items" slot-scope="props">
          <tr>
            <td class="text-xs-center" xs1>{{ (props.index + 1) + (pagination.page - 1) * pagination.rowsPerPage }}</td>
            <template v-for="n in (headers.length - 2)">
              <td :class="'text-xs-' + headers[n].align" style="white-space: nowrap;" v-text="props.item[headers[n].value]"></td>
            </template>
            <td class="text-xs-center" xs1>
              <v-btn flat small fab @click="dialogOpen(props.item)"><v-icon color="success">edit</v-icon></v-btn>
              <v-btn flat small fab @click="dialogOpen(props.item,true)"><v-icon color="error">delete</v-icon></v-btn>
            </td>
          </tr>
        </template>
      </v-data-table>

      <v-spacer></v-spacer>

      <v-card-actions>
        <v-btn flat block color="primary" @click="dialogOpen(null)"><v-icon>person_add</v-icon>新規追加</v-btn>
        <v-spacer></v-spacer>
★1       <csv-download url="/api/admin/user/download" color="primary"></csv-download>
      </v-card-actions>
    </v-card>
  </v-flex>
</template>

<script>
  import user_dialog from './UserDialog.vue'
★2  import csv_download from './CsvDownload.vue'

  export default {
    name: 'UserComponent',

    components: {
      'user-dialog': user_dialog,
★2      'csv-download': csv_download,
    },

    props: {
    },
~~~

★2 でコンポーネント登録
★1 でCSVダウンロードボタンコンポーネントを利用

ユーザ一覧側の変更はこれだけ
ダウンロード処理系はボタンのコンポーネントに閉じ込めたからすっきりです

7.日付処理系の moment 導入

ダウンロードしたCSVファイル名に日時をつけるために momentを導入です

$ npm install moment

読み込み設定

resources/js/app.js
require('./bootstrap');

// Vue
import Vue from 'vue'


// Vuetify
import Vuetify from 'vuetify'
import colors from 'vuetify/es5/util/colors'
Vue.use(Vuetify)
import 'vuetify/dist/vuetify.min.css'
import 'material-design-icons-iconfont/dist/material-design-icons.css'


// Vue-Router
import router from './router'


// moment
window.moment = require('moment')


// Main app
const app = new Vue({
    el: '#app',
    router,
});

8.動作確認

npm コンパイルして実行
管理者でログインして

npm run dev
php artisan serve --host=172.16.0.100  --port=8000

8-1 ユーザ一覧ページを開いて

ダウンロードボタンができていること

zzz1.png

8-2 CSVダウンロードボタンを押すと

CSVファイルがダウンロードされること

zzz2.png

Excel でダウンロードファイルを開いたら、文字化けしていないことも確認!
(Windows10、 Excel2016 で動作確認済み)

以上

なんとか文字化けせずにCSVダウンロードできるようになりました。。。
Vue でいったん受け取ってからファイルにしているからか Laravel側で BOM つけてもうまくいかなかったんですよね。。
めんどくさくなって Vue 側でつけたらすんなりいきました

今回もソースはこちら
https://github.com/u9m31/u9m31/tree/step07

nobu-maple
元Oracleマスター/いまは普通のシステム屋/JavascriptやPHPからCやC++まで言語はなんでも/中学でMSXのBASICプログラムをはじめてからいく年月/最近はUiPathでRPAなことも楽しい
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away