20210802のlaravelに関する記事は14件です。

Laravelの使える?ヘルパ関数

Laravel8のヘルパ関数で実際に使った使えたものをまとめる Laravelではグローバルに使える関数が多数あり、それらを使用することにより開発効率や可読性が向上します。 そのため用途にあったものがある場合、積極的に使用するようにしましょう。 以下では用途にまとめてヘルパ関数を記載しています。 パス mix() mix関数は、バージョンつけしたMixファイルのパスを取得します。 $path = mix('css/app.css'); storage_path() storage_path関数は、アプリケーションのstorageディレクトリへの完全修飾パスを返します。storage_path関数を使用して、ストレージディレクトリ内の特定のファイルへの完全修飾パスを生成することもできます。 $path = storage_path(); $path = storage_path('app/file.txt'); 文字列 Str::lower() Str::lowerメソッドは指定文字列を小文字に変換します。 use Illuminate\Support\Str; $converted = Str::lower('LARAVEL'); // laravel Str::upper() Str::upperメソッドは、指定文字列を大文字に変換します。 use Illuminate\Support\Str; $string = Str::upper('laravel'); // LARAVEL Str::substr() Str::substrメソッドは開始位置と文字列長の引数で指定した部分文字列を返します。 use Illuminate\Support\Str; $converted = Str::substr('The Laravel Framework', 4, 7); // Laravel Str::replace() Str::replaceメソッドは、文字列内の指定した文字列を置き換えます。 use Illuminate\Support\Str; $string = 'Laravel 8.x'; $replaced = Str::replace('8.x', '9.x', $string); // Laravel 9.x Fluent文字列 Fluent文字列はオブジェクト指向で、複数の文字列操作をチェーンできるインターフェイスを提供しています。 複数の文字列操作をする場合は、自然言語のように記述できるため、コードの可読性が上がります。 Str::of()の引数に対象の文字列を指定して使用します。 append appendメソッドは、指定値を文字列へ追加します。 use Illuminate\Support\Str; $string = Str::of('Taylor')->append(' Otwell'); // 'Taylor Otwell' contains containsメソッドは、指定された文字列に指定された値が含まれているかどうかを判別します。このメソッドは大文字と小文字を区別します。 use Illuminate\Support\Str; $contains = Str::of('This is my name')->contains('my'); // true is isメソッドは、指定文字列が指定パターンに一致するかどうかを判別します。アスタリスクはワイルドカード値として使用できます use Illuminate\Support\Str; $matches = Str::of('foobar')->is('foo*'); // true $matches = Str::of('foobar')->is('baz*'); // false trim trimメソッドは、文字列をトリムします。 use Illuminate\Support\Str; $string = Str::of(' Laravel ')->trim(); // 'Laravel' $string = Str::of('/Laravel/')->trim('/'); // 'Laravel' when whenメソッドは指定条件がtrueの場合、指定したクロージャを呼び出します。クロージャは、fluent文字列インスタンスを受け取ります。 use Illuminate\Support\Str; $string = Str::of('Taylor') ->when(true, function ($string) { return $string->append(' Otwell'); }); // 'Taylor Otwell' 必要であれば、3番目のパラメータとして別のクロージャをwhenメソッドに渡せます。このクロージャは、条件パラメータがfalseと評価された場合に実行します。 use Illuminate\Support\Str; $string = Str::of('Steve') ->when(false, function ($string) { return $string->append(' Otwell'); }, function ($string) { return $string->append(' Jobs'); }); // 'Steve Jobs' URL route() route関数は、指定した名前付きルートのURLを生成します。 $url = route('route.name'); ルートがパラメーターを受け入れる場合は、それらを関数の2番目の引数として渡すことができます。 $url = route('route.name', ['id' => 1]); デフォルトでは、route関数は絶対URLを生成します。相対URLを生成する場合は、関数の3番目の引数としてfalseを渡してください。 $url = route('route.name', ['id' => 1], false); その他 abort() abort関数は、例外ハンドラによりレンダーされるであろう、HTTP例外を投げます。 abort(403); ブラウザに送信する必要のある例外のメッセージとカスタムHTTP応答ヘッダを指定することもできます。 abort(403, 'Unauthorized.', $headers); abort_if() abort_if関数は、指定された論理値がtrueと評価された場合に、HTTP例外を投げます。 abort_if(! Auth::user()->isAdmin(), 403); abortメソッドと同様に、例外の応答テキストを3番目の引数として指定し、カスタム応答ヘッダの配列を4番目の引数として関数に指定することもできます。 abort_unless() abort_unless関数は、指定した論理値がfalseと評価された場合に、HTTP例外を投げます。 abort_unless(Auth::user()->isAdmin(), 403); abortメソッドと同様に、例外の応答テキストを3番目の引数として指定し、カスタム応答ヘッダの配列を4番目の引数として関数に指定することもできます。 collect() collect関数は、指定値からコレクションインスタンスを生成します。 $collection = collect(['taylor', 'abigail']); logger() logger関数は、debugレベルのメッセージをログへ書き出します。 logger('Debug message'); 関連情報の配列を関数へ渡すこともできます。 logger('User has logged in.', ['id' => $user->id]); 関数に値を渡さない場合は、ロガーインスタンスが返されます。 logger()->error('You are not allowed here.'); dd() dd関数は指定された変数の内容を表示し、スクリプトの実行を停止します。 dd($value); dd($value1, $value2, $value3, ...); スクリプトの実行を停止したくない場合は、代わりにdump関数を使ってください。 dump() dump関数は指定した変数をダンプします。 dump($value); dump($value1, $value2, $value3, ...); now() now関数は、現時点を表す新しいIlluminate\Support\Carbonインスタンスを生成します。 $now = now(); optional() optional関数はどんな引数も指定でき、そのオブジェクトのプロパティへアクセスするか、メソッドを呼び出せます。指定したオブジェクトがnullだった場合、エラーを発生させる代わりに、プロパティとメソッドはnullを返します。 return optional($user->address)->street; {!! old('name', optional($user)->name) !!} optional関数は、2番目の引数としてクロージャも受け入れます。最初の引数として指定された値がnullでない場合、クロージャが呼び出されます。 return optional(User::find($id), function ($user) { return $user->name; }); 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プログラミング初心者が書いたマイグレーションのやり方について

