20201006のlaravelに関する記事は11件です。

[Laravel]Twitterクローンアプリの、フォロー機能のコード手順メモ。

現在、教材を見ながらTwitterのクローンアプリを作っています。フォロー機能のコード手順メモです。

手順1: まず、以下がフォローリストのViewコード。アバター部分はまだハードコーディング。

friends-list.blade.php
<h3 class="font-bold text-lg mb-4">フォローリスト</h3>

<ul>
  @foreach (auth()->user()->follows as $user)
  <li class="mb-4">
    <div class="flex items-center text-sm">
      <img src="/images/avator.png" alt="" class="rounded-full mr-2 w-10">
      {{ $user->name }}
    </div>
  </li>
  @endforeach
</ul>

手順2:  @foreach (auth()->user()->follows as $user) の部分で使っている、followsのリレーションを設定します。
ユーザーは沢山の他のユーザーをフォローすることができるから、リレーションはbelongsToMany()にします。このリレーションで、どのユーザーがどのユーザをフォローしているのかを管理できます。

app\Models\User.php
    public function follows() {
        return $this->belongsToMany(User::class);
    }

手順3: pivotテーブルとなるfollowsテーブルを作成します。

php artisan make:migration create_follows_table

手順4: マイグレーションファイルを設定します。
ユーザー間及びフォローしているユーザー間の関係を設定する為には、'user_id' と、'following_user_id' が必要になります。
$table->primary(['user_id', 'following_user_id']); の部分で二つプライマリーキーを指定しているのは、ユーザーがボタンを二度押しとかした時に重複を許さないようにする為。

database\migrations\2020_09_30_135049_create_follows_table.php
    public function up()
    {
        Schema::create('follows', function (Blueprint $table) {
            $table->primary(['user_id', 'following_user_id']);
            $table->foreignId('user_id');
            $table->foreignId('following_user_id');
            $table->timestamps();

            $table->foreign('user_id')
            ->references('id')
            ->on('users')
            ->onDelete('cascade');

            $table->foreign('following_user_id')
            ->references('id')
            ->on('users')
            ->onDelete('cascade');
        });
    }

手順5: マイグレーションします。

php artisan migrate

手順6: 取り敢えず、followsテーブルに手動でデータを入れてみます。

image.png

因みに、既に作ってあるusersテーブルはこのような感じ。
image.png

手順7: これからtinkerで確認作業をしていくのですが、その前にUserモデルを以下のように修正しておきます。
'follows'とするのは、Laravelでは慣習的にpivotテーブルの名前は、双方のテーブル名をアルファベット順にアンダーバーでつなげる為です。今回の場合だと『user_user』というファイル名をLaravelが認識するのですが、そうではなくてfollowsだよ、と伝える為です。また、カスタムしたカラムネームを使っている為、foreignPivotKeyとrelatedPivotKeyを明示します。(PivotテーブルでLaravelが認識するデフォルトのカラムネームは、『モデル名_id』です。これと異なる場合は、明示する必要があります。今回の場合だと、foreignPivotKeyとrelatedPivotKeyの両方ともuser_id。)

app\Models\User.php
    public function follows() {
        return $this->belongsToMany(User::class, 'follows', 'user_id', 'following_user_id');
    }

手順8: tinkerを立ち上げて確認してみます。

php artisan tinker
App\Models\User::find(1)->follows;

Userモデルのインスタンスが返されて、以下のように表示されます。Userモデルのid1のユーザーがフォローしている、id3と5のユーザーが出てきます。

>>> App\Models\User::find(1)->follows;
=> Illuminate\Database\Eloquent\Collection {#3772
     all: [
       App\Models\User {#4389
         id: 3,
         name: "原田 里佳",
         email: "sugiyama.taichi@example.com",
         email_verified_at: "2020-10-06 12:13:24",
         current_team_id: null,
         profile_photo_path: null,
         created_at: "2020-10-06 12:13:24",
         updated_at: "2020-10-06 12:13:24",
         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#4177
           user_id: 1,
           following_user_id: 3,
         },
       },
       App\Models\User {#4140
         id: 5,
         name: "宮沢 充",
         email: "ryohei11@example.com",
         email_verified_at: "2020-10-06 12:13:24",
         current_team_id: null,
         profile_photo_path: null,
         created_at: "2020-10-06 12:13:24",
         updated_at: "2020-10-06 12:13:24",
         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#4333
           user_id: 1,
           following_user_id: 5,
         },
       },
     ],
   }

id3のユーザを検索して見てみると。。。

App\Models\User::find(3)->follows;

何も出てこないのでOKです。

