20210304のlaravelに関する記事は13件です。

LaravelのEloquentとCollection

備忘録

主題の通り実務に入ってヒイヒイ言ってるクソ雑魚エンジニアの備忘録です。
では、

Eloquent

Laravelに備わっているデータベースを簡単に扱える機能。
これがあることで生のSQLを書かなくてもデータを追加したり参照したり
様々なことが可能になる。すごい。

Collection

Collectionとはリスト形式でデータを格納できるラッパー
のことらしい。
配列と何が違うの!と思うところ。
このCollectionめっちゃ便利

Collectionで使えるメソッド↓
https://readouble.com/laravel/6.x/ja/collections.html

Collectionは最初からwhereやsortBy,groupByなど様々なメソッドが標準で利用できて、
ソート、絞り込み、グルーピング、繰り返し、もうほんとになんでもできる

本題

EloquentのメソッドとCollectionのメソッド紛らわしくて混乱するわ!!

転職して初めて基幹システムの開発に放り込まれて詰まったところ。ほんこれ。
- get()
- where()
- groupBy()
- orderBy()
- all()
- first()
上のはほんの一部だがこれ全部EloquentにもCollectionにもある。

メソッドの意味もほとんど変わらないんだけど
微妙に扱いが変わってくるから注意が必要。

結論

EloquentとCollectionの使い方は記事が腐るほどあってすぐ慣れたので
要点をかいつまむと

モデルからgetやallで複数件データを取り出すと取り出したデータはCollection型となって返る
あらかじめ必要なレコードがわかっている場合はfirst()
まとめてデータを取得してそれぞれに処理を行いたい場合は
getやallで取得してforeachなどで都度取り出してデータ処理をかけばいい。

       / DBインスタンス
Collection ー DBインスタンス
       \ DBインスタンス

↑こんなイメージ

もうこれは使いまくって慣れるのが一番楽です。(脳筋)
それではまた。

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

laravel实现批量更新多条记录的方法示例

用代码实现凭借成下列sql语句即可同时批量更新多条数据
UPDATE table_name SET column = CASE 'id' WHEN 1 THEN ? WHEN 2 THEN ? ELSE column END WHERE id IN (1,2)

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

laravelで特殊な符号が含めているTABLEを取り扱う方法

table-1じゃsqlの操作ができない
ここは back quoteを付けて処理する
table-1-> table-1

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

LaravelでCloudWatchへログ送信

はじめに

LaravelのログをCloudWatchへ送信する方法は、CloudWatchエージェントを使う方法やaws firelensを使う方法などがあります。しかし、これらの方法はAWSのリソースを作らなくてはならず、設定などが少し手間です。
今回はLaravelのソース(とIAM Userの発行)だけで完結する方法を紹介します。

方法

まずは、ライブラリのインストールを行います。使用するライブラリはこちらです。
https://github.com/maxbanton/cwh

$ composer require maxbanton/cwh

インストールしたライブラリに含まれるLogハンドラーをコンテナに登録します。ポイントはCredentialProvider::defaultProvider()としているところで、このようにすることで、ローカル環境でも本番環境でもよしなに認証情報を取得してくれます。

app/Providers/AppServiceProvider.php
<?php

namespace App\Providers;

use Aws\CloudWatchLogs\CloudWatchLogsClient;
use Aws\Credentials\CredentialProvider;
use Illuminate\Support\ServiceProvider;
use Maxbanton\Cwh\Handler\CloudWatch;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(CloudWatchLogsClient::class, function ($app) {
            return new CloudWatchLogsClient([
                'region' => config('aws.default_region'),
                'version' => 'latest',
                'credentials' => CredentialProvider::defaultProvider(),
            ]);
        });

        $this->app->bind(CloudWatch::class, function ($app) {
            return new CloudWatch(
                $app->make(CloudWatchLogsClient::class),
                config('aws.cloudwatch.log.group_name'),
                config('aws.cloudwatch.log.stream_name'),
                config('aws.cloudwatch.log.retention'),
                10000
            );
        });
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

続いて、configファイルの設定を行います。config/aws.phpというファイルを作り、環境変数から読み取った値をセットします。

config/aws.php
<?php

return [
    'cloudwatch' => [
        'log' => [
            'group_name' => env('AWS_CLOUDWATCH_LOG_GROUP_NAME'),
            'stream_name' => env('AWS_CLOUDWATCH_LOG_STREAM_NAME'),
            'retention' => env('AWS_CLOUDWATCH_LOG_RETENTION'),
        ]
    ],
    'default_region' => env('AWS_DEFAULT_REGION'),
];

そして、config/logging.phpにCloudWatchへログを送信するためのチャネルを追加します。handlerには先ほどコンテナへ登録したハンドラのクラスを設定します。json形式でログを出力したい場合はformatterのコメントを解除してください。

config/logging.php
....
        'emergency' => [
            'path' => storage_path('logs/laravel.log'),
        ],

        'cloudwatch' => [
            'driver' => 'monolog',
            'handler' => Maxbanton\Cwh\Handler\CloudWatch::class,
            // 'formatter' => Monolog\Formatter\JsonFormatter::class, json形式でログを送信する時はコメント解除する
        ],
    ],
];

最後に、.envファイルへ環境変数の登録をします。
LOG_CHANNELには先ほどconfig/logging.phpに追加したログチャネルの名前を設定します。
あとは、AWSの認証情報とロググループ、ログストリーム、ログの保存期間などの設定値を入れます。この時登録するIAM USERには対象のロググループとログストリームへのアクセス権が必要なので注意してください!