はじめに 現在Laravelを学習しているプログラミング初心者の僕が 今回はマイグレーションのやり方について記載していきたいと思います。 マイグレーションとは? マイグレーションとはデータベースのバージョン管理機能のことを指します。 PHPのスクリプトを使うことによりテーブルの作成処理などを用意することが出来るのです。 マイグレーションの手順としては以下のようになります。 マイグレーションファイルの作成 スクリプトの記述 マイグレーションの実行 マイグレーションファイルの作成~実行まで 現在作成しているプロジェクトに移動してから以下のコマンドを実行します。 (僕は今回peopleというファイルを作ります) $ php artisan make:migration cretate_people_table するとdatabaseフォルダのmigrationsフォルダの中に xxxxx_create_people_table.phpというファイルが新たに作られていると思います。 (xxxxxには作成した日時が書いてある) xxxxx_create_people_table.php <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreatePeopleTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('people', function (Blueprint $table) { $table->bigIncrements('id'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('people'); } } 続いては上記のコードにテーブル生成と削除の処理を記載していきます。 public function up() { Schema::create('people', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('name'); //追記 $table->string('mail'); //追記 $table->integer('age'); //追記 $table->timestamps(); }); } これでテーブルの生成は完了です。 テーブルの生成方法は下記の通りです。 public function up() { Schema::create('テーブル名', function (Blueprint $table) { $table->型('フィールド名'); }); } 続いてテーブルの削除処理となります。 削除処理のメソッドはすでにデフォルトで下記のコードが記述されています。 public function down() { Schema::dropIfExists('people'); //テーブルがあれば削除 } ここは特に変更するポイントは御座いません。 そして最後にこの作成したファイルを基にマイグレーションを実行します $ php artisan migrate うまく成功すると画像のようになります 個人的な感想ですが、マイグレーションがうまく決まった時の気持ち良さは結構好きです笑 ちなみにmigrateして上手くいかなかった場合はまとめて巻き戻すことも出来ます。 $ php artisan migrate:rollback 以上がマイグレーションの作成〜実行となります。 問題なく進めばあっという間に完成できるので良ければやってみてください! 参考記事 https://qiita.com/shosho/items/a5a5839735dfef9214b1 https://readouble.com/laravel/5.6/ja/migrations.html
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】package不足でcomposer installができない

既存のLaravelプロジェクトをインストールする工程で、composer installをしますが、 [ec2-user@ip-172-31-39-229 laravel]$ composer install PHP Warning: PHP Startup: Unable to load dynamic library 'pdo' (tried: /opt/remi/php74/root/usr/lib64/php/modules/pdo (/opt/remi/php74/root/usr/lib64/php/modules/pdo: cannot open shared object file: No such file or directory), /opt/remi/php74/root/usr/lib64/php/modules/pdo.so (/opt/remi/php74/root/usr/lib64/php/modules/pdo.so: cannot open shared object file: No such file or directory)) in Unknown on line 0 No composer.lock file present. Updating dependencies to latest instead of installing from lock file. See https://getcomposer.org/install for more information. Loading composer repositories with package information Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - doctrine/dbal[v2.10.0, ..., 2.13.x-dev] require ext-pdo * -> it is missing from your system. Install or enable PHP's pdo extension. - Root composer.json requires doctrine/dbal ^2.10 -> satisfiable by doctrine/dbal[v2.10.0, ..., 2.13.x-dev]. ... と出て、うまく動かなかった。 エラー文:パッケージが不足している ので、パッケージをインストールしていけばいいんですが、EC2だとインストールしたphpのバージョン毎に合わせて入れてあげる必要があります。 私の環境のphpのverionは7.4.22なので、 [ec2-user@ip-172-31-39-229 laravel]$ php -v PHP 7.4.22 (cli) (built: Jul 27 2021 18:08:31) ( NTS ) Copyright (c) The PHP Group Zend Engine v3.4.0, Copyright (c) Zend Technologies php74-php-pdoをインストールすればいいのです。 [ec2-user@ip-172-31-39-229 laravel]$ sudo yum install -y php74-php-pdo 以上です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelでどの項目がバリデーションにかかっているかわからなくなった時

LaravelのBladeで、入力項目が多すぎて、どの項目がバリデーションに引っかかっているかわからなくなった。 以下のコードをbladeに埋め込むと該当の入力箇所が判別する、 @foreach ($errors->all() as $error) {{$error}} @endforeach
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

意味があるのか無いのかわからないものが作りたくて

何かを作る時のアイデアがポンポンでてくる人はホントに羨ましい。天才なんだろうか。怪物かお化け? 少し前から何か作りたい、できれば面白いものを。と思いつつも、何も良いアイデアが思い浮かばなかった。「アイデアの出し方」みたいな本を読んでみたりもしたけどどうしても見つからない。そんな時に見つけたのが孫正義の発明ノート。 1冊目の単語帳に「時計」、2冊目の単語帳に「冷蔵庫」と出たら、「時計付き冷蔵庫」「冷蔵庫付き時計」の可能性を考えるという具合です。「これはいける!」という可能性のあるアイデアが出るまで続けます。 「なるほどー。複数のカードに単語を書いて、それを組み合わせて考えていけばアイデア出るのかー。じゃあそれを作ってみようかな」 とりあえず作ってみた。 ガチャガチャ英単語とガチャガチャ日本語!!! 使い方は簡単。ガチャガチャボタンを押して、単語が3つ出てくるので、組み合わせて文章を作ってツイートする。 意味があるのか無いのかわからないけどガチャガチャ英単語は割と意味があるんじゃないかと思う。文章を考えるときは英単語の意味を知っていないといけないし、知らなかったら調べる必要がある。必要に応じて過去形にしたり、未来形にしたり、考えることが必要だから。だから、割と簡単な動詞を中心に登録しています。Twitterに投稿をゴールとした場合、単語が多すぎると140文字以内は難しいかと思ったので3単語に設定しました。 ドメインを取ろうか悩んだけど、以前つくった5秒英作文と関連があるしドメイン取らずにそのまま引っ付けて作りました。フロントはVueでバックはLaravel。非同期でLaravelからのデータを渡せるのか、どうやってデータをTwitterに渡すのかも知らなかったので、作る前はできるか少し不安だったけど思ったよりも全然簡単でサクッとできました。まだTwitterに渡す部分でwarningがでてる(´;ω;`)動くけども(´;ω;`) 改善がんばります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelでコマンドライン(Command)処理のテストを書く

経緯 ・ Laravelにて、バッチ処理を書くにあたって、Commandで実装してgithub actionsで定期実行する形にしようと考えてバッチ処理実装 ・ テスト書いてないとかお前それ t_wadaの前でも。。。があるので、テストももちろん書く! ・ マニュアルにあるように https://readouble.com/laravel/8.x/ja/console-tests.html $this->artisan('testBatch') ->expectsOutput('batch complete') ->assertExitCode(0); と書く。 ・ おーいけてるいけてる。じゃあDBの値の変更結果も確認しよう。 $this->artisan('testBatch') ->expectsOutput('batch complete') ->assertExitCode(0); $this->assertEquals(Hoge::all()->count(), 1); FAILURES! testBatchの中で1行レコードが追加されるはずなのに何で!?!?!? 結論 ->run()で処理を明示的に実行させないとダメだった。 修正後 $this->artisan('testBatch') ->expectsOutput('batch complete') ->assertExitCode(0) ->run(); $this->assertEquals(Hoge::all()->count(), 1); run()の記述が無い場合、プログラムの最後でrun()が実行されるようで、command処理実行後の値を取得、アサーションする場合は明示的にrun()を呼び出す必要があるみたい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LaravelでArtisanコマンド(コマンドライン(Command))処理のテストを書く

経緯 ・ Laravelにて、バッチ処理を書くにあたって、Commandで実装してgithub actionsで定期実行する形にしようと考えてバッチ処理実装 ・ テスト書いてないとかお前それ t_wadaの前でも。。。があるので、テストももちろん書く! ・ マニュアルにあるように https://readouble.com/laravel/8.x/ja/console-tests.html $this->artisan('testBatch') ->expectsOutput('batch complete') ->assertExitCode(0); と書く。 ・ おーいけてるいけてる。じゃあDBの値の変更結果も確認しよう。 $this->artisan('testBatch') ->expectsOutput('batch complete') ->assertExitCode(0); $this->assertEquals(Hoge::count(), 1); FAILURES! testBatchの中で1行レコードが追加されるはずなのに何で!?!?!? 結論 run()で処理を明示的に実行させないとダメだった。 修正後 $this->artisan('testBatch') ->expectsOutput('batch complete') ->assertExitCode(0) ->run(); $this->assertEquals(Hoge::count(), 1); run()の記述が無い場合、プログラムの最後でrun()が実行されるようで、command処理実行後の値を取得、アサーションする場合は明示的にrun()を呼び出す必要があるみたい。 参考 ・PendingCommandの__destruct()でrun()が実行される。 https://github.com/laravel/framework/blob/8.x/src/Illuminate/Testing/PendingCommand.php#L361 ・execute()はrun()のエイリアス https://github.com/laravel/framework/blob/8.x/src/Illuminate/Testing/PendingCommand.php#L200
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】Routingがあっているはずなのに404となった場合の対処法

概要 ブランチ等を切り替えているうちに、Routing登録済みのページで404となる現象が発生したためその時の対処法を記録 開発環境 Docker PHP 8.0.8 Laravel 8.32.1 対処法 正しいroutingが登録されていない可能性があるため、下記の手順で確認&修正します。 1.   登録されているroutingを確認 php artisan route:list 2. 404が出るページのrouting登録が1のリストで表示されない(または正しいものでない)場合、cacheを削除 php artisan route:clear 3. 再度1のコマンドを打ち、正しいroutingとなったことを確認して該当ページの表示確認を実施
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

久々にLaravelを触ったら、コンテナやフロントエンド開発が簡単になっていた

最近、ちょっと時間ができたのでキャッチアップのために Laravel8を触ってみました。 知識がLaravel5前後で止まっていたので、進化したエコシステムに驚いたので書き記しておきます。 ちなみに、次のLTSはLaravel9(2022/1/25リリース予定)になるので、この手の機能をプロダクションで使うときは、しかるべき時にアップデートをしましょう。 コンテナを使っての開発環境構築が楽になってる!(Laravel Sail) ちょっと前まではコンテナでLaravelを動かす時には自分で適当なDockerFileを書くか、Laradoc使って構築するケースが多かったと思います。 Laradockは大きくなりすぎて余計な機能もあったりしたので、私は自前でDockerFileを書く派でした。これだけでも、ちょっと「面倒くさいな。。。」なんて思っていました。 ここでLaravel Sailの登場です。 Sailを使っての環境構築は、 $ curl -s "https://laravel.build/[ディレクトリ名]" | bash $ cd [ディレクトリ名] # バックグラウンドで動かすなら、-dを付ければ OK # 下記の様にaliasを設定しておくとsailコマンドを使うのが簡単になる # alias sail='bash vendor/bin/sail' $ ./vendor/bin/sail up たったこれだけで、PHP + MySql + mailhog + redis + selenium を使ったプロジェクトが立ち上がります。 seleniumはブラウザテストの時なんかに便利ですね。 # 立ち上げたときの表示 $ sail up -d lara-sample_laravel.test_1 start-container Exit 255 0.0.0.0:80->80/tcp,:::80->80/tcp, 8000/tcp Shutting down old Sail processes... Creating network "lara-sample_sail" with driver "bridge" Creating lara-sample_mysql_1 ... done Creating lara-sample_selenium_1 ... done Creating lara-sample_mailhog_1 ... done Creating lara-sample_meilisearch_1 ... done Creating lara-sample_redis_1 ... done Creating lara-sample_laravel.test_1 ... done ちなみに、 sail artisan sail:publish でDockerFileなどが編集できる様になるので、独自のPHP拡張などを入れるのも簡単ですね React(Vueも)+認証の構築が楽になってる!(Laravel Breeze) とりあえず、Sailを使ってプロジェクトが動かせる様になったら認証を作って行きます。 元々はlaravel/uiがあったのですが、こちらか Jetstreamを使っていく方針の様です。 細かな認証の組み込みが不要な場合や初学者にはBreezeがお薦めですね。 認証をReactやVueで作っていくためには、 php artisan breeze:install vue # Vueの場合はこちら php artisan breeze:install react npm install npm run dev php artisan migrate で導入が可能です。ただ、初期インストール時に作成されるのはJavaScriptになるのでTypeScriptを使う時には下記の修正が必要です。 * webpack.mix.jsの修正 * tsconfig.jsonの作成 * resources/js配下のTypeScript化 下記の様にオプションをつけられる様になったら、もっと楽になると思っています。 # こちらはできません! php artisan breeze:install react --typescript ただ、非公式ですがBreeze + React + TypeScriptのGitHubリポジトリがあるので問題なく対処できるのではないでしょうか。 inertia.jsの衝撃(modern monolithとの出会い) Breezeを使ったReactの導入にはinertia.jsが使われています。 Build single-page apps, without building an API.(SPAをAPIを使うことなく構築できる) ということで、Reactを使ったSPAのルーティング、サーバサイドとのデータの受け渡しなどをbladeと同じ様な感覚で行うことができる様になっています。 おなじみのroutes/web.phpのルーティング情報を使えるのので、メンテナンスを行う上で非常に楽になると思います。 (下記は会員登録画面のサンプル。post(route('register'));が、そこに該当します。LaravelからはProps経由で値を受け取れています。) import Button from '@/Components/Button'; import Guest from '@/Layouts/Guest'; import Input from '@/Components/Input'; import Label from '@/Components/Label'; import React, { useEffect } from 'react'; import ValidationErrors from '@/Components/ValidationErrors'; import { InertiaLink } from '@inertiajs/inertia-react'; import { useForm } from '@inertiajs/inertia-react'; import route from 'ziggy-js'; type Props = { element1: string } export default function Register({ element1 }: Props) { const { data, setData, post, processing, errors, reset } = useForm({ name: '', email: '', password: '', password_confirmation: '', }); useEffect(() => { return () => { reset('password', 'password_confirmation'); }; }, []); const onHandleChange = (event: React.ChangeEvent<HTMLInputElement>) => { setData(event.target.name as "name" | "email" | "password" | "password_confirmation", event.target.type === 'checkbox' ? event.target.checked + '' : event.target.value); }; const submit = (e: React.SyntheticEvent) => { e.preventDefault(); post(route('register')); }; return ( <Guest> <ValidationErrors errors={errors} /> <form onSubmit={submit}> <div> <Label forInput="name" value="Name" /> {element1} <Input type="text" name="name" value={data.name} className="mt-1 block w-full" autoComplete="name" isFocused={true} handleChange={onHandleChange} required /> </div> <div className="mt-4"> <Label forInput="email" value="Email" /> <Input type="email" name="email" value={data.email} className="mt-1 block w-full" autoComplete="username" handleChange={onHandleChange} required /> </div> <div className="mt-4"> <Label forInput="password" value="Password" /> <Input type="password" name="password" value={data.password} className="mt-1 block w-full" autoComplete="new-password" handleChange={onHandleChange} required /> </div> <div className="mt-4"> <Label forInput="password_confirmation" value="Confirm Password" /> <Input type="password" name="password_confirmation" value={data.password_confirmation} className="mt-1 block w-full" handleChange={onHandleChange} required /> </div> <div className="flex items-center justify-end mt-4"> <InertiaLink href={route('login')} className="underline text-sm text-gray-600 hover:text-gray-900"> Already registered? </InertiaLink> <Button className="ml-4" processing={processing}> Register </Button> </div> </form> </Guest> ); } フロントエンドとサーバサイドを分業せずに開発しているプロダクトなどでは非常に有用に思います。 他にもリリースノートを見ると Models Directoryの導入(結局作るんかい!と思いましたが) Migration Squashing(マイグレーションを圧縮してディレクトリの肥大化を防ぐ) などなど、面白そうな機能がたくさんありました! わずか1年強、キャッチアップから離れていただけなのですが進化の速さに驚かされます。 勉強は続けないといけないですね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelでログイン機能を入れる

1 アプリのバックアップをしておく 2 認証機能をインストールする 1 Laravel/ulパッケージをインストールする ターミナル アプリ$composer require laravel/ui 2 パッケージを用いて認証機能を追加する ターミナル アプリ$php artisan ui vue --auth ターミナル(結果) アプリ$do you want~ *ここはyesにする! 3.CSSを適用させる ターミナル アプリ$npm run dev →完成!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ハンズオン】Nuxt.jsとLaravelとLINE Front-end Frameworkを使ってECサイトを作ろう!

はじめに 前回LINE Front-end Framework(LIFF)でお問い合わせフォームを作成しました。 今回は、Nuxt.jsとLaravelを使ってECサイトを作ってみます。 開発環境 今回は以下の技術を採用しています。 フロントエンド バックエンド インフラ サービス LIFF Laravel Docker HubSpot Nuxt.js Netlify Stripe Heroku MicroCMS Dockerは環境構築のために使用しています。 デプロイに関しては、フロントエンドはNetlifyを使い、バックエンドはHerokuを使用しています。 HubSpotは顧客管理、Stripeは決済、MicroCMSはECで販売する商品を登録するために使用しています。 本業でカスタマーサクセスをやっているのでやっぱり顧客管理はCRM使いたいよねってことでHubSpotを選択しています。 Stripeも最初はpayment-linksを使いたかったですが、コード書くならcheckoutでいいよねってことで使いませんでした。 payment-linksはどういうタイミングで使えばいいんでしょうね? MicroCMSは、わざわざ商品を登録するサイトを作るのがめんどくさかったので採用しました。 ECサイトを作る際にも普通に要件次第では使えるのではないかなーと感じました。 完成コード サイトURLはこちらです。 このサイトはLINEログインしないと使えないので、LINEログインをするためにLINEブラウザで開いてください。 ハンズオン! Githubからクローン GitHubにやり方は書いていますが、以下を実行することで環境構築ができます。 ターミナル $ git clone git@github.com:ssk9597/Docker-Laravel-Nuxt-Nginx-MySQL.git $ cd Docker-Laravel-Nuxt-Nginx-MySQL $ make nuxt $ make backend こちらは以下の記事を参考にしてください。 frontend/nuxt.config.jsに関してはこのままだとエラーが発生します。 なのでここだけは以下のコードをコピペしてください。 frontend/nuxt.config.js require('dotenv').config(); const { API_URL } = process.env; export default { head: { title: 'frontend', htmlAttrs: { lang: 'ja', }, meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { hid: 'description', name: 'description', content: '' }, ], link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }], }, css: [], plugins: [], components: true, buildModules: [], watchers: { webpack: { poll: true, }, }, modules: ['@nuxtjs/axios', '@nuxtjs/proxy', '@nuxtjs/dotenv'], env: { API_URL, }, proxy: { '/api': process.env.API_URL, }, axios: { baseURL: process.env.API_URL, browserBaseURL: process.env.API_URL, }, build: {}, }; NetlifyとHerokuにデプロイを行う Netlify 以下の記事で詳細まで説明されているのでこちらを見てもらえれば問題なくできるかと思います。 また、ステージング環境に関してもやっておいた方がいいです。 Heroku Herokuのデプロイは結構躓きがちなので自分が参考にした資料を載せておきます。 今回は複数のbuildpackが必要になるので以下を参考にしましょう。 ほとんどこちらの資料だけでできるはずです。 Papertrailでログも出しておきたいのでこれもやっておきましょう。 LIFFはフロント側でログを出すには、alertしか使えないですし、alertにオブジェクトなどが表示できないなど制限があります。 なので、Papertrailを使ってバックエンド側でログを出しておくのがいいかと思います。 最後に通信できているか確認しましょう。 以下の記事のAxiosの部分を追加しましょう。 これでHello World!が表示されればOKです。 サイト内で使用する画像を準備する 私は以下の2サイトから持ってきました。 まぁ正直なんでもいいです。 LINE Developersでチャネルを作成する 作成手順 ①プロバイダーの作成 ②新規チャネルの作成 ③LIFFの作成 プロバイダーを作成します。 プロバイダー名はご自身のお名前でいいかと思います。 新規チャネルを作成します。 LINEログインを選択してください。 設定を行います。 ウェブページにLINEログインを組み込むので、アプリタイプはウェブアプリにしておいてください。 ではLIFFアプリを追加しましょう! 基本情報の設定は以下のように行います。 これでLIFFアプリの作成ができました! Netlifyに.envを登録する 先ほど作成したLIFFアプリのチャネルIDとLIFF IDを.envに登録します。 それでは登録を行います。 LIFFアプリのタイトルを設定する サイトタイトルをつけますが、こちらは任意の名前でOKです。 私は、LIFF's Shopにしています。 frontend/nuxt.config.js require('dotenv').config(); const { API_URL } = process.env; export default { target: 'static', head: { + title: "LIFF's Shop", htmlAttrs: { lang: 'ja', }, meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { hid: 'description', name: 'description', content: '' }, ], link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }], }, plugins: [], components: true, buildModules: [], watchers: { webpack: { poll: true, }, }, modules: ['@nuxtjs/axios', '@nuxtjs/proxy', '@nuxtjs/dotenv', '@nuxtjs/style-resources'], env: { API_URL, }, proxy: { '/api': process.env.API_URL, }, axios: { baseURL: process.env.API_URL, browserBaseURL: process.env.API_URL, }, build: {}, }; LIFFアプリにLIFF SDKを組み込む npmパッケージもあるのですがうまくいかないことも多いのでSDKを使います。 frontend/nuxt.config.js require('dotenv').config(); const { API_URL } = process.env; export default { target: 'static', head: { title: "LIFF's Shop", htmlAttrs: { lang: 'ja', }, meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { hid: 'description', name: 'description', content: '' }, ], link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }], + script: [{ src: 'https://static.line-scdn.net/liff/edge/2/sdk.js' }], }, plugins: [], components: true, buildModules: [], watchers: { webpack: { poll: true, }, }, modules: ['@nuxtjs/axios', '@nuxtjs/proxy', '@nuxtjs/dotenv', '@nuxtjs/style-resources'], env: { API_URL, }, proxy: { '/api': process.env.API_URL, }, axios: { baseURL: process.env.API_URL, browserBaseURL: process.env.API_URL, }, build: {}, }; .envのLIFF_IDなどをnuxt.config.jsで読み取れるようにする 先ほどNetlifyに登録したenvをフロントエンドで使えるようにしましょう! frontend/nuxt.config.js require('dotenv').config(); + const { API_URL, LIFF_ID, LIFF_CHANNEL_ID, MICROCMS_API_KEY } = process.env; export default { target: 'static', head: { title: "LIFF's Shop", htmlAttrs: { lang: 'ja', }, meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { hid: 'description', name: 'description', content: '' }, ], link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }], script: [{ src: 'https://static.line-scdn.net/liff/edge/2/sdk.js' }], }, css: [{ src: '@/assets/styles/style.scss', lang: 'scss' }], styleResources: { scss: ['@/assets/styles/style.scss'], }, plugins: [], components: true, buildModules: [], watchers: { webpack: { poll: true, }, }, modules: ['@nuxtjs/axios', '@nuxtjs/proxy', '@nuxtjs/dotenv', '@nuxtjs/style-resources'], env: { API_URL, LIFF_ID, + LIFF_CHANNEL_ID, + MICROCMS_API_KEY, }, proxy: { '/api': process.env.API_URL, }, axios: { baseURL: process.env.API_URL, browserBaseURL: process.env.API_URL, }, build: {}, }; リセットCSSを導入する いろいろ選択肢がありますが私は、css-wipeを使っています。 html5doctorのHTML5 Resetをベースに作られたスタイルシートです。 「box-sizing: border-box;」が定義されており、すべてのmargin, padding, borderが消されています。 導入方法はめちゃくちゃ簡単です。 ターミナル $ npm install css-wipe --save layoutのCSSに1行入れるだけでOKです。 frontend/layouts/default.vue <style lang="scss"> + @import 'css-wipe'; </style> エラー発生 ここまでをコミットした場合以下のエラーが発生するはずです。 発生しなかった人はそのままスルーで! Node Sass version 6.0.1 is incompatible with ^4.0.0 || ^5.0.0. 「node-sassのバージョン6はたけーぞ、4か5に下げろや」と怒られるはずなので、対応しましょう。 ターミナル $ npm uninstall -D node-sass $ npm install -D node-sass@5.0.0 これでOKです。 再度コミットしてください! 多分エラーは消えるはずです。 LIFFアプリを初期化する liff.init()メソッドを実行すると、LIFFアプリが初期化され、LIFFアプリからLIFF SDKのほかのメソッドを実行できるようになります。 アクセスした際に初期化されるようにしたいので、pages/index.vue内で読み込むことにしましょう。 frontend/pages/index.vue <template> <div> </div> </template> <script> export default { + async mounted() { + await liff.init({ + liffId: process.env.LIFF_ID, + }); + }, }; </script> まずはフロントエンド側のデザインをやっていきましょう! 完成予想図は以下のような感じです。 構成としては4つのブロックになっています。 ブロック ヘッダー ヒーロー 会員登録 商品 必要なコンポーネントです。 Atoms Molecules Template FailAlert Header Cancel Heading Hero Index LinkButton Registration Success SuccessAlert それでは必要なコンポーネントを作りつつ、そのコンポーネントをなぜ使用するのかも解説していきます。 ただその前に今回は変数定義用のscssファイルを作りましょう。 今回は使用する色に限りがあるのでこちらをscssで管理していきます。 frontend/assets/styles/utility/_color.scss $color_black: #2a2b2f; $color_white: #fff; $color_deep_gray: #979797; $color_gray: #bfbfbe; $color_yellow: #f5c500; $color_success: #4caf51; $color_fail: #e57472; frontend/assets/styles/style.scss @import "utility/color"; では最後にnuxt.config.jsに読み込みましょう。 frontend/nuxt.config.js require('dotenv').config(); const { API_URL, LIFF_ID, LIFF_CHANNEL_ID } = process.env; export default { target: 'static', head: { title: "LIFF's Shop", htmlAttrs: { lang: 'ja', }, meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { hid: 'description', name: 'description', content: '' }, ], link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }], script: [{ src: 'https://static.line-scdn.net/liff/edge/2/sdk.js' }], }, + css: [{ src: '@/assets/styles/style.scss', lang: 'scss' }], + styleResources: { + scss: ['@/assets/styles/style.scss'], + }, plugins: [], components: true, buildModules: [], watchers: { webpack: { poll: true, }, }, modules: ['@nuxtjs/axios', '@nuxtjs/proxy', '@nuxtjs/dotenv', '@nuxtjs/style-resources'], env: { API_URL, LIFF_ID, LIFF_CHANNEL_ID, }, proxy: { '/api': process.env.API_URL, }, axios: { baseURL: process.env.API_URL, browserBaseURL: process.env.API_URL, }, build: {}, }; これでOKです。 それではコンポーネントの作成に移ります! ⚠️注意点 完成形のコードをすべて記載しました。 API側のやり取りなど解説は後ほど行うこととします。 なのでこちらはすべてコピペしていただければOKです。 FailAlert.vue なぜ作るのか こちらは会員登録に失敗したときに表示されるアラートです。 要件としては3秒間表示されて勝手に消えるようにします。 では作っていきます frontend/components/Atoms/FailAlert.vue <template> <div class="alert"> <p class="alert-title">ログインに失敗しました</p> </div> </template> <script> export default {}; </script> <style lang="scss" scoped> .alert { position: fixed; z-index: 100; background: $color_fail; width: 100%; max-width: 390px; height: 50px; display: flex; justify-content: center; align-items: center; &-title { font-size: 12px; color: $color_white; } } </style> Heading.vue なぜ作るのか 見出しとして使用します。 では作っていきます frontend/components/Atoms/Heading.vue <template> <div class="heading"> <h2 class="heading-main">{{ main }}</h2> <p class="heading-sub">{{ sub }}</p> </div> </template> <script> export default { props: { main: { type: String, required: true, }, sub: { type: String, required: true, }, }, }; </script> <style lang="scss" scoped> .heading { margin: 60px 0 30px 0; text-align: center; &-main { font-size: 32px; margin-bottom: 10px; } &-sub { font-size: 16px; } } </style> LinkButton.vue なぜ作るのか ページ移動を伴うボタンです。 TOPページに戻るために使用します。 では作っていきます frontend/components/Atoms/LinkButton.vue <template> <div class="button"> <nuxt-link to="/" class="button-btn">トップに戻る</nuxt-link> </div> </template> <script> export default {}; </script> <style lang="scss" scoped> .button { margin: 40px auto 0 auto; width: 200px; height: 40px; background: $color_yellow; border-color: transparent; &-btn { line-height: 40px; font-size: 18px; color: $color_black; text-decoration: none; font-weight: bold; } } </style> SuccessAlert.vue なぜ作るのか こちらは会員登録に成功したときに表示されるアラートです。 要件としては3秒間表示されて勝手に消えるようにします。 では作っていきます frontend/components/Atoms/SuccessAlert.vue <template> <div class="alert"> <p class="alert-title">ログインに成功しました</p> </div> </template> <script> export default {}; </script> <style lang="scss" scoped> .alert { position: fixed; z-index: 100; background: $color_success; width: 100%; max-width: 390px; height: 50px; display: flex; justify-content: center; align-items: center; &-title { font-size: 12px; color: $color_white; } } </style> これでAtomsは完成です。 次に、Moleculesを作成していきます。 Header.vue なぜ作るのか サイトのヘッダーデザインです。 では作っていきます frontend/components/Molecules/Header.vue <template> <header class="header"> <p class="header-title">LIFF's Shop</p> </header> </template> <script> export default {}; </script> <style lang="scss" scoped> .header { position: fixed; background: $color_black; width: 100%; max-width: 390px; height: 50px; display: flex; justify-content: center; align-items: center; &-title { font-size: 20px; color: $color_white; font-weight: bold; } } </style> Hero.vue なぜ作るのか サイトのヒーローデザインです。 では作っていきます frontend/components/Molecules/Hero.vue <template> <div> <div class="blank-space"></div> <div class="hero"> <img class="hero-img" src="@/assets/images/top.png" alt="ヒーロー" /> </div> </div> </template> <script> export default {}; </script> <style lang="scss" scoped> .blank-space { padding-top: 50px; } .hero { height: 300px; &-img { width: 390px; height: 300px; } } </style> Registration.vue なぜ作るのか 会員登録のデザインです。 こちらには会員登録したユーザーむけに、データの活用方法に関して記載しています。 こちらの記述をすることで、LIFFログインでメールアドレスを取得することができます。 では作っていきます frontend/components/Molecules/Registration.vue <template> <div class="registration"> <Heading :main="'Registration'" :sub="'会員登録'" /> <div class="registration-container"> <p class="registration-container-text"> 本Webサービスでは、ログイン時の認証画面にて許可を頂いた場合のみ、あなたのLINEアカウントに登録されているメールアドレスを取得します。 </p> <p class="registration-container-text"> 取得したメールアドレスは、HubSpotにユーザー登録するIDとして利用する目的以外では使用いたしません。また、法令に定められた場合を除き、第三者への提供はいたしません。 </p> <div class="registration-container-image"> <img src="@/assets/images/line.png" alt="LINEログイン" @click="childLineLogin" /> </div> </div> </div> </template> <script> // components import Heading from '@/components/Atoms/Heading'; export default { components: { Heading, }, props: { lineLogin: { type: Function, required: true, }, }, methods: { childLineLogin() { this.lineLogin(); }, }, }; </script> <style lang="scss" scoped> .registration-container { width: 250px; margin: 0 auto; background: $color_white; padding: 20px; &-text { font-size: 0.5rem; line-height: 1.5; padding-bottom: 10px; } &-image { padding-top: 20px; text-align: center; } } </style> LINEログインの時に「ユーザーのメールアドレス」を取得できるようにしましょう ということでここまでをgithubに、コミット、プッシュしましょう。 そしてこの情報取り扱いに関する記述をした部分をスクショしましょう! それでは、LINEにメールアドレス取得権限を付与してもらいましょう。 結構すぐに権限がもらえるはずです。 画像認識等をしているんでしょうね〜〜 「申請済み」になったらOKなので、スコープを付与しましょう。 これでLINEログインの際にメールアドレスが取得できるようになりました! これでMoleculesは完成です。 次に、Templateを作成していきます。 Cancel.vue なぜ作るのか Stripeの決済を行う際に、キャンセルページが必要なため作成します。 では作っていきます frontend/components/Template/Cancel.vue <template> <div class="container"> <Header /> <div class="message"> <p class="message-text">商品購入に失敗しました。</p> <p class="message-text">もう一度ご確認ください。</p> <LinkButton /> </div> </div> </template> <script> // components import Header from '@/components/Molecules/Header'; import LinkButton from '@/components/Atoms/LinkButton'; export default { components: { Header, LinkButton, }, }; </script> <style lang="scss"> .container { margin: 0 auto; background: $color_gray; height: 100vh; width: 100%; max-width: 390px; padding-bottom: 60px; } .message { padding-top: 70px; &-text { padding: 0 10px; line-height: 1.5; } } </style> では、キャンセルページの作成をしましょう。 frontend/pages/cancel.vue <template> <div> <Cancel /> </div> </template> <script> // components import Cancel from '@/components/Template/Cancel'; export default { components: { Cancel, }, }; </script> Index.vue なぜ作るのか TOPページです。 こちらでは、MicroCMSに登録した商品を受け取る処理を記載する必要があります。 なので、まずはMicroCMSの商品登録を行いましょう。 MicroCMSへ商品登録を行う まずはアカウントを作成しましょう。 作成方法に関することは公式サイトのブログがわかりやすいのでこちらを参考にされてください。 下記のようなスキーマを作成していきます。 Stripeの商品料金API IDは後ほど取得しますので今は適当な値を埋めておきましょう。 ということでこんな感じで3つ程度商品登録をしましょう! APIキーも必要になるので取得してNetlifyに登録しましょう。 では登録しましょう。 では作っていきます MicroCMSのデータを取得していきましょう。 asyncDataはpagesディレクトリでないとできないのでまずはpages/index.vueを作成します。 frontend/pages/index.vue <template> <div> <Index :products="products" /> </div> </template> <script> // components import Index from '@/components/Template/Index'; export default { components: { Index, }, + async asyncData({ $axios }) { + const products = await $axios.$get('https://liff-nuxt-laravel.microcms.io/api/v1/product', { + headers: { 'X-API-KEY': process.env.MICROCMS_API_KEY }, + }); + return { products }; }, data() { return { products: '', }; }, async mounted() { await liff.init({ liffId: process.env.LIFF_ID, }); }, }; </script> では、Templateを作成します。 frontend/components/Template/Index.vue <template> <div class="container"> <div v-if="isSuccess"> <SuccessAlert /> </div> <div v-if="isFail"> <FailAlert /> </div> <Header /> <Hero /> <Registration :lineLogin="lineLogin" /> <div class="product"> <Heading :main="'Product'" :sub="'商品'" /> <div class="card"> <div class="card-container" v-for="product in products.contents" :key="product.id"> <img class="card-product-img" :src="product.image.url" /> <p class="card-product-name">{{ product.name }}</p> <p class="card-product-price">¥{{ product.price }}</p> <div class="button"> <button v-if="idToken" class="button-btn" @click="checkout(product)">購入する</button> <button v-else class="button-btn disabled" disabled>購入する</button> </div> </div> </div> </div> </div> </template> <script> // components import Heading from '@/components/Atoms/Heading'; import SuccessAlert from '@/components/Atoms/SuccessAlert'; import FailAlert from '@/components/Atoms/FailAlert'; import Header from '@/components/Molecules/Header'; import Hero from '@/components/Molecules/Hero'; import Registration from '@/components/Molecules/Registration'; export default { components: { Heading, SuccessAlert, FailAlert, Header, Hero, Registration, }, props: { products: { type: Object, required: true, }, }, data() { return { idToken: '', isSuccess: false, isFail: false, }; }, updated() { setTimeout(() => { this.isSuccess = false; }, 3000); setTimeout(() => { this.isFail = false; }, 3000); }, methods: { async lineLogin() { try { // IDトークン this.idToken = liff.getIDToken(); if (this.idToken) { await this.$axios.$post('/hubspot/store', { idToken: this.idToken, }); this.isSuccess = true; } else { this.isFail = true; } } catch (err) { alert(err); alert(err.response.data.error); alert(err.response.data.error_description); } }, async checkout(product) { const url = await this.$axios.$post('/stripe/store', { productStripePriceApi: product.stripe_price_api, idToken: this.idToken, }); if (url) { window.location.href = url; } }, }, }; </script> <style lang="scss"> .container { margin: 0 auto; background: $color_gray; width: 100%; max-width: 390px; padding-bottom: 60px; } .card { &-container { border-radius: 10px; background: $color_white; width: 250px; height: 300px; margin: 0 auto 30px auto; } &-product { &-img { border-radius: 10px 10px 0 0; width: 250px; height: 150px; } &-name { padding: 20px 0 0 20px; } &-price { padding: 20px 0 20px 20px; font-weight: bold; } } } .button { text-align: center; &-btn { display: inline-block; width: 200px; height: 40px; font-size: 18px; font-weight: bold; background: $color_yellow; border-color: transparent; &.disabled { color: $color_deep_gray; background: $color_gray; } } } </style> 本当はCardの部分もコンポーネント化してあげる方がいいのですが今回はデータのやり取りがめんどくさかったので諦めましたw Success.vue なぜ作るのか Stripeの決済を行う際に、サクセスページが必要なため作成します。 では作っていきます frontend/components/Template/Success.vue <template> <div class="container"> <Header /> <div class="message"> <p class="message-text">ご購入ありがとうございます!</p> <p class="message-text">ご不明点がありましたらお気軽にご連絡ください。</p> <LinkButton /> </div> </div> </template> <script> // components import Header from '@/components/Molecules/Header'; import LinkButton from '@/components/Atoms/LinkButton'; export default { components: { Header, LinkButton, }, }; </script> <style lang="scss"> .container { margin: 0 auto; background: $color_gray; height: 100vh; width: 100%; max-width: 390px; padding-bottom: 60px; } .message { padding-top: 70px; &-text { padding: 0 10px; line-height: 1.5; } } </style> では、サクセスページの作成をしましょう。 frontend/pages/success.vue <template> <div> <Success /> </div> </template> <script> // components import Success from '@/components/Template/Success'; export default { components: { Success, }, }; </script> これでフロントエンドの開発は終了です。 ではこの後はLINEログインをやっていきましょう。 LINEログインでプロフィールを取得する LIFFでのログインは、liff.getDecodedIDToken()が一番簡単です。 これで名前もメールアドレスもすべて取得することができます。 しかし、ユーザー情報をサーバーに送信しないでくださいとあるようにセキュリティの問題があります。 ということで、以下のように行います。 フロントエンド バックエンド ①IDトークンを取得 ②IDトークンをサーバーに渡す ③IDトークンを検証して、ユーザーのプロフィールを取得する ということでやっていきましょう。 ①IDトークンを取得 IDトークンの取得方法は簡単です。 frontend/components/Template/Index.vue <template> <div class="container"> <Registration :lineLogin="lineLogin" /> </div> </template> <script> // components import Registration from '@/components/Molecules/Registration'; export default { components: { Registration, }, data() { return { idToken: '', }; }, methods: { async lineLogin() { try { // IDトークン + this.idToken = liff.getIDToken(); } catch (err) { alert(err); alert(err.response.data.error); alert(err.response.data.error_description); } }, }, }; </script> frontend/components/Molecules/Registration.vue <template> <div class="registration"> <Heading :main="'Registration'" :sub="'会員登録'" /> <div class="registration-container"> <p class="registration-container-text"> 本Webサービスでは、ログイン時の認証画面にて許可を頂いた場合のみ、あなたのLINEアカウントに登録されているメールアドレスを取得します。 </p> <p class="registration-container-text"> 取得したメールアドレスは、HubSpotにユーザー登録するIDとして利用する目的以外では使用いたしません。また、法令に定められた場合を除き、第三者への提供はいたしません。 </p> <div class="registration-container-image"> <img src="@/assets/images/line.png" alt="LINEログイン" @click="childLineLogin" /> </div> </div> </div> </template> <script> // components import Heading from '@/components/Atoms/Heading'; export default { components: { Heading, }, props: { lineLogin: { type: Function, required: true, }, }, methods: { childLineLogin() { this.lineLogin(); }, }, }; </script> ②IDトークンをサーバーに渡す 取得したIDトークンをLaravelに渡してあげます。 frontend/components/Template/Index.vue <template> <div class="container"> <div class="product"> <Heading :main="'Product'" :sub="'商品'" /> <div class="card"> <div class="card-container" v-for="product in products.contents" :key="product.id"> <img class="card-product-img" :src="product.image.url" /> <p class="card-product-name">{{ product.name }}</p> <p class="card-product-price">¥{{ product.price }}</p> <div class="button"> <button v-if="idToken" class="button-btn" @click="checkout(product)">購入する</button> <button v-else class="button-btn disabled" disabled>購入する</button> </div> </div> </div> </div> </div> </template> <script> // components import Heading from '@/components/Atoms/Heading'; export default { components: { Heading, }, props: { products: { type: Object, required: true, }, }, data() { return { idToken: '', }; }, methods: { async lineLogin() { try { // IDトークン this.idToken = liff.getIDToken(); + if (this.idToken) { + await this.$axios.$post('/hubspot/store', { + idToken: this.idToken, + }); + } } catch (err) { alert(err); alert(err.response.data.error); alert(err.response.data.error_description); } }, }, }; </script> ③IDトークンを検証して、ユーザーのプロフィールを取得する プロフィールを取得するには、ルーティングとコントローラーの2つが必要になります。 ルーティング api/routes/api.php <?php use Illuminate\Http\Request; /* |-------------------------------------------------------------------------- | API Routes |-------------------------------------------------------------------------- */ Route::get("/", function () { return "Hello World!"; }); // HubSpotにユーザーを登録する + Route::post("/hubspot/store", "HubSpotController@store"); コントローラー コントローラーを作成しましょう。 ターミナル $ php artisan make:controller HubSpotController --resource また、Guzzleを使用するのでGuzzleをインストールしましょう。 ターミナル $ composer require guzzlehttp/guzzle 次に、.envを使うのでconfigからアクセスさせましょう。 env()を使ってはいけない理由などは以下の記事を見てみてください。 api/config/env.php <?php return [ "line_client_id" => env("LINE_CLIENT_ID"), ]; また、Herokuの.envの値も確認しておきましょう。 .envの値をコントローラー内で取得しましょう。 api/app/Http/Controllers/HubSpotController.php <?php namespace App\Http\Controllers; use Illuminate\Http\Request; use GuzzleHttp\Client; // Log use Illuminate\Support\Facades\Log; class HubSpotController extends Controller { public function store(Request $request) { try { // request $idToken = $request->input("idToken"); Log::info($idToken); // env + $clientID = config("env.line_client_id"); Log::info($clientID); } catch (\GuzzleHttp\Exception\BadResponseException $e) { Log::info($e); return $e->getResponse()->getBody()->getContents(); } } } では、IDトークンを検証してユーザーのプロフィールとメールアドレスを取得しましょう! api/app/Http/Controllers/HubSpotController.php <?php namespace App\Http\Controllers; use Illuminate\Http\Request; use GuzzleHttp\Client; // Log use Illuminate\Support\Facades\Log; class HubSpotController extends Controller { public function store(Request $request) { try { // request $idToken = $request->input("idToken"); Log::info($idToken); // env $clientID = config("env.line_client_id"); Log::info($clientID); // guzzle + $client = new Client(); + $response = $client->request("POST", "https://api.line.me/oauth2/v2.1/verify", [ + "form_params" => [ + "id_token" => $idToken, + "client_id" => $clientID + ] + ]); } catch (\GuzzleHttp\Exception\BadResponseException $e) { Log::info($e); return $e->getResponse()->getBody()->getContents(); } } } では次にこの取得した名前やメールアドレスをHubSpotに登録させましょう。 まずはHubSpotのAPIを取得しましょう。 このAPIをHerokuの.envに入力しましょう。 次に、.envを使うのでconfigからアクセスさせましょう。 api/config/env.php <?php return [ "line_client_id" => env("LINE_CLIENT_ID"), + "hubspot_api_key" => env("HUBSPOT_API_KEY"), ]; 最後にコントローラーを作成します。 LINEログイン情報を使用してHubSpotのコンタクトを作成するので、POST /crm/v3/objects/contactsを使います。 今回は、cURLコマンドで作成します。 api/app/Http/Controllers/HubSpotController.php <?php namespace App\Http\Controllers; use Illuminate\Http\Request; use GuzzleHttp\Client; // Log use Illuminate\Support\Facades\Log; class HubSpotController extends Controller { public function store(Request $request) { try { // request $idToken = $request->input("idToken"); // env $clientID = config("env.line_client_id"); + $HubSpotApiKey = config("env.hubspot_api_key"); // guzzle $client = new Client(); $response = $client->request("POST", "https://api.line.me/oauth2/v2.1/verify", [ "form_params" => [ "id_token" => $idToken, "client_id" => $clientID ] ]); // HubSpot Value + $profile = json_decode($response->getBody()->getContents(), true); + $profileName = $profile["name"]; + $profileEmail = $profile["email"]; // Log::info($profile["name"]); // Log::info($profile["email"]); // Register HubSpot + $data = array( + "properties" => [ + "email" => $profileEmail, + "firstname" => $profileName + ] + ); + $formParams = json_encode($data, JSON_UNESCAPED_UNICODE); // Log::info($formParams); // cURL + $curl = curl_init(); + curl_setopt_array($curl, array( + CURLOPT_URL => "https://api.hubapi.com/crm/v3/objects/contacts?hapikey=" . $HubSpotApiKey, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "POST", + CURLOPT_POSTFIELDS => $formParams, + CURLOPT_HTTPHEADER => array( + "accept: application/json", + "content-type: application/json" + ), + )); + $response = curl_exec($curl); + $err = curl_error($curl); + curl_close($curl); + if ($err) { + Log::info($err); + } else { + Log::info($response); + } } catch (\GuzzleHttp\Exception\BadResponseException $e) { Log::info($e); return $e->getResponse()->getBody()->getContents(); } } } これでHubSpotにユーザー登録が可能になりました。 それでは最後にStripeを使って決済を行いましょう。 Stripeで決済を作成する Stripeで決済を作成するには以下のように進めていきます。 フロントエンド バックエンド サービス ①Stripeに商品登録を行う ②StripeのAPIを取得する ③IDトークンとStripeの商品IDをバックエンドに渡す ④IDトークンを検証して、ユーザーのプロフィールを取得する ⑤Stripeで決済処理を行う ⑥決済画面をフロントエンドに渡す ⑦決済画面へページ移動させる ①Stripeに商品登録を行う 「商品を追加」から商品を登録します。 商品詳細ページにAPI IDがあるのでこちらをMicro CMSに入力しましょう。 ②StripeのAPIを取得する APIが取得できたらこれをHerokuに登録します。 これでOKです! ③IDトークンとStripeの商品IDをバックエンドに渡す frontend/components/Template/Index.vue <template> <div class="container"> <div class="product"> <Heading :main="'Product'" :sub="'商品'" /> <div class="card"> <div class="card-container" v-for="product in products.contents" :key="product.id"> <img class="card-product-img" :src="product.image.url" /> <p class="card-product-name">{{ product.name }}</p> <p class="card-product-price">¥{{ product.price }}</p> <div class="button"> <button v-if="idToken" class="button-btn" @click="checkout(product)">購入する</button> <button v-else class="button-btn disabled" disabled>購入する</button> </div> </div> </div> </div> </div> </template> <script> // components import Heading from '@/components/Atoms/Heading'; export default { components: { Heading, }, props: { products: { type: Object, required: true, }, }, data() { return { idToken: '', }; }, methods: { async lineLogin() { try { // IDトークン this.idToken = liff.getIDToken(); if (this.idToken) { await this.$axios.$post('/hubspot/store', { idToken: this.idToken, }); } } catch (err) { alert(err); alert(err.response.data.error); alert(err.response.data.error_description); } }, + async checkout(product) { + const url = await this.$axios.$post('/stripe/store', { + productStripePriceApi: product.stripe_price_api, + idToken: this.idToken, + }); + }, }, }; </script> ④IDトークンを検証して、ユーザーのプロフィールを取得する これは先ほどやったことと全く同じですね。 まずはルーティングから作成しましょう。 api/routes/api.php <?php use Illuminate\Http\Request; /* |-------------------------------------------------------------------------- | API Routes |-------------------------------------------------------------------------- */ Route::get("/", function () { return "Hello World!"; }); // HubSpotにユーザーを登録する Route::post("/hubspot/store", "HubSpotController@store"); // Stripeを使って決済を行う + Route::post("/stripe/store", "PayloadController@store"); では次にコントローラーの作成をしましょう。 ターミナル $ php artisan make:controller PayloadController --resource コントローラーの記述をしていきます。 api/app/Http/Controllers/PayloadController.php <?php namespace App\Http\Controllers; use Illuminate\Http\Request; // guzzle use GuzzleHttp\Client; class PayloadController extends Controller { public function store(Request $request) { // line + $idToken = $request->input("idToken"); // env + $clientID = config("env.line_client_id"); // guzzle + $client = new Client(); + $response = $client->request("POST", "https://api.line.me/oauth2/v2.1/verify", [ + "form_params" => [ + "id_token" => $idToken, + "client_id" => $clientID + ] + ]); + $profile = json_decode($response->getBody()->getContents(), true); + $profileEmail = $profile["email"]; } } ⑤Stripeで決済処理を行う Stripeで決済処理を行うには、checkoutを使用します。 公式サイトに具体的なやり方が書いてあるのでこちらに倣ってやっていきます。 まずはライブラリをインストールします ターミナル $ composer require stripe/stripe-php checkoutセッションを作成する api/app/Http/Controllers/PayloadController.php <?php namespace App\Http\Controllers; use Illuminate\Http\Request; // stripe use Stripe\Stripe; use Stripe\Checkout\Session; // guzzle use GuzzleHttp\Client; class PayloadController extends Controller { public function store(Request $request) { // $request // line $idToken = $request->input("idToken"); // product + $productStripePriceApi = $request->input("productStripePriceApi"); // env $clientID = config("env.line_client_id"); + $stripeApiKey = config("env.stripe_api_key"); // guzzle $client = new Client(); $response = $client->request("POST", "https://api.line.me/oauth2/v2.1/verify", [ "form_params" => [ "id_token" => $idToken, "client_id" => $clientID ] ]); $profile = json_decode($response->getBody()->getContents(), true); $profileEmail = $profile["email"]; // API + Stripe::setApiKey($stripeApiKey); // header + header('Content-Type: application/json'); // domain + $domain = "https://liff-nuxt-laravel-ec.netlify.app"; // checkout + $checkout_session = Session::create([ + 'customer_email' => $profileEmail, + 'payment_method_types' => ['card'], + 'line_items' => [[ + 'price' => $productStripePriceApi, + 'quantity' => 1, + ]], + 'mode' => 'payment', + 'success_url' => $domain . '/success', + 'cancel_url' => $domain . '/cancel', + ]); } } ⑥決済画面をフロントエンドに渡す 公式サイトではリダイレクト処理で決済画面へ移動させていますが、 ページ移動などはフロントエンドで処理したいのでURLをフロントエンド側に渡しましょう。 api/app/Http/Controllers/PayloadController.php <?php namespace App\Http\Controllers; use Illuminate\Http\Request; // stripe use Stripe\Stripe; use Stripe\Checkout\Session; // guzzle use GuzzleHttp\Client; class PayloadController extends Controller { public function store(Request $request) { // $request // line $idToken = $request->input("idToken"); // product $productStripePriceApi = $request->input("productStripePriceApi"); // env $clientID = config("env.line_client_id"); $stripeApiKey = config("env.stripe_api_key"); // guzzle $client = new Client(); $response = $client->request("POST", "https://api.line.me/oauth2/v2.1/verify", [ "form_params" => [ "id_token" => $idToken, "client_id" => $clientID ] ]); $profile = json_decode($response->getBody()->getContents(), true); $profileEmail = $profile["email"]; // API Stripe::setApiKey($stripeApiKey); // header header('Content-Type: application/json'); // domain $domain = "https://liff-nuxt-laravel-ec.netlify.app"; // checkout $checkout_session = Session::create([ 'customer_email' => $profileEmail, 'payment_method_types' => ['card'], 'line_items' => [[ 'price' => $productStripePriceApi, 'quantity' => 1, ]], 'mode' => 'payment', 'success_url' => $domain . '/success', 'cancel_url' => $domain . '/cancel', ]); // return redirect($checkout_session->url, 303, [], true); + return $checkout_session->url; } } ⑦決済画面へページ移動させる これで決済画面ページへ移動されます。 frontend/components/Template/Index.vue <template> <div class="container"> <div class="product"> <Heading :main="'Product'" :sub="'商品'" /> <div class="card"> <div class="card-container" v-for="product in products.contents" :key="product.id"> <img class="card-product-img" :src="product.image.url" /> <p class="card-product-name">{{ product.name }}</p> <p class="card-product-price">¥{{ product.price }}</p> <div class="button"> <button v-if="idToken" class="button-btn" @click="checkout(product)">購入する</button> <button v-else class="button-btn disabled" disabled>購入する</button> </div> </div> </div> </div> </div> </template> <script> // components import Heading from '@/components/Atoms/Heading'; export default { components: { Heading, }, props: { products: { type: Object, required: true, }, }, data() { return { idToken: '', }; }, methods: { async lineLogin() { try { // IDトークン this.idToken = liff.getIDToken(); if (this.idToken) { await this.$axios.$post('/hubspot/store', { idToken: this.idToken, }); } } catch (err) { alert(err); alert(err.response.data.error); alert(err.response.data.error_description); } }, async checkout(product) { const url = await this.$axios.$post('/stripe/store', { productStripePriceApi: product.stripe_price_api, idToken: this.idToken, }); + if (url) { + window.location.href = url; + } }, }, }; </script> これで完成です! お疲れ様でした^ ^ 終わりに せっかくCRMのHubSpotを入れたので、商品購入のログなども残せるようにしたいなーと思います。 この辺も今後チャレンジしていきたいと思います。 また、HubSpotにはLINE接続するアプリもあるので同様の機能が自分自身で準備できるかも試してみたいです。 ここまで読んでいただきありがとうございました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel + Vue.js講座】いいね機能を作ろう!!をlaravel8+vue-router+xamppで作ってみた♡