=> Illuminate\Database\Eloquent\Collection {#4394
     all: [],

手順9: リレーション設定したものを、データベースに保存する関数を作ります。

app\Models\User.php
    public function follow(User $user) {    // ←この関数追記
        return $this->follows()->save($user);
    }

    public function follows() {
        return $this->belongsToMany(User::class, 'follows', 'user_id', 'following_user_id');
    }

手順10: 上記で作ったfollow()関数を、tinkerで確認していくのですが、その前にデータをリフレッシュしておきます。

php artisan migrate:fresh

手順11: 新たにuserテーブルにデータを入れていきます。このような感じ。(id1だけはブラウザのregisterページから手動で登録して、その他はFactory機能で登録しました。)
image.png

手順12: tinkerでフォローの設定・確認をしていきます。
id1(ログインしている自分のid)が、id3とid5をフォローしていて、id3とid5がそれぞれid1をフォローしている、という設定をしてみます。(相互フォロー)

follow()関数に渡すのは、ユーザーモデルのインスタンスでなければいけないので、それぞれのidを探して、変数に格納します。

$me = App\Models\User::find(1);
$taichi = App\Models\User::find(3);
$yoko = App\Models\User::find(5);

手順13: follow()関数を使って、それぞれがフォローするようにテーブルに登録します。

$me->follow($taichi);
$me->follow($yoko);
$taichi->follow($me);  
$yoko->follow($me);  

テーブルには以下のように入ります。
image.png
ブラウザで確認(id1のアカウント)すると、ちゃんと表示もされています。
image.png

手順14: id1がid6をフォローしてみます。

$yumiko = App\Models\User::find(6);
$me->follow($yumiko);

手順15: id1がフォローしている全idを確認してみると、以下のように出ます。

$me->follows;
=> Illuminate\Database\Eloquent\Collection {#4390
     all: [
       App\Models\User {#4391
         id: 3,
         name: "若松 太一",
         email: "manabu.saito@example.com",
         email_verified_at: "2020-10-06 17:02:31",
         current_team_id: null,
         profile_photo_path: null,
         created_at: "2020-10-06 17:02:31",
         updated_at: "2020-10-06 17:02:31",
         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#4394
           user_id: 1,
           following_user_id: 3,
         },
       },
       App\Models\User {#4392
         id: 5,
         name: "喜嶋 陽子",
         email: "minoru89@example.org",
         email_verified_at: "2020-10-06 17:02:31",
         current_team_id: null,
         profile_photo_path: null,
         created_at: "2020-10-06 17:02:31",
         updated_at: "2020-10-06 17:02:31",
         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#4333
           user_id: 1,
           following_user_id: 5,
         },
       },
       App\Models\User {#4395
         id: 6,
         name: "中津川 裕美子",
         email: "lkimura@example.com",
         email_verified_at: "2020-10-06 17:02:31",
         current_team_id: null,
         profile_photo_path: null,
         created_at: "2020-10-06 17:02:31",
         updated_at: "2020-10-06 17:02:31",
         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#3457
           user_id: 1,
           following_user_id: 6,
         },
       },
     ],
   }

ブラウザ確認してみても、正常に反映されています。
image.png

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel taskscheduleでClass 'ZipArchive' not foundとエラー出たがPHPの指定を間違えていたのが原因だった

初Qiitaです。足りない、誤りがある箇所についてはコメントから指摘いただけますと嬉しいです。

環境:MacOS Catalina, PHP 7.4.11, Laravel 7.28.3

■やりたかったこと
Laravelのタスクスケジュールの設定を行い自動実行できるようにしたかった。