.env
...
LOG_CHANNEL=cloudwatch
...
AWS_ACCESS_KEY_ID=XXXX
AWS_SECRET_ACCESS_KEY=XXXX
AWS_DEFAULT_REGION=ap-northeast-1
AWS_CLOUDWATCH_LOG_GROUP_NAME=laravel_cloudwatch_group
AWS_CLOUDWATCH_LOG_STREAM_NAME=laravel_cloudwatch_stream
AWS_CLOUDWATCH_LOG_RETENTION=1

実行

routes/web.phpにログの出力を仕込んでアクセスします。

routes/web.php
...
Route::get('/', function () {
    \Illuminate\Support\Facades\Log::info('ok');
    return view('welcome');
});

AWSコンソールからCloudWatchログを確認すると、ログが飛んできてるのがわかると思います。

スクリーンショット 2021-03-04 21.25.06.png

最後に

今回の方法は、ログのフォーマット変換から送信まで全てPHPのレイヤーで行っています。お手軽な一方で、PHPの処理には負担がかかっていることが想定できます。大量のログを飛ばすような場合は、やはり一度標準出力に出してからCloudWatchエージェントなりaws firelensなりでCloudWatchへ飛ばすのが良いのかなと思います。
実際に計測したわけじゃないので、どれくらい負荷がかかっているのかは分かりませんが。。。

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

[419エラー]ECSでセッションが保存されていない問題

概要

Laravel sanctumを利用してセッションベースのログイン認証を実装しており、ローカルではログインできていたのにAWS環境を使ったら419エラーが出てしまいました。

あまり情報もなく(というかAWS知識がなくてググり力が足りない)、解決するのに手間取ってしまったのでメモします。

AWS環境

ALB経由でECS(EC2)につなげている環境

419エラーとは

LaravelのPostする際のCSRF漏れエラーです。

解決策

今回はEC2インスタンスにつけているターゲットグループのスティッキーセッションが無効になっているのが原因でした。

スクリーンショット 2021-03-03 15.35.31.png

ターゲットグループ>group detailsタブのAttributesのEditをクリック
sticknessをdisabledからenabledに変更(画像は変更後です)。

まとめ

この項目をenabledにしないとセッションが保存されないみたい?なので、今後も注意したいです。

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

外部API呼び出しするクラスをモックする

この記事について

バックエンドから外部の API を呼び出すことがありますが、テストではそれをモックで差し替えたいシチュエーションで、Laravel + PHPUnit でどうやるといいか、備忘録です。

はじめに

環境

  • Laravel: 8.30.1
  • PHPUnit: 9.5.2
  • PHP: 8.0.2

概要

架空の API に ping というエンドポイントがあり、テスト時はそいつのスタブをつくって処理を上書きしつつ、想定される引数が渡されるか、とかのチェックをするようにします。

times で回数を指定して、その回数分 pong をカンマ区切りで返すとします。

curl -X POST -d times=2 https://api.some-service.example/ping
// pong,pong

実装

プロダクションコード

API 呼び出しを担当するクラスを ExternalApi として作成します。基本的にはエンドポイントにつき1つ公開メソッドを持たせるといいでしょう。

<?php

namespace App\Services\Api;

use GuzzleHttp\Client;

class ExternalApi
{
    private Client $http;

    public function __construct()
    {
        $this->http = new Client([
            'base_uri' => 'https://api.some-service.example',
        ]);
    }

    public function ping(?int $times = 1): string
    {
        assert($times > 0 && $times < 256);
        // 架空のサービスなのでローカルで完結するようにしておくが、イメージは下記のような感じ
        // $this->http->request('POST', 'ping', ['times' => $times]);
        return collect()->times($times)->map(fn() => 'pong')->join(',');
    }
}

呼び出しのコントローラーのイメージはこんな感じ。

Route::get('/ping', function (Request $request) {
    $user = $request->user();
    $api = app(\App\Services\Api\ExternalApi::class);
    $response = $api->ping($user->ping_count);
    return ['message' => $response];
});

テストコード

ExternalApi のモックをつくる処理をトレイトにまとめます。

<?php

namespace Tests\Concerns\Mock;

use App\Services\Api\ExternalApi;
use Mockery\MockInterface;

trait CreatesExternalApiMock
{
    public function createExternalApiMock(string $method, ...$args): MockInterface
    {
        $stubMethod = "{$method}Stub";
        if (!method_exists($this, $stubMethod)) {
            throw new \InvalidArgumentException('method not found.');
        }
        return $this->$stubMethod($args);
    }

    private function pingStub(array $args): MockInterface
    {
        $state = $args[0] ?? 'default';
        $method = "pingStub_${state}";
        $options = $args[1] ?? [];
        if (!method_exists($this, $method)) {
            throw new \InvalidArgumentException('state not found.');
        }
        return $this->$method($options);
    }

    private function pingStub_default(array $options): MockInterface
    {
        return $this->mock(ExternalApi::class, function (MockInterface $mock) {
            $mock->shouldReceive('ping')->with(null)->once()->andReturn('pong');
        });
    }

    private function pingStub_multiple(array $options): MockInterface
    {
        return $this->mock(ExternalApi::class, function (MockInterface $mock) use ($options) {
            $mock->shouldReceive('ping')->with($options['times'])->once()->andReturn(implode(',', array_fill(0, $options['times'], 'ping')));
        });
    }
}

パターンごとにスタブを用意してやります。

使い方のイメージはこんな感じ。

<?php

namespace Tests\Feature\Http\Controllers;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\Concerns\Mock\CreatesExternalApiMock;
use Tests\TestCase;

class PingControllerTest extends TestCase
{
    use RefreshDatabase;
    use CreatesExternalApiMock;