【Laravel + Vue.js講座】いいね機能を作ろう!!を手本にして作りました。 講座ではlaravel5で作成していましたので私はlaravel8とxamppとvue-routerで作ってみた。 作成する機能は講座とほとんど同じです。v-routerでの投稿機能(CRUD)は下記のリンクを参考にしています。 認証機能 投稿機能(CRUD) イイね機能(1つの投稿にユーザーは1回(toggle)いいねが可能) 目的は laravel8+vue+vue-route+xamppの備忘記録 ◎プロジェクトの作成 laravel new riku_larabel_vue_good_btn composer require laravel/ui php artisan ui vue --auth npm install && npm run dev 再度 npm run dev 確認 localhostにアクセスして、loginとregister画面の確認 vue-routeのインストール npm install vue-router npm run dev ◎asset関数からmix関数へ変更する そうすることで効率的に作成できるみたい。 ①.resources\views\layouts\app.blade.phpファイルを編集 resources\views\layouts\app.blade.php <!-- Scripts --> {{-- コメントアウト<script src="{{ asset('js/app.js') }}" defer></script> --}} <script src="{{ mix('js/app.js') }}" defer></script> <!-- Styles --> {{-- コメントアウト<link href="{{ asset('css/app.css') }}" rel="stylesheet"> --}} <link href="{{ mix('css/app.css') }}" rel="stylesheet"> 再度確認するとスタイル等が崩れるている localhostにアクセスして、loginとregister画面の確認してみると崩れているのを確認。 ②.mix_urlを追記する。config/app.phpに追加(asset_urlの直後がいいのでは) config\app.php 'mix_url' => env('MIX_ASSET_URL', 'http://localhost/riku_larabel_vue_good_btn/public/'), 確認するとスタイル等があたっている。 localhost ◎webpack.mix.jsファイルの編集 .sourceMaps().version()を追加する。 webpack.mix.js mix.js('resources/js/app.js', 'public/js') .vue() .sass('resources/sass/app.scss', 'public/css') .sourceMaps() .version(); npm run dev .sourceMaps()で下記の警報が消える DevTools failed to load source map: Could not load content for http://localhost/riku_larabel_vue_good_btn/public/js/popper.js.map: HTTP error: status code 404, net::ERR_HTTP_RESPONSE_CODE_FAILURE .version() でmixの変更が反映されないのを防ぐ!!これがためにmix関数を使っている。 ◎データーベースの作成   http://localhost/phpmyadmin/index.php 1.データーベース名はプロジェクト名と同じ   riku_larabel_vue_good_btn 2.文字コードは   utf8mb4_general_ci envの設定 変更なし。 DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=riku_larabel_vue_good_btn DB_USERNAME=root DB_PASSWORD= ◎モデルとコントローラーとテーブルの作成 ①PostとLikeモデルを作成する。 php artisan make:model Post -a php artisan make:model Like -a ②postsとlikesマイグレーションファイルの編集 postsテーブルのマイグレーションの編集 database\migrations\2021_xx_xx_xxxxx_create_posts_table.php public function up() { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained(); $table->string('content')->nullable(); $table->timestamps(); }); } likesテーブルのマイグレーションの編集 database\migrations\2021_xx_xx_xxxxx_create_likes_table.php public function up() { Schema::create('likes', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained()->onUpdate('cascade')->onDelete('cascade'); $table->foreignId('post_id')->constrained()->onUpdate('cascade')->onDelete('cascade'); $table->unique(['user_id', 'post_id']); $table->timestamps(); }); } 独り言:メモ likesテーブルはpostsとusersテーブルの多対多の関係にあるので、中間テーブルとしてpost_userテーブルでも良いと思ったがとんだ勘違いだった。 postにuserタグを貼り付けるとも言えるが、postにはすでにuserの外部キーがあるし・・余計複雑怪奇になるわ。 usersとpostsとの関係はあくまでも1対多の関係であるため、likesテーブルがusersとpostsの多対多の関係にならないし、なんの関係もない。と思う。 ③モデルの編集 app\Models\Post.php protected $fillable = [ 'user_id','content', ]; public function likes() { return $this->hasMany(Like::class); } app\Models\Like.php protected $fillable = [ 'user_id','post_id', ]; ④migrateの実行 php artisan migrate ◎ルート構成 ・認証機能等はlaravelのデフォルト ・ポストのCRUDはvue-router(index,create,edit) ・postsテーブルとの通信はaxiosを使用 ・likesテーブルとの通信もaxiosで使用。 routes\web.php use App\Http\Controllers\PostController;//忘れずに Auth::routes(); Route::get('/', [App\Http\Controllers\HomeController::class, 'index'])->name('home'); //laravel用のルート Route::get('/posts', function () { return view('posts.index'); })->name('posts.index')->middleware('auth'); //vue用のルート Route::get('posts/{any}', function () { return view('posts.index'); })->where('any', '.*')->middleware('auth'); //axiosでの通信用のルート 外部通信は不要なのでweb.phpに記述 Route::prefix('api')->as('api')->group(function () { Route::resource('posts', PostController::class); Route::post('posts/{post}/like', [PostController::class, 'like']); Route::post('posts/{post}/unlike', [PostController::class, 'unlike']); }); ログイン遷移先レジスター遷移先を変更する。 app\Providers\RouteServiceProvider.phpの編集 RouteServiceProvider.php public const HOME = '/'; ナビにポスト画面へのリンクを作成する。 resources\views\layouts\app.blade.phpの編集 resources\views\layouts\app.blade.php <ul class="navbar-nav ml-auto"> <!-- Authentication Links --> @guest //省略 @if (Route::has('register')) -------ここから------ <li class="nav-item"> <a class="nav-link" href="{{ route('register') }}">{{ __('Register') }}</a> </li> ------ここまでコピー--@endifのすぐ上------ @endif @else -------@elseのすぐ下に貼り付けて編集------    <li class="nav-item">     <a class="nav-link" href="{{ route('posts.index') }}">Post</a>    </li> //省略 @endguest ◎簡単にvueの動作確認をしておく。 ①PostのIndexファイルの作成 resources\views\posts\index.blade.phpを作成する。 home.blade.phpをコピーして編集。 resources\views\posts\index.blade.php @extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">Post Index</div> <div class="card-body"> {{-- デフォルトのvueコンポネントで動作確認 --}} <example-component></example-component> </div> </div> </div> </div> </div> @endsection vueコンポーネントが描写されているか確認 localhost 描写されているのが確認できたらとりあえず npm run watch して、 resources\js\components\ExampleComponent.vueを適当に編集して 画面が変更されるかを再度確認する。 vueコンポーネントの編集が反映されていることを確認 ◎xamppでのvue-routerの動作確認もしておく。 ①index.blade.phpの編集 resources\views\posts\index.blade.php @extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">Post Index</div> <div class="card-body"> {{-- デフォルトのvueコンポネントで動作確認 --}} {{-- コメントアウト<example-component></example-component> --}} <router-view /> {{-- 追加--}} </div> </div> </div> </div> </div> @endsection ②resources\js\app.jsの編集 resources\js\app.js //ルーティングは別ファイルに記述する方法でやる。 import router from "./router"; //<---追加 require('./bootstrap'); window.Vue = require('vue').default; Vue.component('example-component', require('./components/ExampleComponent.vue').default); const app = new Vue({ el: '#app', router: router, //<---追加 }); ③resources\js\router.jsファイルを作成する resources\js\router.js import Vue from "vue"; import VueRouter from "vue-router"; Vue.use(VueRouter); import index from "./components/index.vue"; import create from "./components/create.vue"; const router = new VueRouter({ mode: "history", //baseの設定を忘れずに、xampp対応。 base:'/riku_larabel_vue_good_btn/public/posts/', routes: [ { path: "", name: "index", component: index }, { path: "/create", name: "create", component: create }, ] }); export default router; ④Vueコンポーネントファイルの作成 1.resources\js\components\ index.vueの作成 resources\js\components\index.vue <template> <div> <h1>POST INDEX</h1> </div> </template> 2.resources\js\components\ create.vueの作成 resources\js\components\create.vue <template> <div> <h1>POST CREATE</h1> </div> </template> ⑤webpack.mix.jsの編集 webpack.mix.js mix.js('resources/js/app.js', 'public/js') .vue() .js("resources/js/router.js", "public/js") //追加 .sass('resources/sass/app.scss', 'public/css') .sourceMaps() .version(); npm run dev npm run watch vue-routerの動作確認 描写とルーティングの確認 post.index post.create ◎PostのCRUD機能の実装(取得と登録編) ①index.vueを編集 ・新規投稿画面へのリンク ・postsテーブルを取得してテーブルで表示 ・詳細画面へのリンク ・編集画面へのリンクボタン ・削除ボタン ・投稿ごとのlikes数の取得(これはCRUDの後で実装) resources\js\components\index.vue <template> <div> <router-link :to="{ name: 'create' }">新規追加</router-link> <table class="table table-bordered text-center table-sm" style="table-layout: fixed"> <thead> <tr> <th class="col-1" scope="col">ID</th> <th class="col-7" scope="col">本文</th> <th class="col-1" scope="col">?</th> <th class="col-2" scope="col">action</th> </tr> </thead> <tbody> <tr v-for="post in posts" :key="post.id"> <th scope="row"> <router-link :to="{ name: 'show', params: { id: post.id } }">{{ post.id }}</router-link> </th> <td class="text-truncate">{{ post.content }}</td> <td class="text-truncate">{{ post.likes_count }}</td> <td class="d-flex justify-content-around"> <router-link class="btn btn-secondary btn-sm" :to="{ name: 'edit', params: { id: post.id } }" >編集</router-link > <button class="btn btn-danger btn-sm" @click="postDstroy(post.id)"> 削除 </button> </td> </tr> </tbody> </table> </div> </template> <script> export default { data() { return { message: "", posts: {}, }; }, created: function () { this.getPost(); }, methods: { getPost() { axios .get("/riku_larabel_vue_good_btn/public/api/posts/") .then((response) => { console.log(response.data.posts); this.posts = response.data.posts; }) .catch((error) => { this.message = error; }); }, postDstroy(id) { axios .delete("/riku_larabel_vue_good_btn/public/api/posts/" + id) .then((response) => { this.getPost(); this.message = ""; }) .catch((error) => { this.message = error; }); }, }, }; </script> ②create.vueを編集 エラー確認のためformにnovalidateを貼っている。 resources\js\components\create.vue <template> <div> <router-link :to="{ name: 'index' }">トップ</router-link> <form @submit.prevent="postStore" novalidate> <div class="form-group row"> <label for="content" class="col-12 col-form-label"> Exsample Post </label> <div class="col-12"> <textarea id="content" type="text" class="form-control" v-bind:class="{ 'is-invalid': isInvalid }" name="content" v-model="content" required rows="5" ></textarea> <p class="text-danger" v-if="message">{{ message }}</p> </div> </div> <div class="form-group row mb-0"> <div class="col-md-8 offset-md-4 text-right"> <button type="submit" class="btn btn-primary">投稿</button> </div> </div> </form> </div> </template> <script> export default { props: ["userId"], data() { return { message: "", posts: {}, content: "", isInvalid:false, }; }, methods: { postStore() { axios .post("/riku_larabel_vue_good_btn/public/api/posts", { content: this.content, user_id: this.userId, }) .then((response) => { console.log(response.data); this.$router.push({ name: "index" }); }) .catch(({ response }) => { console.log(response.data.errors.content); this.isInvalid = true; this.message = response.data.errors.content[0]; }); }, }, }; </script> ③ポストコントローラーの編集 app\Http\Controllers\PostController.phpの編集 indexメソッドとstoreメソッドの作成 app\Http\Controllers\PostController.php public function index(Request $request) { //if ($request->ajax())の判定には // headers: { "X-Requested-With": "XMLHttpRequest" },が必 if ($request->ajax()) { $posts = Post::all(); return response()->json(['posts' => $posts]); } else { return view('posts.index'); } } public function store(PostRequest $request) { Post::create([ 'content'=>$request->content, 'user_id'=>$request->user_id, ]); } ④バリデーションファイルの作成 php artisan make:request PostRequest app\Http\Requests\PostRequest.phpの編集 app\Http\Requests\PostRequest.php <?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class PostRequest extends FormRequest { public function authorize() { return true; } public function rules() { return [ 'content' => 'required|max:255', 'user_id' => 'required|integer', ]; } } PostControllerに追記 app\Http\Controllers\PostController.php use App\Http\Requests\PostRequest; ⑤index.blade.phpの編集 vue-routerへAuth::id()データーを渡してやる resources\views\posts\index.blade.php @extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">Post Index</div> <div class="card-body"> {{-- デフォルトのvueコンポネントで動作確認 --}} {{-- コメントアウト<example-component></example-component> --}} <router-view :user-id="{{ json_encode(Auth::id()) }}"/>{{-- 編集--}} </div> </div> </div> </div> </div> @endsection ⑥axios通信のための設定を追加するためresources\js\app.jsの編集 resources\js\app.jsに追記 resources\js\app.js //ルーティングは別ファイルに記述する方法でやる。 import router from "./router"; //<↓axios通信のための設定> window.axios = require('axios'); window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest' const token = document.head.querySelector('meta[name="csrf-token"]') if (token) { window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content } //<↑/axios通信のための設定> require('./bootstrap'); window.Vue = require('vue').default; Vue.component('example-component', require('./components/ExampleComponent.vue').default); const app = new Vue({ el: '#app', router: router, }); index画面、create画面での表示と登録機能の確認 create画面で投稿してindex画面での表示を確認 また、バリデーションが機能しているか確認 posts.create ◎PostのCRUD機能の実装(削除機能と詳細表示と編集機能 編) ①詳細表示の作成 resources\js\components\show.vueの作成 resources\js\components\show.vue <template> <div> <table class="table-bordered w-100 text-center"> <tr> <th class="col-4">ID</th> <td class="col-8">{{post.id}}</td> </tr> <tr> <th>?</th> <td>{{ post.likes_count }}</td> </tr> <tr> <th>本文</th> <td>{{ post.content }}</td> </tr> </table> </div> </template> <script> export default { data() { return { message: "", post: {}, }; }, created() { axios .get("/riku_larabel_vue_good_btn/public/api/posts/" + this.$route.params.id) .then(response => (this.post = response.data)) .catch(erorr => console.log(error)); }, methods: { }, }; </script> ②編集画面の作成 resources\js\components\edit.vueの作成 resources\js\components\edit.vue <template> <div> <table class="table-bordered w-100 text-center"> <tr> <th class="col-4">ID</th> <td class="col-8">{{post.id}}</td> </tr> <tr> <th>?</th> <td>{{ post.likes_count }}</td> </tr> <tr> <th>本文</th> <td><textarea class="w-100" rows="5" v-model="post.content">{{ post.content }}</textarea></td> </tr> </table> <div class="text-right mt-3"> <button class="btn btn-success" v-on:click="PostUpdate">更新</button> <button class="btn btn-danger" v-on:click="updateCancel">キャンセル</button> </div> </div> </template> <script> export default { props:['userId'], data() { return { message: "", post: {}, }; }, created() { axios .get("/riku_larabel_vue_good_btn/public/api/posts/" + this.$route.params.id) .then(response => (this.post = response.data)) .catch(erorr => console.log(error)); }, methods: { updateCancel() { this.$router.push({ name: "index" }); }, PostUpdate() { axios .put("/riku_larabel_vue_good_btn/public/api/posts/" + this.post.id, { content: this.post.content, user_id: this.userId, }) .then(response => { this.$router.push({ name: "index" }); }) .catch(erorr => { this.message = erorr; }); } }, }; </script> ③Vue-routerの追加 resources\js\router.jsの編集 resources\js\router.js import Vue from "vue"; import VueRouter from "vue-router"; Vue.use(VueRouter); import index from "./components/index.vue"; import create from "./components/create.vue"; //<↓追加> import show from "./components/show.vue"; import edit from "./components/edit.vue"; //<↑/追加> const router = new VueRouter({ mode: "history", //baseの設定を忘れずに、xampp対応です。 base:'/riku_larabel_vue_good_btn/public/posts/', routes: [ { path: "", name: "index", component: index }, { path: "/create", name: "create", props: true, component: create }, //<↓追加> { path: "/:id", name: "show", component: show }, { path: "/:id/edit", name: "edit", component: edit } //<↑/追加> ] }); export default router; ④destroyメソッドとshowメソッドとupdateメソッドの追加 app\Http\Controllers\PostController.php public function show($id) { return Post::find($id); } public function update(Request $request,$id) { $update = [ 'content'=>$request->content, 'user_id'=>$request->user_id, ]; Post::where('id', $id)->update($update); } public function destroy($id) { Post::where('id', $id)->delete(); } PostのCRUDの最終確認 ①ポストして posts.create ②インデックス画面で一覧表示の確認 posts.index ③詳細画面で詳細確認 posts.show ④編集画面で編集投稿 posts.edit ⑤インデックス画面で削除を実行 posts.index ◎いいね機能を実装する ①イイねボタンのコンポーネントを作成する resources\js\components\like.vueの作成 resources\js\components\like.vue <template> <div class=""> <button type="button" class="btn" :class="liked?'btn-danger':'btn-outline-danger'" :disabled="processing" @click="like" @mouseleave="mouseLeaveAction" > いいね♡{{ likeCount }} </button> </div> </template> <script> export default { props: ["postId", "userId", "defaultLiked", "defaultCount"], data() { return { changed: false, processing: false, liked: false, likeCount: 0, }; }, watch: { defaultLiked: { handler() { if (!this.defaultLiked) return; this.liked = this.defaultLiked; }, }, defaultCount: { handler() { if (!this.defaultCount) return; this.likeCount = this.defaultCount; }, }, }, methods: { like() { this.changed = !this.changed; if ((this.liked = !this.liked)) { this.likeCount += 1; } else { this.likeCount -= 1; } this.$emit('love', this.likeCount) }, mouseLeaveAction() { if (!this.changed) return; if (this.processing) return; this.processing = true; this.changed = false; let url = this.liked ? `/riku_larabel_vue_good_btn/public/api/posts/${this.postId}/like` : `/riku_larabel_vue_good_btn/public/api/posts/${this.postId}/unlike`; axios .post(url, { user_id: this.userId, }) .then((response) => { this.likeCount = response.data.likeCount; }) .catch((error) => { console.log(error); }) .finally(() => { this.processing = false; }); }, }, }; </script> ②app.jsにlikeコンポーネントを登録する resources\js\app.jsに追記 resources\js\app.js import router from "./router"; require('./bootstrap'); window.Vue = require('vue').default; //<↓axios通信のための設定> window.axios = require('axios'); window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest' const token = document.head.querySelector('meta[name="csrf-token"]') if (token) { window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content } //<↑/axios通信のための設定> Vue.component('example-component', require('./components/ExampleComponent.vue').default); Vue.component('like', require('./components/like.vue').default);//追加 const app = new Vue({ el: '#app', router: router, }); ②showコンポーネントで表示させるため、show.vueに追記。 resources\js\components\show.vueの編集 resources\js\components\show.vue <template> <div> <table class="table-bordered w-100 text-center"> <tr> <th class="col-4">ID</th> <td class="col-8">{{post.id}}</td> </tr> <tr> <th>?</th> <td>{{ post.likes_count }}</td> </tr> <tr> <th>本文</th> <td>{{ post.content }}</td> </tr> </table> <!-- ↓追加 --> <div class="text-right mt-3"> <like @love="updateLikeCount" :post-id="post.id" :user-id="userId" :defaultLiked="defaultLiked" :defaultCount="post.likes_count" > </like> </div> <!-- /↑追加 --> </div> </template> <script> export default { props: ["userId"], //追加 data() { return { message: "", defaultLiked: "", //追加 post: {}, }; }, created() { const queries = { user_id: this.userId }; //追加 axios //変更 .get("/riku_larabel_vue_good_btn/public/api/posts/" + this.$route.params.id, { params: queries, }) //変更 .then((response) => { console.log(response.data.defaultLiked); this.post = response.data.post; this.defaultLiked = response.data.defaultLiked; }) .catch(erorr => console.log(error)); }, methods: { //追加 updateLikeCount(likeCount) { this.post.likes_count = likeCount; }, }, }; </script> ③ポストコントローラーを編集する indexメソッドとshowメソッドの再修正 likeメソッドとunlikeメソッドの追加 PostControllerファイルの全記述 app\Http\Controllers\PostController.php <?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Http\Request; use App\Http\Requests\PostRequest; use App\Models\Like; class PostController extends Controller { public function __construct() { } public function index(Request $request) { //if ($request->ajax())の判定には // headers: { "X-Requested-With": "XMLHttpRequest" },が必 if ($request->ajax()) { $posts = Post::withCount('likes') ->orderBy('likes_count','desc')->get(); return response()->json(['posts' => $posts]); } else { return view('posts.index'); } } // public function create() // { // // // } public function store(PostRequest $request) { Post::create([ 'content'=>$request->content, 'user_id'=>$request->user_id, ]); } public function show(Request $request ,$id) { $post = Post::find($id); $post->loadCount('likes'); $defaultLiked =Like::where('post_id',$post->id)->where('user_id',$request->user_id)->exists(); return response()->json(['post'=>$post,'defaultLiked'=>$defaultLiked]); } public function update(Request $request,$id) { $update = [ 'content'=>$request->content, 'user_id'=>$request->user_id, ]; Post::where('id', $id)->update($update); } public function destroy($id) { Post::where('id', $id)->delete(); } public function like(Post $post, Request $request) { $like = Like::create(['post_id' => $post->id, 'user_id' => $request->user_id]); $likeCount = Like::where('post_id', $post->id)->count(); return response()->json(['likeCount' => $likeCount]); } public function unlike(Post $post, Request $request) { $like = Like::where('user_id', $request->user_id)->where('post_id', $post->id)->first(); $like->delete(); $likeCount = Like::where('post_id', $post->id)->count(); return response()->json(['likeCount' => $likeCount]); } } ④edit.vueの再編集(showメッドの返り値を変更したため) resources\js\components\edit.vue resources\js\components\edit.vue <template> <div> <table class="table-bordered w-100 text-center"> <tr> <th class="col-4">ID</th> <td class="col-8">{{post.id}}</td> </tr> <tr> <th>?</th> <td>{{ post.likes_count }}</td> </tr> <tr> <th>本文</th> <td><textarea class="w-100" rows="5" v-model="post.content">{{ post.content }}</textarea></td> </tr> </table> <div class="text-right mt-3"> <button class="btn btn-success" v-on:click="PostUpdate">更新</button> <button class="btn btn-danger" v-on:click="updateCancel">キャンセル</button> </div> </div> </template> <script> export default { props:['userId'], data() { return { message: "", post: {}, }; }, created() { axios .get("/riku_larabel_vue_good_btn/public/api/posts/" + this.$route.params.id) .then(response => (this.post = response.data.post)) //修正 .catch(erorr => console.log(error)); }, methods: { updateCancel() { this.$router.push({ name: "index" }); }, PostUpdate() { axios .put("/riku_larabel_vue_good_btn/public/api/posts/" + this.post.id, { content: this.post.content, user_id: this.userId, }) .then(response => { this.$router.push({ name: "index" }); }) .catch(erorr => { this.message = erorr; }); } }, }; </script> ◎機能の最終確認 posts.index 以上 終わり
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt+Laravelを単一のホストで動かす