■問題
php artisan schedule:runをすると問題なく動くが、スケジュールを実行するとZipArchiveが見つからないと以下のエラーが出てしまう。
local.ERROR: Class 'ZipArchive' not found {"exception":"[object] (Error(code: 0): Class 'ZipArchive'

■解決方法
使用したいphpの場所を
which php
で取得してcrontabで明示的に以下のような感じで書く。

* * * * * cd /var/www/ && /usr/bin/php7 /var/www/artisan schedule:run >> /var/www/storage/logs/cron.log 2>&1

こちらの記事を参照すると分かりやすいです。
https://stackoverrun.com/ja/q/12644861

■問題の原因
Localで使用しているbrewでインストールしたphpとcronが使用するPHPは違うものらしい。(ここが曖昧なので詳しい方教えていただけると嬉しいです。)
そのため php -m をしてlocalのphpにzipがインストールされていることを確認しても、cronが使用するphpにはzipがインストールされていないためClass 'ZipArchive' not foundとなってしまった。

以下余談です。
当該エラーでググると多くの記事で「zipをインストールしろ。そしてモジュールを確認しろ」と書いてあり、それはやったのに何故エラーが出るのか。とずっと悩み続けてしまいました。

このエラーで相当な時間を費やして最終的に会社の先輩に解決してもらったのですが、私のようにcronに全然詳しくない・初めて使う。という場合にはいきなり全部組み込むんじゃなくて
1. echoを試す
2. php -vを試す
3. php artisanを試す
みたいに段階的に発展させると大きなつまずきが減りますよ。とのことでした。

精進します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel ルーティングの設定 〜 自分用

体裁は後々揃えていくことにします。。。

ルーティングの設定

resources/wiewフォルダにあるwelcome.blade.phpに設定

routes/web.php
1  Route::get('/', function () {
2      return view('welcome');
3  });

 メインページ以外の設定。
 該当のアドレスを設定し、その後にどのコントローラーのどのメソッドかを記述。
 最後にnameとして設定する。

routes/web.php
1  Route::get('/', function () {
2    return view('welcome');
3  });
4
5  Route::get('/articles', 'ArticleController@index')->name('article.list');
6  Route::get('/articles/{id}', 'ArticleController@show')->name('article.show');
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【laravel-admin】【grid】Custom Actions Class

gridの\Encore\Admin\Grid\Displayers\ActionsをoverrideしてCustomActionsを作る

CustomActions.php
namespace App\Admin\Controllers;

use Encore\Admin\Admin;

class CustomActions extends \Encore\Admin\Grid\Displayers\Actions
{
    protected $actions = ['edit'];
    protected function renderEdit()
    {
        if(false !== strpos($_SERVER["REQUEST_URI"], env('ADMIN_ROUTE_PREFIX', 'admin')."/auth")){
            return <<<EOT
<a href="{$this->getResource()}/{$this->getRouteKey()}/edit" class="{$this->grid->getGridRowName()}-edit">
    <i class="fa fa-edit"></i>
</a>
EOT;
        }else{
            $this->setupEditScript();
            return <<<EOT
<a href="{$this->getResource()}/" class="{$this->grid->getGridRowName()}-edit btn btn-xs btn-default">
    <i class="fa fa-edit"></i> 編集
</a>
EOT;
        }
    }
    protected function setupEditScript()
    {
        $trans = [
            'delete_confirm' => trans('admin.delete_confirm'),
            'confirm'        => trans('admin.confirm'),
            'cancel'         => trans('admin.cancel'),
        ];

        $script = <<<SCRIPT

$('.{$this->grid->getGridRowName()}-edit').click(function (e) {
    var id = $(this).closest('tr').find('.column-ID').text();
    id = $.trim(id);
    var currentHref = $(this).attr('href');
    var newHref = currentHref + id + '/edit';
    window.location.href= newHref;
});

SCRIPT;

        Admin::script($script);
    }
}
bootstrap.php
Encore\Admin\Grid\Column::define('__actions__', \App\Admin\Controllers\CustomActions::class);

gridの編集ボタン&それをクリックしたときの挙動を変えたかった時に対応しました。

\Encore\Admin\Grid\Displayers\ActionsをoverrideしたCustomActionsクラスを作って、bootstrap.phpに上記を追記します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【laravel-admin】【controller】javascript

laravel-adminのController側でjavascriptを使う

ちょっとしたjavascriptのためにいちいちファイルを作って、とかviewを変更して、とかが大変な時は以下の方法でcontroller側にjavascriptを書いてページに埋め込むことができます。
viewの一部を変更したいけどviewには手を加えたくない...という場合などに便利です。

SampleController.php
use Encore\Admin\Admin;

class SampleController extends AdminController
{
    /**
     * sample
     */
    public function sample(Content $content)
    {
        $script = <<<SCRIPT
$('.breadcrumb li:first').remove();
$('.breadcrumb .active').html('<i class="fa fa-home"></i>メニュー');
$('.navbar-custom-menu').find('.pull-left').html('');
$('.main-footer').find('strong').html('');
SCRIPT;
        Admin::script($script);
        Admin::style('.form-control {margin-top: 10px;}');
        return $content
        ->title('メニュー')
        ->description('メニューです。')
     ->row('メニュー1')
        ->row('メニュー2')
        ->row('メニュー3');
    }
}

1行目→パンくずリストの1つ目を削除
2行目→自身がいる位置を現在地として表示
3行目→ユーザー設定のボタンを削除
4行目→フッターの文字を削除
こっちはcss→Admin::style('.form-control {margin-top: 10px;}');

参考:Laravel admin | CSS / JavaScript

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LaravelからVueコンポーネントへのデータ受け渡し

Laravelで作成したアプリのデータを非同期通信したいと思って、Vueを使い始めた。
なかなか、理解が悪く先にすすんでいないが、とりあえずいつものごとく備忘録を書いていく。

やりたいこと

初期のみDBにあるデータをLaravelで静的に受け取って、
その後はVueで作成したコンポーネントを使用しながら、非同期通信でデータを操作する。

詰まった箇所

Laravelでデータの処理してから、どのようにして、Vueコンポーネントに渡していいかわからなかった。
Vueコンポーネント同士の親子でのデータ受け渡しについては記事をよく見かけたが、ララベルとVue間でのデータ受け渡しについては、調べてもよくわからなかった。理解が悪いだけかもしれないが。。。

結論

色々やってみて、以下の通りでうまく行った。

  1. Laravelのコントローラで、app.jsが読まれているBladeテンプレート(これが親となる)へDBからデータ値を送る。
  2. 上記Bladeテンプレート内の、Bodyタグ内の最下部にscriptタグを配置。その中に、コントローラから送られたDB値を格納する。例えば、DBから$data_hogeを受け取っているとしたら、<script>let dataHoge = @json($data_hoge)</script>と書く。@jsonとしているのはphpのデータをjson形式にしないと、vueで使えないので。
  3. app.jsの、Vueインスタンスの中に、dataオプションを記述し、データを受け取る。
  4. app.jsが読まれたBlade内で使われるVueコンポーネントは、子コンポーネント(Blade-app.jsが親)になる。Blade内に書いた、Vueコンポーネントタグ内に、v-bind:オプション値として、子コンポーネントに渡す準備をする。そして、子コンポーネント内では、それをpropsで受ける。子コンポーネント内でデータを動的に変更するには、dataプロパティにつけ直して、自由に使う(propsはそのまま使ってはいけない)。
  5. 子コンポーネントから、親(Blade)に値を返したいときは、$emitでイベント発火して、値を受け取る。

考え方は、LaravelBlade-app.jsが親、Bladeに書かれたコンポーネントが子になる。
文章にしてみると、単にVueやJavaScriptの理解が浅いだけな気がしてきた。

後で、もうちょっと加筆するかも。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者によるLaravel-忘備録 自分用(プロジェクトの作成及びサーバー起動)

新規のプロジェクト作成
  laravel new *****

*****にプロジェクト名

サーバー起動
php artisan serve

モデルの作成

hp artisan make:model ****

*****はモデル名で単数形。

php artisan make:model -h

上記でオプションの全容を確認できる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者によるLaravel-忘備録 自分用(プロジェクトの作成及びサーバー起動、モデル作成及びDB)

新規のプロジェクト作成
  laravel new *****

*****にプロジェクト名

サーバー起動
php artisan serve

モデルの作成

php artisan make:model ****

*****はモデル名で単数形。

php artisan make:model -h

上記でオプションの全容を確認できる。

カラムの追加

該当のマイグレーションファイルに記入することでカラムの追加ができる

database/migrations/xxxx_xx_xx_xxxxxxxx_create_articles_table.php
public function up()
    {
        Schema::create('articles', function (Blueprint $table) {
            $table->increments('id');
            $table->string('content');
            $table->timestamps();
        });
    }

DB migrate

% php artisan migrate
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

同一リポジトリでNuxt.js(Frontend) + Laravel(API)な開発環境を準備する

概要

LaravelのBlade運用にツラさを感じてきてフロントを分離したくなったので調査して開発環境を準備した。
同一リポジトリにした理由はフロント側の開発をしている時に同一の方がAPIの検証をしやすいと思ったからです。
調査ついでに記事として残しておく。

環境

  • Host:Mac
  • Docker
    • nginx:1.19-alpine
    • node:13.8-alpine
    • php:7.4-fpm-alpine

完成イメージ

spa_dev_template.png

完成品

https://github.com/nagi125/spa_dev_template
※ NuxtとLaravelのプロジェクトはREADME.mdを見て生成してください。

ざっくり説明

Laravelのプロジェクト内に「frontend」というディレクトリを準備をし、Nuxtのプロジェクト一式はそちらに準備します。
volume機能を用いてNuxt側とLaravel側で、各種ファイルを共有しています。
そしてNuxt側はデフォルト位置を「/app/frontend」に、Laravel側はデフォルト位置を「/app」にしておけば完成です。

各コンテナについて

Nginx

FROM nginx:1.19-alpine

ENV TZ Asia/Tokyo

COPY conf/default.conf /etc/nginx/conf.d/default.conf

Nginxのconfファイル

server {
    server_name         localhost;
    root  /app/public;
    index index.php;

    proxy_set_header    Host                 $host;
    proxy_set_header    X-Real-IP            $remote_addr;
    proxy_set_header    X-Forwarded-Host     $host;
    proxy_set_header    X-Forwarded-Server   $host;
    proxy_set_header    X-Forwarded-For      $proxy_add_x_forwarded_for;

    location / {
        proxy_pass    http://frontend:3000;
    }

    location /api {
        try_files $uri $uri/ /index.php$is_args$args;
    }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass   api:9000;
        fastcgi_index  index.php;

        include        fastcgi_params;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param  PATH_INFO $fastcgi_path_info;
    }
}