    /**
     * @param string $state
     * @param array $args
     * @param string $expected
     * @return void
     * @dataProvider dataPing
     */
    public function testPing(string $state, array $args, string $expected)
    {
        $user = factory(User::class)->create(['ping_count' => $args['times'] ?? null]);

        $this->createExternalApiMock('ping', $state, $args);

        $response = $this
            ->actingAs($user)
            ->get('/');

        $response
            ->assertOk()
            ->assertJson(['message' => $expected]);
    }

    public function dataPing(): array
    {
        return [
            '1' => [
                'state' => 'default',
                'args' => [],
                'expected' => 'pong',
            ],
            '2' => [
                'state' => 'multiple',
                'args' => ['times' => 2],
                'expected' => 'pong,pong',
            ],
        ];
    }
}

個人的には入出力のパターンのテストはフィーチャーテストではなくユニットテストでやりたいですが、今回は例なのでフィーチャーテストに書きました。

おわりに

いくつかのプロダクトでこの方法を導入していますが、いまのところ便利に使えています。他にも取り入れられそうなアイデアがあれば、コメント欄にて教えていただけると助かります :bow:

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

Laravelで外部API呼び出しするクラスをモックする

この記事について

バックエンドから外部の API を呼び出すことがありますが、テストではそれをモックで差し替えたいシチュエーションで、Laravel + PHPUnit でどうやるといいか、備忘録です。

はじめに

環境

  • Laravel: 8.30.1
  • PHPUnit: 9.5.2
  • PHP: 8.0.2

概要

架空の API に ping というエンドポイントがあり、テスト時はそいつのスタブをつくって処理を上書きしつつ、想定される引数が渡されるか、とかのチェックをするようにします。

times で回数を指定して、その回数分 pong をカンマ区切りで返すとします。

curl -X POST -d times=2 https://api.some-service.example/ping
// pong,pong

実装

プロダクションコード

API 呼び出しを担当するクラスを SomeServiceApi として作成します。基本的にはエンドポイントにつき1つ公開メソッドを持たせるといいでしょう。

<?php

namespace App\Services\Api;

use GuzzleHttp\Client;

class SomeServiceApi
{
    private Client $http;

    public function __construct()
    {
        $this->http = new Client([
            'base_uri' => 'https://api.some-service.example',
        ]);
    }

    public function ping(?int $times = 1): string
    {
        assert($times > 0 && $times < 256);
        // 架空のサービスなのでローカルで完結するようにしておくが、イメージは下記のような感じ
        // $this->http->request('POST', 'ping', ['times' => $times]);
        return collect()->times($times)->map(fn() => 'pong')->join(',');
    }
}

呼び出しのコントローラーのイメージはこんな感じ。

Route::get('/ping', function (Request $request) {
    $user = $request->user();
    $api = app(\App\Services\Api\SomeService::class);
    $response = $api->ping($user->ping_count);
    return ['message' => $response];
});

テストコード

SomeServiceApi のモックをつくる処理をトレイトにまとめます。

<?php

namespace Tests\Concerns\Mock;

use App\Services\Api\SomeServiceApi;
use Mockery\MockInterface;

trait CreatesExternalApiMock
{
    public function createExternalApiMock(string $method, ...$args): MockInterface
    {
        $stubMethod = "{$method}Stub";
        if (!method_exists($this, $stubMethod)) {
            throw new \InvalidArgumentException('method not found.');
        }
        return $this->$stubMethod($args);
    }

    private function pingStub(array $args): MockInterface
    {
        $state = $args[0] ?? 'default';
        $method = "pingStub_${state}";
        $options = $args[1] ?? [];
        if (!method_exists($this, $method)) {
            throw new \InvalidArgumentException('state not found.');
        }
        return $this->$method($options);
    }

    private function pingStub_default(array $options): MockInterface
    {
        return $this->mock(ExternalApi::class, function (MockInterface $mock) {
            $mock->shouldReceive('ping')->with(null)->once()->andReturn('pong');
        });
    }

    private function pingStub_multiple(array $options): MockInterface
    {
        return $this->mock(ExternalApi::class, function (MockInterface $mock) use ($options) {
            $mock->shouldReceive('ping')->with($options['times'])->once()->andReturn(implode(',', array_fill(0, $options['times'], 'ping')));
        });
    }
}

パターンごとにスタブを用意してやります。

使い方のイメージはこんな感じ。

<?php

namespace Tests\Feature\Http\Controllers;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\Concerns\Mock\CreatesExternalApiMock;
use Tests\TestCase;

class PingControllerTest extends TestCase
{
    use RefreshDatabase;
    use CreatesExternalApiMock;

    /**
     * @param string $state
     * @param array $args
     * @param string $expected
     * @return void
     * @dataProvider dataPing
     */
    public function testPing(string $state, array $args, string $expected)
    {
        $user = factory(User::class)->create(['ping_count' => $args['times'] ?? null]);

        $this->createExternalApiMock('ping', $state, $args);

        $response = $this
            ->actingAs($user)
            ->get('/');

        $response
            ->assertOk()
            ->assertJson(['message' => $expected]);
    }

    public function dataPing(): array
    {
        return [
            '1' => [
                'state' => 'default',
                'args' => [],
                'expected' => 'pong',
            ],
            '2' => [
                'state' => 'multiple',
                'args' => ['times' => 2],
                'expected' => 'pong,pong',
            ],
        ];
    }
}

個人的には入出力のパターンのテストはフィーチャーテストではなくユニットテストでやりたいですが、今回は例なのでフィーチャーテストに書きました。

おわりに

いくつかのプロダクトでこの方法を導入していますが、いまのところ便利に使えています。他にも取り入れられそうなアイデアがあれば、コメント欄にて教えていただけると助かります :bow:

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

LaravelとVue.jsを使った見積作成アプリ その3