あらすじ お金をかけずに(とか諸々の理由で), Laravel+Nuxtで自前でCMSを作ろうということになって, レンタルサーバでNuxtもLaravelも動かしたいという話になりました. この記事の続きとして書きます. https://qiita.com/YSY/items/3a0abdc6ec5296373d17 上記の記事では, Nuxt をデプロイするところまで書きました. Nuxt Conoha VPS にデプロイした後に, Laravel と連携するようにしていきます. まずphpを入れる https://www.rem-system.com/centos8-php-install/ が丁寧に書いてました. Conoha vps を使っており, CentOS8 上で作ることになるので, dnf コマンドを使っていきます. 必要があれば yum に置き換えてもらえればと思います. dnf info php php-mbstring php-xml php-xmlrpc php-gd php-pdo php-mysqlnd php-json で確認した後、 dnf install -y php php-mbstring php-xml php-xmlrpc php-gd php-pdo php-mysqlnd php-json で必要なモジュールをインストールします. php -vでphp7.2が入っていることを確認します. Composer を入れる まず公式の言う通りに, インストーラを入れましょう. php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" そんで, php composer-setup.php すれば手元に composer.phar が生成されたと思います. php -r "unlink('composer-setup.php');" をすればインストーラを削除できます. 次に, mv composer.phar /usr/local/bin/composer して, どこからでも composer コマンドを使えるようにしましょう. composer -Vでバージョンが表示されるようになりました. Laravel が動くようにする それはお任せします. envを用意したり, composer installとかしたりしてください. 僕の場合はgit cloneしてcomposer updateしてcomposer installしてcp .env.example .envしてphp artisan key:generateしました. php public/index.phpで動くことを確認して終わりです. MySQLの設定 https://qiita.com/yasushi-jp/items/1579c301075d693a2a36 を参考にしました. dnf localinstall https://dev.mysql.com/get/mysql80-community-release-el8-1.noarch.rpm dnf module disable mysql dnf install mysql-community-server service mysqld start そんでmysql_secure_installationを実行してパスワードを変更したりする. .envにパスワードを入れてくおく. Apache の設定 例えば以下のように設定して、とりあえず Laravel をみれるようにしましょう. <VirtualHost *:80> DocumentRoot 自分で指定したやつ <Proxy *> Require all granted </Proxy> ProxyRequests Off ProxyPreserveHost On # ProxyPass / http://localhost:3000/ # ProxyPassReverse / http://localhost:3000/ </VirtualHost> みれましたね. /apiで Laravel にリクエストがいくようにしたらゴールとしましょう. まずは Laravel の.htaccessを有効化するために, httpd.confを以下のようにDirectoryディレクティブ(?)を追加しましょう(confを分けておいた方がいいとは思う) <VirtualHost *:80> DocumentRoot /var/www/project-name/public <Proxy *> Require all granted </Proxy> <Directory "/path/to/public"> AllowOverride All </Directory> ProxyRequests Off ProxyPreserveHost On # ProxyPass / http://localhost:3000/ # ProxyPassReverse / http://localhost:3000/ </VirtualHost> まだ編集中です 最終的にはポート分けました. それ以外の方法がわからなかった... Listen 80 NameVirtualHost *:80 Listen 81 NameVirtualHost *:81 ~~省略~~ <VirtualHost *:80> <Proxy *> Require all granted </Proxy> ProxyRequests Off ProxyPreserveHost On ProxyVia Off ProxyPass /api http://127.0.0.1:81/api ProxyPass / http://localhost:3000/ ProxyPassReverse / http://localhost:3000/ </VirtualHost> <VirtualHost *:81> DocumentRoot /path/to/public <Directory "/path/to/public"> AllowOverride All </Directory> </VirtualHost> これで/api/test/とかは Laravel で処理して, それ以外は Nuxt が動くようにしました!! あーしんどかった...
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TravisCIでLaravel用パッケージを複数のLaravelバージョンで自動テストする