開発環境なので若干雑に作っている部分はあります。
/apiの場合はLaravelを見にいき、デフォルトはNuxtを見にいくようにRP設定してあります。
※FastCGIの場合は厳密にはRPと言わないかもしれませんが・・・。

Nuxt.js

FROM node:13.8-alpine

RUN apk update && \
    apk add git && \
    apk add --no-cache curl && \
    curl -o- -L https://yarnpkg.com/install.sh | sh && \
    yarn add @vue/cli @vue/cli-service-global nuxt create-nuxt-app

ENV TZ Asia/Tokyo
ENV PATH $HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:$PATH

WORKDIR /app/frontend

CMD ["/bin/ash"]

npmではなくyarn派なのでyarnを利用するように調整してあります。
ポイントは作業領域を「/app/frontend」にしておくところです。

Laravel

FROM php:7.4-fpm-alpine

ENV TZ Asia/Tokyo
ENV COMPOSER_ALLOW_SUPERUSER 1

# install Lib
RUN apk update && \
    apk add --no-cache --virtual .php-builds oniguruma-dev git zip unzip

# add php-ext-module
RUN docker-php-ext-install mbstring && \
    docker-php-ext-enable mbstring

# install Composer
RUN curl -sS https://getcomposer.org/installer | php && \
    mv composer.phar /usr/local/bin/composer && \
    chmod +x /usr/local/bin/composer

WORKDIR /app

こちらのコンテナは作業領域を「/app」にしておきます。

docker-compose