前回の復習

前回はVue.jsを使用し見積編集ページのテンプレートを作成しました。今回はdompdfを利用したpdf表示ページとユーザー認証関係のページを作成していきます。

dompdfの導入

larave-dompdfパッケージのインストールはcomposerを使用して行います。

$ composer require barryvdh/laravel-dompdf

config/app.phpを以下のように編集します。
providersに下記を追記します。

app.php
Barryvdh\DomPDF\ServiceProvider::class,

aliasesに下記を追記します。

app.php
'PDF' => Barryvdh\DomPDF\Facade::class,

コントローラーの作成

PDFの作成を行うため、コマンドを使ってコントローラーを作成します。

$ php artisan make:controller PDFController

作成されたコントローラーに記述していきます。

PDFController.php
<?php

namespace App\Http\Controllers;

use App\Estimate;
use App\Item;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use PDF;

class PDFController extends Controller
{
    public function index(Request $request){
        $user = Auth::user();
        $estimate_id = $request->input('estimate');
        $estimate = Estimate::find($estimate_id);
        $estimated_at = $estimate->estimated_at;
        $items = Item::where('estimate_id', $estimate_id)->orderBy('id')->get();
        $item_price = Item::where('estimate_id', $estimate_id)->get(['quantity', 'unit_price']);
        $sum_price = 0;

        if($user->id != $estimate->user_id) {
            return redirect()->route('estimates.index');
        }

        for($i=0; $i<count($item_price); $i++){
            $calculation_price = $item_price[$i]['quantity'] * $item_price[$i]['unit_price'];
            $sum_price += $calculation_price;
        }

        $pdf = PDF::loadView('pdf/generate_pdf', [
            'user' =>$user,
            'estimate' => $estimate,
            'estimated_at' => date('Y年m月d日', strtotime($estimated_at)),
            'items' => $items,
            'sum_price' => $sum_price,
        ]);

        return $pdf->stream();
    }
}

loadView() を利用することで、通常のビューを用意するのと同じ手順で、PDFに出力したい内容をHTMLで記述することができるようになります。変数を渡すことも可能です。

ルーティングの作成

web.php
Route::get('pdf','PDFController@index');

これでテンプレートを作成すればPDFが表示されます。

日本語化

PDFの作成は可能になりましたが、このままだと日本語が文字化けしてしまいます。日本語のPDFが作成できるように設定を行います。

fontsディレクトリの作成

strageディレクトリの下にfontsディレクトリを作成します。

$ mkdir fonts

IPAフォントのダウンロード

以下のサイトからIPAフォントをダウンロードします。
https://moji.or.jp/ipafont/

zipファイルとしてダウンロードされるので、解凍後、その中にあるファイルを先ほど作成したstorage/fonts/ディレクトリの下にコピーしてください。

これで無事日本語に対応しましたのでテンプレートを作成していきます。

PDFテンプレートの作成

views/pdf/generate_pdf.blade.phpを作成し記述していきます。

generate_pdf.blade.php
<!doctype html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <style type="text/css">
        @font-face {
            font-family: ipam;
            font-style: normal;
            font-weight: normal;
            src: url('{{ storage_path('fonts/ipam.ttf') }}') format('truetype');
        }
        @font-face {
            font-family: ipam;
            font-style: bold;
            font-weight: bold;
            src: url('{{ storage_path('fonts/ipam.ttf') }}') format('truetype');
        }
        body {
            font-family: ipam !important;
        }
        .box {
          width        : 50px;
          height       : 50px;
          background   : white;
          border       : medium solid #000000;
          position: absolute;
        }
    </style>
</head>
<body>
  <h1 style="text-align:center;">御見積書</h1>
  <div style="float:left">
    <h2>{{ $estimate['customer'] }}様</h2>
    <p>下記の通り御見積申し上げます</p>
    <p>件名:{{ $estimate['title'] }}</p>
    <p>納入期限:{{ $estimate['deadline_at'] }}</p>
    <p>納入場所:{{ $estimate['location'] }}</p>
    <p>取引方法:{{ $estimate['transaction'] }}</p>
    <p>有効期限:{{ $estimate['effectiveness'] }}</p>
    <u><h2>御見積合計金額 ¥{{ number_format($sum_price * 1.1) }}-</h2></u>
  </div>
  <div style="float:right">
    <p>見積日 {{ $estimated_at }}</p>
    <p style="padding-top:25px">〒{{ $user['postal_code'] }}</p>
    <p>{{ $user['address'] }}</p>
    <p>{{ $user['address2'] }}</p>
    <p>{{ $user['company'] }}</p>
    <p>TEL: {{ $user['phone_number']}}  FAX: {{ $user['fax_number'] }}</p>
    <p>担当者: {{ $user['name'] }}</p>
  </div>
  <p class="box" style="margin-top:375px;margin-left: 547px;"></p>
  <a class="box" style="margin-top:375px;margin-left: 597px;"></a>
  <a class="box" style="margin-top:375px;margin-left: 647px;"></a>
  <div style="margin-top:360px">
    <table border="1" width="100%" cellspacing="0" cellpadding="0" style="table-layout: auto">
      <tr>
        <th>商品名</th>
        <th>単位</th>
        <th>数量</th>
        <th>単価</th>
        <th>金額</th>
        <th>備考</th>
      </tr>
      @foreach ($items as $item)
        <tr>
          <td>
            {{ $item['name'] }}
          </td>
          <td style="text-align:center">
            {{ $item['unit'] }}
          </td>
          <td style="text-align:right">
            {{ $item['quantity'] }}
          </td>
          <td style="text-align:right">
            {{ number_format($item['unit_price']) }}
          </td>
          <td style="text-align:right">
            {{ number_format($item['quantity'] * $item['unit_price']) }}
          </td>
          <td style="font-size:12px">
            {{ $item['other'] }}
          </td>
        </tr>
      @endforeach
      <tr>
        <td style="text-align:right"><税抜合計金額></td>
        <td></td>
        <td></td>
        <td></td>
        <td style="text-align:right">{{ number_format($sum_price) }}</td>
        <td></td>
      </tr>
      <tr>
        <td style="text-align:right"><消費税></td>
        <td></td>
        <td></td>
        <td></td>
        <td style="text-align:right">{{ number_format($sum_price * 0.1) }}</td>
        <td></td>
      </tr>
      <tr>
        <td>毎度ありがとうございます。</td>
        <td></td>
        <td></td>
        <td style="text-align:center">合計</td>
        <td style="text-align:right">{{ number_format($sum_price * 1.1) }}</td>
        <td></td>
      </tr>
    </table>
  </div>