概要 Testing Laravel Package Against Multiple Laravel Versions. Laravel用のパッケージを開発する際に、Travisを利用して複数のLaravelバージョンでテストを回すための設定ファイル.travis.yml について。 (検索のワードが良くないのか、イマイチ日本語の情報が出てこなかったので残しておく。) 主要な目的 Laravel用パッケージに対する自動テストをTravisで回す ※Laravel本体を使ったアプリケーションのテストではなく、パッケージのテストである点に注意 php、Laravelの各バージョンを組み合わせてのテストを回す ※本稿ではPHP7.2~7.4、Laravel6~8を対象とする 結論 language: php matrix: include: - php: '7.2' env: 'ORCHESTRA_VERSION=4.*' - php: '7.2' env: 'ORCHESTRA_VERSION=5.*' - php: '7.3' env: 'ORCHESTRA_VERSION=6.*' - php: '7.4' env: 'ORCHESTRA_VERSION=6.*' install: - 'composer config github-oauth.github.com ${GH_TOKEN};' - 'composer remove --dev --no-update orchestra/testbench' - 'composer require --dev --prefer-dist --no-interaction orchestra/testbench $ORCHESTRA_VERSION' - 'composer install' script: 'vendor/bin/phpunit' 本論 orchestral/testbench のバージョンについて Laravel用パッケージのテストについては、https://github.com/orchestral/testbench の利用がスタンダードであるので、これを利用する。まず、Laravel5.5以降のバージョンとorchestral/testbenchバージョンの対応について整理する。 orchestral/testbench Laravel PHP 備考 3.5 5.5 7.0 3.6 5.6 7.1 3.7 5.7 7.1 3.8 5.8 7.1 3.9 6.0 7.2 4.0 6.0 7.2 Laravelが6系からセマンティックバージョンを採用したためと思われる 4.* 6.* 7.2 5.* 7.* 7.2.5 Laravel7系はPHP7.2.5以上 6.* 8.* 7.3 7.* 9.* 8.0 testbench、Laravelともにdev(2021-08-01現在) Travisのテスト設定は、上記に基づいて考えていけばよい。 複数バージョンテストのためのcomposer.json Travisのためにcomposer.json を色々といじりまわすのは面倒なので、とりあえずrequire-dev の項目のバージョン指定を曖昧(*)にしておく。 "require-dev": { "phpunit/phpunit": "*", "mockery/mockery": "*", "vimeo/psalm": "*", "infection/infection": "*", "maglnet/composer-require-checker": "*", "slevomat/coding-standard": "*", "squizlabs/php_codesniffer": "*", "jakub-onderka/php-parallel-lint": "*", "orchestra/testbench": "*", "phpmetrics/phpmetrics": "*", "pdepend/pdepend": "*" }, こんな感じ。これで、基本的にはビルドした環境で使えるものが入るはず。 複数バージョンテストのための.travis.yml バージョン対応表に基づいて、使用するPHPバージョン、testbenchのバージョンを設定ファイルに書いていく。下記の要領。 matrix: include: - php: '7.2' env: 'ORCHESTRA_VERSION=4.*' - php: '7.2' env: 'ORCHESTRA_VERSION=5.*' - php: '7.3' env: 'ORCHESTRA_VERSION=6.*' - php: '7.4' env: 'ORCHESTRA_VERSION=6.*' 次に、リポジトリに同梱してあるcomposer.jsonの内容をテストのために必要最低限だけ書き換える。 今回は、orchestra/testbench のバージョンだけ明示的にコントロールしたいので install: - 'composer remove --dev --no-update orchestra/testbench' - 'composer require --dev --prefer-dist --no-interaction orchestra/testbench $ORCHESTRA_VERSION' - 'composer install' こんな感じ。composer.json に書いてある orchestra/testbench を一旦削除して、明示的にバージョンを指定する。 最後に、composerでinstallした、ビルド環境にあっているphpunitを叩く必要があるので、 script: 'vendor/bin/phpunit' こう。 所感 .travis.yml については、試行錯誤しながら実施したら結構すんなり行けた。過去に、複数のPHPバージョンでテストを回したことがある人なら、特に困る箇所は無いだろうと思う。 TravisCIの画面から環境変数追加できるの知らなかった・・・ めちゃ便利やん トラブルシューティング 発生したトラブルとしては主に以下の3点。 おそい(PHP7.2以下の環境だとビルドが遅い。) =>未解決、しょうがない気がする Laravel5系で、テストが失敗する(App::makePartial() が定義されていない、というエラー) =>一応解決(Laravel5系は対応バージョンから外した) composer require 時にgithubから怒られる(Could not authenticate against github.com ) =>解決(personal token 使いましょう) 参考URL Laravel Package Development: Testing Against Multiple Laravel Versions https://medium.com/homesteading-like-an-artisan/laravel-package-development-testing-against-multiple-laravel-versions-3485cb5762e2 Testing Laravel packages for backward compatibility using Travis https://medium.com/@remi_collin/testing-laravel-packages-for-backward-compatibility-using-travis-cabc3210e120 orchestral / testbench https://github.com/orchestral/testbench Github auth token on TravisCI https://blog.wyrihaximus.net/2015/09/github-auth-token-on-travis/ api-rate-limit-and-oauth-tokens https://getcomposer.org/doc/articles/troubleshooting.md#api-rate-limit-and-oauth-tokens
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む