version: '3'
services:
  nginx:
    container_name: nginx
    build:
      context: ./docker/nginx
      dockerfile: Dockerfile
    ports:
      - 80:80
    tty: true
    depends_on:
      - frontend

  frontend:
    container_name: frontend
    build:
      context: ./docker/frontend
      dockerfile: Dockerfile
    environment:
      PORT: '3000'
      HOST: '0.0.0.0'
      API_URL: 'http://localhost/api/v1'
    expose:
      - 3000
    volumes:
      - .:/app
    stdin_open: true
    tty: true
    restart: always
    depends_on:
      - api

  api:
    container_name: api
    build:
      context: ./docker/api
      dockerfile: Dockerfile
    environment:
      LANG: 'ja_JP.UTF-8'
      TZ: 'Asia/Tokyo'
    volumes:
      - .:/app
    expose:
      - 9000
    tty: true
    restart: always

docker-composeではNuxtとLaravelのコンテナの「/app」をホストと共有します。
DBを追加したい場合はdocker-composeに書き足せばよいでしょう。

起動

上記まで準備できたら下記コマンドで開発環境が立ち上がるはずです。

$ docker-compose build
$ docker-compise up

NuxtとLaravelプロジェクトの作成

Nuxt

下記コマンドでNuxtプロジェクトの作成をします。
質問の答えについてはここでは省略します。
お好みの設定にしてください。

$ docker-compose exec frontend yarn create nuxt-app ./

下記コマンドで開発モードの立ち上げをします。

$ docker-compose exec frontend yarn dev

Laravel

下記コマンドでLaravelプロジェクトの作成をします。
バージョンはお好みの物にしてください。
※ 直下を指定するとファイルが存在しておりプロジェクト作成不可能と怒られますので一旦「blog」として作ります。

$ docker-compose exec api composer create-project --prefer-dist laravel/laravel blog "6.*"

「blog」ディレクトリの内容を直下にコピーします。

$ docker-compose exec api cp -fR ./blog/. ./

「blog」ディレクトリの削除

$ docker-compose exec api rm -rf ./blog

完成

NuxtとLaravelのプロジェクトの生成が完了すれば開発環境として利用できるようになります。
Nuxt側には「localhost」で、Laravel側には「localhost/api」でアクセスできます。
※Laravel側のroute設定を忘れずに。

本番運用は?

開発用のDockerfileなのでECSとかを利用する場合はこちらはDockerfile.devとして、本番用のDockerfileを準備してください。
Heroku + Netlifyの場合はこのリポジトリをそのままPushすればそのまま使えます。
※Heroku側とNetlify側でpush検知の設定をしてください。

最後に

調査内容のメモとなりますが、何かのお役に立てれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

docker-composeを利用して同一リポジトリでNuxt.js(Frontend) + Laravel(API)な開発環境を準備する

概要

LaravelのBlade運用にツラさを感じてきてフロントを分離したくなったので調査して開発環境を準備した。
同一リポジトリにした理由はフロント側の開発をしている時に同一の方がAPIの検証をしやすいと思ったからです。
調査ついでに記事として残しておく。

環境

  • Host:Mac
  • Docker
    • nginx:1.19-alpine
    • node:13.8-alpine
    • php:7.4-fpm-alpine

完成イメージ

spa_dev_template.png

完成品

https://github.com/nagi125/spa_dev_template
※ NuxtとLaravelのプロジェクトはREADME.mdを見て生成してください。

ざっくり説明

Laravelのプロジェクト内に「frontend」というディレクトリを準備をし、Nuxtのプロジェクト一式はそちらに準備します。
dockerのvolume機能を用いてNuxt側とLaravel側で、各種ファイルを共有しています。
そしてNuxt側はデフォルト位置を「/app/frontend」に、Laravel側はデフォルト位置を「/app」にしておけば完成です。

各コンテナについて

Nginx

FROM nginx:1.19-alpine

ENV TZ Asia/Tokyo

COPY conf/default.conf /etc/nginx/conf.d/default.conf

Nginxのconfファイル

server {
    server_name         localhost;
    root  /app/public;
    index index.php;

    proxy_set_header    Host                 $host;
    proxy_set_header    X-Real-IP            $remote_addr;
    proxy_set_header    X-Forwarded-Host     $host;
    proxy_set_header    X-Forwarded-Server   $host;
    proxy_set_header    X-Forwarded-For      $proxy_add_x_forwarded_for;

    location / {
        proxy_pass    http://frontend:3000;
    }

    location /api {
        try_files $uri $uri/ /index.php$is_args$args;
    }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass   api:9000;
        fastcgi_index  index.php;

        include        fastcgi_params;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param  PATH_INFO $fastcgi_path_info;
    }
}

開発環境なので若干雑に作っている部分はあります。
/apiの場合はLaravelを見にいき、デフォルトはNuxtを見にいくようにRP設定してあります。
※FastCGIの場合は厳密にはRPと言わないかもしれませんが・・・。

Nuxt.js

FROM node:13.8-alpine

RUN apk update && \
    apk add git && \
    apk add --no-cache curl && \
    curl -o- -L https://yarnpkg.com/install.sh | sh && \
    yarn add @vue/cli @vue/cli-service-global nuxt create-nuxt-app

ENV TZ Asia/Tokyo
ENV PATH $HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:$PATH

WORKDIR /app/frontend

CMD ["/bin/ash"]

npmではなくyarn派なのでyarnを利用するように調整してあります。
ポイントは作業領域を「/app/frontend」にしておくところです。

Laravel

FROM php:7.4-fpm-alpine

ENV TZ Asia/Tokyo
ENV COMPOSER_ALLOW_SUPERUSER 1