</body>
</html>

headタグの@font-faceでは使用するフォントの設定を行っています。storage_pathにはIPAフォントを保存したstorage/fontsを指定します。
font-style:normalだけでは、h1タグのようなfont-weightがboldに設定されているフォントに文字化けが発生します。上記のようにfont-faceを複数設定することで対応できます。

これでPDF表示ページが完成しました。

認証機能

ユーザーと見積を紐付ける

認証機能の追加にともなって、データ構造としてはまずユーザーが存在して、ユーザーごとに自分の見積もりを作っていく形にしたいと思います。

マイグレーション

$ php artisan make:migration add_user_id_to_estimates --table=estimates

作成されたファイルを編集していきます。

add_user_id_to_estimates.php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddUserIdToEstimates extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('estimates', function (Blueprint $table) {
            $table->integer('user_id')->unsigned();
            $table->foreign('user_id')->references('id')->on('users');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('estimates', function (Blueprint $table) {
            $table->dropColumn('user_id');
        });
    }
}

upメソッドではuser_idカラムを追加して外部キーを設定する処理を記述しています。downメソッドでは逆にuser_idカラムを削除する処理を記述しています。
マイグレーションを実行しましょう。

$ php artisan migrate:fresh

次にユーザーと見積の関係性をモデルに記述していきます。

User.php
class User extends Authenticatable
{
    // 中略

    public function folders()
    {
        return $this->hasMany('App\Folder');
    }
}

シーダーの作成

ユーザーのシーダーを作成しましょう。

$ php artisan make:seeder UsersTableSeeder

database/seeds/UsersTableSeeder.php が作成されるので、以下の内容で編集します。

UserTableSeeder.php
<?php

use Carbon\Carbon;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class UsersTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        DB::table('users')->insert([
            'name' => 'test',
            'email' => 'dummy@email.com',
            'password' => bcrypt('test1234'),
            'created_at' => Carbon::now(),
            'updated_at' => Carbon::now(),
        ]);
    }
}

データベース上、ユーザーのパスワードは必ず暗号化してデータベースに保存します。平文では保存しません。bcrypt関数は与えられた文字列の暗号化を行います。

次に見積テーブル用のシーダーを編集します。星マークが追加した行です。

EstimateTableSeeder.php
<?php

use Carbon\Carbon;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class EstimateTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $user = DB::table('users')->first(); // ★

        $titles = ['2021年おめでとうセール', '商品見積の件', 'サンプル見積の件'];
        $customers = ['株式会社XXX', '株式会社YYY', '株式会社ZZZ'];

        foreach (array_map(NULL, $titles, $customers) as [ $title, $customer ]) {
            DB::table('estimates')->insert([
                'title' => $title,
                'user_id' => $user->id, // ★
                'customer' => $customer,
                'created_at' => Carbon::now(),
                'updated_at' => Carbon::now(),
            ]);
        }
    }
}

会員登録機能

Laravelには認証機能が最初から搭載されています。認証機能を受け持つコントローラーはapp/Http/Controllers/Authディレクトリにすでに用意されています。ルーティングについても認証用の設定を吐き出すメソッドが用意されているので、基本的にはテンプレートを作成するだけでアプリケーションに認証機能を追加することができます。

ルーティング

web.php
Auth::routes();

この1行を記述するだけで、会員登録・ログイン・ログアウト・パスワード再設定の各機能で必要なルーティング設定をすべて定義します。

テンプレート

以下の内容で resources/views/auth/register.blade.phpを作成します。

register.blade.php
@extends('layout')

@section('content')
  <div class="container">
    <div class="row">
      <div class="col col-md-offset-3 col-md-6">
        <nav class="panel panel-default">
          <h2 class="panel-heading" style="padding-top:25px">会員登録</h2>
          <div class="panel-body">
            @if($errors->any())
              <div class="alert alert-danger">
                @foreach($errors->all() as $message)
                  <p>{{ $message }}</p>
                @endforeach
              </div>
            @endif
            <form action="{{ route('register') }}" method="POST">
              @csrf
              <div class="form-group">
                <label for="email">メールアドレス</label>
                <input type="text" class="form-control" id="email" name="email" value="{{ old('email') }}" />
              </div>
              <div class="form-group">
                <label for="name">ユーザー名</label>
                <input type="text" class="form-control" id="name" name="name" value="{{ old('name') }}" />
              </div>
              <div class="form-group">
                <label for="password">パスワード</label>
                <input type="password" class="form-control" id="password" name="password">
              </div>
              <div class="form-group">
                <label for="password-confirm">パスワード(確認)</label>
                <input type="password" class="form-control" id="password-confirm" name="password_confirmation">
              </div>
              <div class="text-right">
                <button type="submit" class="btn btn-dark">送信</button>
              </div>
            </form>
            <div class="text-center" style="padding-top:25px">
              <a href="{{ route('login.guest') }}"><button class="btn btn-dark">ゲストユーザーとしてログイン</button></a>
            </div>
          </div>
        </nav>
      </div>
    </div>
  </div>