# install Lib
RUN apk update && \
    apk add --no-cache --virtual .php-builds oniguruma-dev git zip unzip

# add php-ext-module
RUN docker-php-ext-install mbstring && \
    docker-php-ext-enable mbstring

# install Composer
RUN curl -sS https://getcomposer.org/installer | php && \
    mv composer.phar /usr/local/bin/composer && \
    chmod +x /usr/local/bin/composer

WORKDIR /app

こちらのコンテナは作業領域を「/app」にしておきます。

docker-compose

version: '3'
services:
  nginx:
    container_name: nginx
    build:
      context: ./docker/nginx
      dockerfile: Dockerfile
    ports:
      - 80:80
    tty: true
    depends_on:
      - frontend

  frontend:
    container_name: frontend
    build:
      context: ./docker/frontend
      dockerfile: Dockerfile
    environment:
      PORT: '3000'
      HOST: '0.0.0.0'
      API_URL: 'http://localhost/api/v1'
    expose:
      - 3000
    volumes:
      - .:/app
    stdin_open: true
    tty: true
    restart: always
    depends_on:
      - api

  api:
    container_name: api
    build:
      context: ./docker/api
      dockerfile: Dockerfile
    environment:
      LANG: 'ja_JP.UTF-8'
      TZ: 'Asia/Tokyo'
    volumes:
      - .:/app
    expose:
      - 9000
    tty: true
    restart: always

docker-composeではNuxtとLaravelのコンテナの「/app」をホストと共有します。
DBを追加したい場合はdocker-composeに書き足せばよいでしょう。

起動

上記まで準備できたら下記コマンドで開発環境が立ち上がるはずです。

$ docker-compose build
$ docker-compise up

NuxtとLaravelプロジェクトの作成

Nuxt

下記コマンドでNuxtプロジェクトの作成をします。
質問の答えについてはここでは省略します。
お好みの設定にしてください。

$ docker-compose exec frontend yarn create nuxt-app ./

下記コマンドで開発モードの立ち上げをします。

$ docker-compose exec frontend yarn dev

Laravel

下記コマンドでLaravelプロジェクトの作成をします。
バージョンはお好みの物にしてください。
※ 直下を指定するとファイルが存在しておりプロジェクト作成不可能と怒られますので一旦「blog」として作ります。

$ docker-compose exec api composer create-project --prefer-dist laravel/laravel blog "6.*"

「blog」ディレクトリの内容を直下にコピーします。

$ docker-compose exec api cp -fR ./blog/. ./

「blog」ディレクトリの削除

$ docker-compose exec api rm -rf ./blog

完成

NuxtとLaravelのプロジェクトの生成が完了すれば開発環境として利用できるようになります。
Nuxt側には「localhost」で、Laravel側には「localhost/api」でアクセスできます。
※Laravel側のroute設定を忘れずに。

本番運用は?

開発用のDockerfileなのでECSとかを利用する場合はこちらはDockerfile.devとして、本番用のDockerfileを準備してください。
Heroku + Netlifyの場合はこのリポジトリをそのままPushすればそのまま使えます。
※Heroku側とNetlify側でpush検知の設定をしてください。

最後に

調査内容のメモとなりますが、何かのお役に立てれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【第2回】「みんなのポートフォリオまとめサイト」を作ります~SPA認証で死闘編~

はじめに

この連載記事は、僕が「みんなのポートフォリオまとめサイト」を作る過程をゼロから発信しながらみなさんに見てもらいつつ、作っている途中からみなさんにアドバイスをいただいて、よりよいサービスにしていきたいというお話です。

あとは「サービスを作っていく過程って初学者の人にとっては結構興味ある内容だったりするのでは?(少なくとも自分は知りたかった!)」と思い、このようなスタイルで記事を書いています。

一週間に一度のペースで更新しますとか言っちゃいましたが、前回更新からなんと1ヶ月半も経ってしまった!
今後も完成までゆるく更新していく予定です。

前回までの記事

【第0回】「みんなのポートフォリオまとめサイト」を作ります~宣言編~
「とりあえずこれから作るからみんな見てて!」と宣言しただけの記事。

【第1回】「みんなのポートフォリオまとめサイト」を作ります~着手編~
仕様を決めたり、サービス名考えたり、デザインを作ったり。

今回から、いよいよ実装に入っていきます。

これまでやったことと作業時間

やったこと 前回までの作業時間 ( h ) 今回の作業時間 ( h )
仕様決め(競合調査) 3
サービス名考案 2
手書きでWF作成 2
デザイン作成 8
DB設計 0 4
実装(環境構築) 0 4.5
実装 0 25
その他 2

作業時間計 ( h )

前回まで 今回 合計
17 33.5 50.5

1ヶ月半で33.5hという怠惰っぷり。何をしていたんだろう(前回も同じようなこと言ってる。)
Reactでアウトプットするのが初めてなんですが、Udemyの講座で軽めにインプットしたのが1ヶ月以上前だったので完全に忘れてしまって思い出すためにUdemyの動画を見たり、社内LTでの登壇が2回あってその準備をしていたりしてましたね(言い訳)。

参考:Reactの勉強に使ったUdemyの講座たち