@endsection

次にログイン機能を実装します。

resources/views/auth/login.blade.phpを以下の内容で作成します。

login.blade.php
@extends('layout')

@section('content')
  <div class="container">
    <div class="row">
      <div class="col col-md-offset-3 col-md-6">
        <nav class="panel panel-default">
          <h2 class="panel-heading" style="padding-top:25px">ログイン</h2>
          <div class="panel-body">
            @if($errors->any())
              <div class="alert alert-danger">
                @foreach($errors->all() as $message)
                  <p>{{ $message }}</p>
                @endforeach
              </div>
            @endif
            <form action="{{ route('login') }}" method="POST">
              @csrf
              <div class="form-group">
                <label for="email">メールアドレス</label>
                <input type="text" class="form-control" id="email" name="email" value="{{ old('email') }}" />
              </div>
              <div class="form-group">
                <label for="password">パスワード</label>
                <input type="password" class="form-control" id="password" name="password" />
              </div>
              <div class="text-right">
                <button type="submit" class="btn btn-dark">ログイン</button>
              </div>
            </form>
          </div>
        </nav>
        <div class="text-center" style="padding-top:25px">
          <a href="{{ route('login.guest') }}"><button class="btn btn-dark">ゲストユーザーとしてログイン</button></a>
        </div>
      </div>
    </div>
  </div>
@endsection

これでログイン機能の実装は完了です。

プロフィール編集ページ

最後に見積に表示するためのプロフィール編集ページを作成します。

コントローラーの作成

プロフィール編集ページを作成するためのユーザーコントローラーを作成します。

$ php artisan make:controller ItemController
UserController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class UserController extends Controller
{
    public function showEditForm() 
    {
        $auth = Auth::user();

        return view('auth/edit',[
            'auth' => $auth,
        ]);
    }

    public function edit(Request $request)
    {
        $current_user = Auth::user();

        $current_user->name = $request->name;
        $current_user->postal_code = $request->postal_code;
        $current_user->address = $request->address;
        $current_user->address2 = $request->address2;
        $current_user->company = $request->company;
        $current_user->phone_number = $request->phone_number;
        $current_user->fax_number = $request->fax_number;

        $current_user->save();
        return redirect()->route('estimates.index');
    }
}

Auth::user()でユーザーの情報を取得しユーザー情報を表示・編集します。

ルーティングの追加

web.php
Route::group(['middleware' => 'api'], function(){
    Route::get('/user/edit', 'UserController@showEditForm')->name('user.edit');
    Route::post('/user/edit', 'UserController@edit');
});

あとはテンプレートを作成するだけです。

ユーザー編集ページのテンプレートを作成

@extends('layout')

@section('content')
  <div class="container">
    <h2 class="panel-heading" style="padding-top:25px">ユーザー情報編集</h2>
    <form action="{{ route('user.edit')}}" method="post">
      @csrf
      <div class="row">
        <div class="col-sm-2">ユーザー名</div>
        <div class="col-sm-10" style="padding: 3px;">
          <input type="text" name="name" value="{{$auth->name}}">
        </div>
      </div>
      <div class="row">
        <div class="col-sm-2">郵便番号</div>
        <div class="col-sm-10" style="padding: 3px;">
          <input type="text" name="postal_code" value="{{$auth->postal_code}}">
        </div>
      </div>
      <div class="row">
        <div class="col-sm-2">住所</div>
        <div class="col-sm-10" style="padding: 3px;">
          <textarea name="address" value="{{$auth->address}}">{{$auth->address}}</textarea>
        </div>
      </div>
      <div class="row">
        <div class="col-sm-2">ビル・マンション名</div>
        <div class="col-sm-10" style="padding: 3px;">
          <textarea name="address2">{{$auth->address2}}</textarea>
        </div>
      </div>
      <div class="row">
        <div class="col-sm-2">会社名</div>
        <div class="col-sm-10" style="padding: 3px;">
          <textarea name="company">{{$auth->company}}</textarea>
        </div>
      </div>
      <div class="row">
        <div class="col-sm-2">電話番号</div>
        <div class="col-sm-10" style="padding: 3px;">
          <input type="text" name="phone_number" value="{{$auth->phone_number}}">
        </div>
      </div>
      <div class="row">
        <div class="col-sm-2">FAX番号</div>
        <div class="col-sm-10" style="padding: 3px;">
          <input type="text" name="fax_number" value="{{$auth->fax_number}}">
        </div>
      </div>
      <div style="padding-top:25px;">
        <button type="submit" class="btn btn-dark">保存</button>
      </div>
    </form>
  </div>
@endsection

これで予定していた全ての機能が完成しました。

LaravelとVue.js共にまだまだ使いこなせてない機能がたくさんあるので勉強と開発を進めていこうと思います。

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

LaravelでぐるなびAPIを叩く方法(Guzzle)

Laravel5.8を使用している。

手順

guzzleをインストール

composer require guzzlehttp/guzzle

コントローラーに処理を書く

今回はぐるなびAPIでレストラン検索をするから、
RestaurantControllerを作成し、その中にget_restaurantアクションを作成した。

RestaurantController
public function get_restaurant()
    {
        $client = new \GuzzleHttp\Client();
        $response = $client->request(
            'GET',
            'https://api.gnavi.co.jp/RestSearchAPI/v3/',
            ['query' => [
                'keyid' => '123123123123123',
                'pref' => 'PREF39', 
                'freeword' => '餃子',   
                'hit_per_page' => 30
            ]]
        );

        $data = [
            'res' => json_decode($response->getBody(), true)
        ];
        return view('restaurant.index', $data);

まず、Clientのインスタンスを作成し、その中に
メソッド URL クエリ
を入れて、リクエストとして送っている。

getBody()メソッドで中身を取得することはできるが、これはオブジェクトが返されるため、
json_decodeでjson形式に変換している。

ビューを編集

restaurant/index.blade.php
@foreach ($res['rest'] as $r) 
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">おすすめ</div>

                <div class="card-body">
                    <h4>{{ $r['name'] }}</h4>
                    <br>
                    <p>{{ $r['pr']['pr_long'] }}</p>
                </div>
            </div>
        </div>
        @endforeach

ぐるなびAPIのレストラン検索機能では、
['rest']の中に複数のお店の情報が格納されており、さらに、その一つ一つのお店に
店名やその他詳細がぞれぞれ格納されているから、

rest-0-name
      -url
      -pr

    -1-name
      -url
      -pr

    -2-name
      -url
      -pr

このような順番で格納されている

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

Laravel Authのユーザ登録とログインのそれぞれのカスタマイズのやり方

ユーザ登録はname, age, tel, address, passwordに変更し、
ログイン画面はtel, passwordに変更する。

ユーザ登録のカスタマイズ

マイグレーションの作成

デフォルト状態から、カラムを変更する。

create_users_table.php
$table->bigIncrements('id');
            $table->string('name');
            $table->integer('age');
            $table->unsignedBigInteger('tel');
            $table->string('address');
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();

モデルの編集

$fillableのカラムを変更する

User.php
protected $fillable = [
        'name', 'age', 'tel', 'address', 'password',
    ];

コントローラーの編集

バリデーションアクション、新規保存アクションそれぞれを変更する

register.php
protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => ['required', 'string', 'max:255'],
            // 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'age' => ['required', 'integer', 'max:255'],
            'tel' => ['required', 'numeric', 'digits_between:10,11'],
            'address' => ['required', 'string', 'max:255'],
            'password' => ['required', 'string', 'min:8', 'confirmed'],
        ]);
    }


protected function create(array $data)
    {
        return User::create([
            'name' => $data['name'],
            // 'email' => $data['email'],
            'age' => $data['age'],
            'tel' => $data['tel'],
            'address' => $data['address'],
            'password' => Hash::make($data['password']),
        ]);
    }

register.blade.phpの変更

メールアドレスの項目を削除し、name項目をコピーして、nameと記載している箇所をすべて、適当なカラム名にする。
例として、age項目のみ下に記載している。

register.blade.php
                        //age 
                        <div class="form-group row">
                            <label for="age" class="col-md-4 col-form-label text-md-right">{{ __('Age') }}</label>

                            <div class="col-md-6">
                                <input id="age" type="number" min="1" class="form-control @error('age') is-invalid @enderror" name="age" value="{{ old('age') }}" required autocomplete="age" autofocus>

                                @error('age')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

以上の編集で新規登録のカスタマイズはできているはず。

ログイン機能のカスタマイズ

bladeファイルの編集

emailの項目を参考に、emailと記載の箇所を全て、telに変更している。

login.blade.php
                        //tel 
                        <div class="form-group row">
                            <label for="tel" class="col-md-4 col-form-label text-md-right">{{ __('Tel') }}</label>

                            <div class="col-md-6">
                                <input id="tel" type="text" class="form-control @error('tel') is-invalid @enderror" name="tel" value="{{ old('tel') }}" required autocomplete="tel" autofocus>

                                @error('tel')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

コントローラの編集

login.blade.phpを開いてみると、ログイン処理のアクション見当たらない。ログイン処理自体は、そのファイル内に記載している、
use AuthenticatesUsers;
の中身で実行されている。だから、そのクラスの中身を変更する必要がある。
以下のアクションを以下のように変更する。
一つ目がログインの際のバリデーション設定で、二つ目はよくわからないが、
emailを対象にしているよっていうことを宣言しているだけだと思う。だからここをtelに変更して、
telがログイン対象だよってことを変更した。

AuthenticatesUsers.php
protected function validateLogin(Request $request)
    {
        $request->validate([
            $this->username() => 'required',
            'password' => 'required|string',
        ]);
    }

public function username()
    {
        return 'tel';
    }

以上の編集でログインカスタムができていると思う。

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

Laravel クエリビルダで実行しているSQLを取得する

# 目的

  • Laravelのクエリビルダで実行しているSQLを取得する方法をまとめる

環境

  • ハードウェア環境
項目 情報
OS macOS Catalina(10.15.5)
ハードウェア MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports)
プロセッサ 2 GHz クアッドコアIntel Core i5
メモリ 32 GB 3733 MHz LPDDR4
グラフィックス Intel Iris Plus Graphics 1536 MB
  • ソフトウェア環境
項目 情報 備考
PHP バージョン 7.4.8 Homebrewを用いてこちらの方法で導入→Mac HomebrewでPHPをインストールする
Laravel バージョン 6.X commposerを用いてこちらの方法で導入→Mac Laravelの環境構築を行う
MySQLバージョン 8.0.19 for osx10.13 on x86_64 Homwbrewを用いてこちらの方法で導入→Mac HomebrewでMySQLをインストールする

情報

  • 筆者はMacのローカルにて本記事に記載されている内容を実装し確認する。