React×Reduxの基礎はこちらで
フロントエンドエンジニアのための React ・ Redux アプリケーション開発入門

Hooksの基礎はこちらで
React Hooks 入門 - Hooksと Redux を組み合わせて最新のフロントエンド状態管理手法を習得しよう!

あ、あと会社でReactのチュートリアルをもらったのでそれを苦しみながら進めたりもしていましたね。React×Redux自体慣れてないところにTypeScriptとテストコードも組み込まれていて、途中から何が分からないのかが分からないくらいに難しかったので挫折してしましました。このアプリを作ってレベルアップしてから再挑戦する。

DB設計

だいたい似たようなアプリケーションは数回作ったことあるので、過去の自作アプリを参考にサクっと設計していきます。

ちゃんとやるんだったらER図を作ったりしながらやるのがいいんでしょうが、面倒臭がりなのでスプレッドシートでやっちゃいました。
(ER図を作れるサイト:dbdiagram.io

こんな感じ↓↓↓
スクリーンショット 2020-10-04 21.42.42.png

softdeleteってのはカラム名ではなくLaravelのソフトデリートという「論理削除」機能のことを指していて、削除メソッドを実行してもDBにレコードは残ったままで、代わりにdeleted_atカラムに削除時間が記録されます。deleted_atに値が入っているものは、データ一覧を取得した際も含まれないので便利です。

Laravel ソフトデリート

環境構築

Laravelに関しては必要最低限な分だけやり、フロントに関しても以前のプロジェクトのものをまるっと持ってくることで時間節約します。
個人的には、browser-syncだけ動いてくれればそれでいい。保存したら自動でブラウザを更新、それだけで十分です。

ちなみにLaravelのバージョンは v 7.26.1でインストール当時の最新版です。
(7系は記事がまだ少ないので、6系にしとけばよかったと思いました。。)

package.json
{
    "private": true,
    "scripts": {
        "dev": "npm run development",
        "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
        "watch": "npm run development -- --watch",
        "watch-poll": "npm run watch -- --watch-poll",
        "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --disable-host-check --config=node_modules/laravel-mix/setup/webpack.config.js",
        "prod": "npm run production",
        "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
    },
    "devDependencies": {
        "@babel/preset-react": "^7.0.0",
        "axios": "^0.19",
        "bootstrap": "^4.0.0",
        "browser-sync": "^2.25.0",
        "browser-sync-webpack-plugin": "^2.0.1",
        "cross-env": "^7.0",
        "import-glob-loader": "^1.1.0",
        "jquery": "^3.2",
        "laravel-mix": "^5.0.1",
        "lodash": "^4.17.19",
        "popper.js": "^1.12",
        "react": "^16.2.0",
        "react-dom": "^16.2.0",
        "react-redux": "^7.2.1",
        "react-router-dom": "^5.2.0",
        "redux": "^4.0.5",
        "resolve-url-loader": "^3.1.0",
        "sass": "^1.15.2",
        "sass-loader": "^8.0.0",
        "styled-components": "^5.2.0"
    }
}
webpack.mix.js
const mix = require('laravel-mix');

mix.webpackConfig({
    module: {
        rules: [{
            test: /\.scss/,
            enforce: 'pre',
            loader: 'import-glob-loader'
        }]
    }
})
/*
 |--------------------------------------------------------------------------
 | Mix Asset Management
 |--------------------------------------------------------------------------
 |
 | Mix provides a clean, fluent API for defining some Webpack build steps
 | for your Laravel application. By default, we are compiling the Sass
 | file for the application as well as bundling up all the JS files.
 |
 */

mix.react('resources/js/app.js', 'public/js')
    .sass('resources/sass/app.scss', 'public/css')
    .sourceMaps()
    .browserSync({ 
        https: false, // httpsのサイトをproxyするならtrueをセット
        files: [ 
            './resources/**/*',
            './app/**/*',
            './config/**/*',
            './routes/**/*',
            './public/**/*'
        ],
        proxy: {
            target: 'http://127.0.0.1:8000'
        },
        open: true, //BrowserSync起動時にブラウザを開かない
        reloadOnRestart: true //BrowserSync起動時にブラウザにリロード命令おくる
    });

フロントのnpmモジュールはnodeのバージョン依存によるエラーが頻繁に起こって超厄介なのですが、browser-syncも例外なくそれにあたります。
以前のプロジェクトで、
node: v 9.11.0
browser-sync: v 2.25.0
という組み合わせでうまくいったので、今回もbrowser-syncはv2.25.0を指定してインストール。ちなみに今回nodeはv 12.11.0ですが、これでうまくいきました。

あ、あとESLintとStyleLintは入れたいところだな。。こいつらも設定がバッティングしてしまってうまく動かすのが難しいんですよね。。

いざ実装

SPAってなんぞ?

SPAとは、Single Page Application(シングルページアプリケーション)の略称であり、単一のWebページでアプリケーションを構成する設計構造の名称です。

SPAの説明については別リンク参照しておきますが、ユーザー目線でいうと「動作がサクサクになって快適!」ってことで是非とも採用したいと考えました。

デメリットの一つとして実装コストがかかるとのことですが、気合いで乗り切りましょう(無策

(参考)SPAについて
SPA(Single Page Application)ってなに?
SPA(Single Page Application)の基本

認証系をSPAで書き換える

以前の記事を書いてから今回までの間、ほとんどの時間をこの「認証系のSPAへの書き換え」に費やしていました。
Laravelはコマンド一つで認証系が構築できてしまうのですが、SPAをやろうとするといろいろとLaravel側の設定を書き換えなければいけなくて、これがまあ難しかった。。。

セッション認証とトークン認証

認証系を理解するうえで、この違いを認識するところからのスタートでした。(正直いまだにあんまり理解できてない)
フォームから値を送信してユーザー登録やログインを行うのはセッション認証です。
一方トークン認証は、ログイン情報をサーバー側に保持しないという点でセッション認証と違います。

(参考)セッション認証とトークン認証についてはこちら
Cookie(Session)での認証と Token での認証の違いについて
JWT・Cookieそれぞれの認証方式のメリデメ比較

Laravelの「Sanctum」という認証システム

そして、LaravelにはSPA認証をお手軽に実装できる「Sanctum」というライブラリが存在することを知ったので、こいつを使うことにしたのですが、ここで1つ大きな勘違いをしていました。

(参考)
Laravel 7.x Laravel Sanctum

何となくいろんなところで「SPAでwebアプリを作る場合、バックエンドをAPIとして使う」という情報を見聞きしていたので、「SPAならAPIかー」という浅い理解をしていました。

で、「SPA API 認証」というキーワードでぐぐると、「SPAではトークン認証をしましょう」という記事がヒットします。当然僕は、「そうか!SPAはトークン認証なんだ!」と思い、先ほどのSanctumを使ったトークン認証を実装していました。

↓これ
スクリーンショット 2020-10-06 6.12.06.png

トークン認証に書き換えるの、めっちゃ苦労しました。。。
(たぶん10時間くらいはかかりました。HTTPリクエストヘッダーの「Authorization」にBearerトークンをセットしなきゃいけないとか、まじで ?????? でした)

そしてもがき苦しみながらトークン認証に書き換え終わったころ、見てはいけないものを見てしまいました。

SanctumでSPAを認証するのにAPIトークンを使ってはいけません

先ほどのSanctumのAPIトークン認証のページ

スクリーンショット 2020-10-06 6.12.06.png

皆さん自身のファーストパーティSPAを認証するためにAPIトークンを決して利用してはいけません。代わりに、Sanctumの組み込みSPA認証を使用してください。

、、、トークン認証ダメ!ゼッタイ!ってめっちゃ書いてあるやんけ!!!

スクリーンショット 2020-10-06 6.24.29.png

うん、APIトークン認証のちょっと下にSPA認証の項ががっつり書いてあった。。

よくよく調べてみると、「Laravel×SanctumでSPA認証」みたいな記事をみると、ちゃんとこのSPA認証の内容が載ってる。。

(参考)Laravel×Sanctum によるSPA認証の記事

一番わかりやすかった記事
Laravel 7.x Sanctumの使い方!実例

SanctumじゃないしVueの記事だけど、SPAでアプリを作るチュートリアルでLaravel側の設定などはかなり参考になる
Vue + Vue Router + Vuex + Laravelで写真共有アプリを作ろう (4) 認証API

どうやら、SPAのように「フロントとバックの分離」をするときはトークン認証をするのが一般的?なようですが、SanctumではLaravelに組み込まれているセッション認証を使うようです。セッション認証だとCSRF保護が使えたりするので、トークン認証よりセキュリティ面で安心なようです(説明間違ってたらごめんなさい。。)

書き換えたトークン認証の処理を戻す

というわけで、頑張って書き換えたトークン認証の処理は必要ないということが分かりました。
セッション認証ならそんなに難しくないので、さくっとトークン認証から書き換えて一件落着。

まとめ

今回の個人開発、技術的な側面でいうと「Reactのアウトプット」がメインだったはずなのに、今のところほぼLaravelしか触ってない。サーバーサイドは使い慣れたLaravelで学習コストを抑えよう〜なんて思ってたけど、とんでもなかった。SPAの認証って難しい。

かなりもがき苦しみましたが、認証系をSPAに書き換える過程でHTTP通信についてもかなり勉強して理解が深まった気がします。自分でHTTPリクエストのヘッダーを書き換えたり追加したりなんてしたことなかった。

みんなのポートフォリオまとめサイト、いつになったら完成できるのか。
最初は9月いっぱいで作るとかふざけたこと言ってたような気がします。10月ですら無理そうなので、11月いっぱいを目標に変更しよう。

亀の歩みですが、認証系が一通り終わったのでSPA実装の最初の「0→1」フェーズは超えたと思ってます。
ここからは一気に加速して、、、いく、、、、はず

おわりに

Qiitaは不定期更新でやっておりますが、Twitter ( @kiwatchi1991 ) では日々感じたことや細かい試行錯誤の様子などを発信しております。

また、ブログを毎日更新していて(今は50日を超えたあたり)、数日に一度個人開発に関する記事を書いています。

38日目 個人開発記⑤-実装開始-
39日目 個人開発記⑥-思い込み-
44日目 個人開発記⑦-結局Laravel-

各論はそっちで細かく書いてたりもするので、よかったらこちらもぜひのぞいてみてください。

それではまた次回!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む