方法

  1. 下記のようなクエリビルダの記載が任意のメソッド内にあったとする。

    $saveInfo = $this->content;
    $saveInfo['user_id'] = $saveData['userId'];
    $saveInfo['content'] = $saveData['content'];
    $saveInfo->save();
    
  2. 下記のようにすることで変数$sqlに実行直前で実行されたSQL文が格納される。

    $saveInfo = $this->content;
    $saveInfo['user_id'] = $saveData['userId'];
    $saveInfo['content'] = $saveData['content'];
    $saveInfo->save();
    $sql = $saveInfo->toSql();
    
  3. 下記のように記載することでSQL文をログに出力する事ができる。

    use Illuminate\Support\Facades\Log;
    
    $saveInfo = $this->content;
    $saveInfo['user_id'] = $saveData['userId'];
    $saveInfo['content'] = $saveData['content'];
    $saveInfo->save();
    $sql = $saveInfo->toSql();
    
    Log::info($sql);
    
  4. 実際のメソッド内部に処理を記載したものを下記に記載する。筆者はRepositoryにDBアクセス処理を記載している。

    アプリ名ディレクトリ/Repositories/ContntRepository.php
    <?php
    
    namespace App\Repositories;
    
    use App\Repositories\ContentRepositoryInterface;
    use App\Models\Content;
    use Illuminate\Support\Facades\Log;
    
    class ContentRepository implements ContentRepositoryInterface
    {
        /**
         * @var Content
         */
        private $content;
    
        public function __construct(Content $content)
        {
          $this->content = $content;  
        }
    
        /**
         * 全ての投稿内容を取得する
         *
         * @return Content|\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model|null
         */
        public function getContentAll()
        {
            return $this->content->all();
        }
    
        public function saveContent($saveData)
        {
            $saveInfo = $this->content;
            $saveInfo['user_id'] = $saveData['userId'];
            $saveInfo['content'] = $saveData['content'];
            $saveInfo->save();
            $sql = $saveInfo->toSql();
            Log::info($sql);
    
            return true;   
        }
    }
    
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

docker-composeでcomposer require した時にFatal error: Allowed memory size of 1610612736 bytes exhausted ....が出た対処法

解決策

php -d memory_limit=-1 /usr/bin/composer require ****/****
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】FromRequestのバリデーションルールを共通化する + おまけ

・ヮ・)あ、おはようございまーす

みなさんは複数のリクエストで同じパラメータをバリデーションするときにどうしていますか?

私はコピペで書いていたのですが、ルール変更したいときに修正コスト高くなったり
同じパラメータなのに違うルールが書いてあったり…:joy:

そんなことがあったので Laravelのバリデーションルールを共通化するやつ を作りました
メモとして置いておきます

インストール方法

というにはおこがましいですが、ファイル1つだけなんで ValidationRules.php を app 直下にコピーしてださい

※違うディレクトリに置く場合は、名前空間をよしなに書き換えてください

使い方

ルールを定義する

RULES にバリデーションルールを定義します

protected const RULES = [
    'posts' => [
        'title' => ['string', 'between:10,255'],
        'body' => ['string'],
        'publish_at' => ['date'],
    ],
];
  • ネストすることもできます
  • ルールは配列記法、 | 区切りの文字列記法に対応しています

ルールを取得する

FormRequest などバリデーションルールを取得したいところで getRules() を呼び出します

public function rules(\App\ValidationRules $rules)
{
    return $rules->getRules([
        'posts.title' => ['required'],
        'posts.body',
        'posts.publish_at',
    ]);
}
  • ネストされたルールは . (ドット)記法で書いてください
  • 追加したいルールがあれば、連想配列にして渡してあげてください
  • 定義していないルールを取得しようとすると OutOfRangeException がスローされます

上の例では以下のルールが返ります

[
  'title' => ['required', 'string', 'between:10,255'],
  'body' => ['string'],
  'publish_at' => ['date'],
]

パラメーター名に別名をつける

users.namepets.name のルールを取得しようとすると、後に指定した pets.name のルールのみ name のキーで返ってきます

このような状況を避けるには第2引数で別名を指定します

$keyAliases = [
    'users.name' => 'user_name',
    'pets.name' => 'pet_name',
];

app(\App\ValidationRules::class)->getRules(
    [
        'users.name',
        'pets.name',
    ],
    $keyAliases
);

また、常に別名を付けたいパラメータ名がある場合は ValidationRules::KEY_ALIASES に定義してください

getRules() の第2引数
KEY_ALIASES 定数
ドット区切りの最後の文字列(デフォルト)
の優先順位でパラメータ名が採用されます

※パラメータ名を変えた場合は、required_if のような他のパラメータを参照するルールに気をつけてください
※また、 attributes にも気をつけてください

カスタマイズする

ルールを定義する場所を変更する

ValidationRules::RULES じゃない場所にしたい場合は all() を書き換えるか、 ValidationRules を継承してオーバーライドしてください

指定したルールが見つからなかった場合の挙動を変えたい

デフォルトでは定義していないルールを取得しようとすると OutOfRangeException がスローされます

定義していないルールを取得しようとしたとき、空配列を返したい場合は notFound() を書き換えるか、 ValidationRules を継承してオーバーライドしてください

おまけ

FormRequestにゲッターメソッドを作る

コントローラーで Requestの input all query get __get only except 使うのって怖くないですか?
FormRequestでバリデーションされているから安心?
FormRequest側でバリデーションルール変更したとき影響の調査面倒じゃないでしょうか?

public function getEmail(): string
{
    return $this->input('email');
}

こんなメソッドをバリデーションを実施しているFormRequestに書いてあげて
ついでにユニットテストも書いてあげれば
コントローラーで安心してリクエストパラメーターを受け取れますね

また、バリューオブジェクトに変換してあげることもできるのでDDDニキもにっこりですね:relaxed:

FormRequestでゲッターを書いてよりよいLaravelライフを!

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