20200213のlaravelに関する記事は10件です。

DB変更のmigrationを出来るようにする

DB変更用のライブラリのインストール

以下のコマンドを実行する。

cd ~/environment/xxxxx
composer require doctrine/dbal
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Larave Facadeの使い方

ファサードとは

ファサードとは、コンテナを通じてオブジェクトにアクセス方法を提供するクラスのことです

公式サイト

https://readouble.com/laravel/5.5/ja/facades.html

制作物のゴール

Hogeファサードの作成
hogehpgeを出力する

サービスプロバイダーの作成

$ php artisan make:provider HogeServiceProvider
Provider created successfully.

hogeクラスの登録

app/Providers/HogeServiceProvider.php
    public function register()
    {
        $this->app->bind(
          'hoge',
          'App\Http\Compornents\Hoge'
        );
    }

bind()メソッドの第一引数にはいわゆる識別子としての名前を記載する
第二引数には、対象となる独自処理クラスであるHogeクラスを渡します。
今回の設置箇所はApp\Http\Compornents\Hogeとする。

ファサードクラスの作成

laravel/app 配下にFacadesディレクトリを作成し、その中にHoge.phpを作成

コンポーネントの登録名を取得する為のアクセサーgetFacadeAccessor()メソッドを以下のように定義します。

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

use Illuminate\Support\Facades\Facade;


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

独自処理クラスの作成

laravel/app/Http 配下に Components ディレクトリを作成し、その中にHoge.phpを作成

app/Http/Components/Hoge.php
<?php
namespace App\Http\Components;

class Hoge
{
  public function echoHoge()
  {
    return 'HOGEHOGE';
  }
}

サービスプロパイダーとエイリアスの登録

config/app.php
<?php

return [
    'providers' => [

        App\Providers\HogeServiceProvider::class,

    ],

    'aliases' => [

        'Hoge' => App\Facades\Hoge::class,

    ],

];

tinkerで動作確認

$ php artisan tinker
Psy Shell v0.9.12 (PHP 7.3.13 — cli) by Justin Hileman
>>> Hoge::echoHoge();
=> "HOGEHOGE"
>>> 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】マルチログイン(ユーザーと管理者)機能

Laravel6で認証をユーザーと管理者など複数にわける場合のメモ。

まずは普通にログイン機能を実装

Laravel 6.0 ログイン機能を実装する - Qiita

Laravel6.0「make:Auth」が無くなった 〜Laravel6.0でのLogin機能の実装方法〜MyMemo - Qiita

認証 6.x Laravel

ここは参考になる記事が多くあるので説明は省略。

作成したログイン機能に追加

新しく管理者(Admin)の認証機能を追加する。

1. モデルクラスを作成

他のモデルクラスと同じようにマイグレーションファイル(DB)とモデルクラス(Admin.php)を作成。

モデルクラスはIlluminate\Database\Eloquent\ModelではなくIlluminate\Foundation\Auth\Userを継承する。

Admin.php
use Illuminate\Foundation\Auth\User as Authenticatable;

class Admin extends Authenticatable // ModelではなくAuthenticatable 
{
}

2. 認証ファイルに追記

下記の内容をconfig/auth.phpに追記する。

auth.php
'guards' => [
...
        'admin' => [
            'driver' => 'session',
            'provider' => 'admin',
        ],
...
],

'providers' => [
...
        'admin' => [
            'driver' => 'eloquent',
            'model' => App\Admin::class, // 1 で作成したモデルを指定
        ],
...
],

'passwords' => [
...
        'admin' => [
            'provider' => 'admin',
            'table' => 'password_resets',
            'expire' => 60,
        ],
...
],

3. Routeに認証機能を追加

web.php
Route::group(['middleware' => 'auth:admin'], function () {
...
});

4. ログインしていない場合のリダイレクト先を指定

ログイン処理が必要なURLにログインせずにアクセスした場合のリダイレクト先を
ユーザー、管理者等で分けたい場合は記述する。

app/Exceptions/Handler.php
   protected function unauthenticated($request, AuthenticationException $exception)
    {
        if($request->expectsJson()){
            return response()->json(['message' => $exception->getMessage()], 401);
        }

        if (in_array('admin', $exception->guards(), true)) {
            return redirect()->guest(route('admin.login'));
        }

        return redirect()->guest(route('login'));
    }

5. ログイン後にログインページにアクセスした場合のリダイレクト先を指定

app/Http/Middleware/RedirectIfAuthenticated
    public function handle($request, Closure $next, $guard = null)
    {
        switch ($guard) {
            case 'admin':
                $redirectPath = '/admin/index';
                break;
            default:
                $redirectPath = '/index';
                break;
        }
        if (Auth::guard($guard)->check()) {
            return redirect($redirectPath);
        }

        return $next($request);
    }

6. ログインコントローラー作成

ユーザーの認証に使用しているLoginControllerとは別に作成する。

LoginController
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;

class LoginController extends Controller
{
    use AuthenticatesUsers;

    protected $redirectTo = 'admin/index';  // ログイン後のリダイレクト先

    public function __construct(Request $request)
    {
        $this->middleware('guest:admin')->except('logout');;
    }

    protected function guard()
    {
        return Auth::guard('admin');
    }

}

その他username, showLoginFormなどのメソッドはユーザーのLoginControllerと同じように、必要に応じて設定する。

参考

Laravelでマルチ認証(マルチログイン)を実装する

【Laravel】マルチログイン(ユーザーと管理者など)機能を設定してみた【体験談】 | もんプロ~問題発見と解決のためのプログラミング〜

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

LaravelでSalesForceのREST APIを利用する

概要

LaravelでSalesForceのREST APIを利用する方法について調査したので記事として残しておく。
ここではよく使いそうなCRUD処理のサンプルを記載する。
※ Laravel特有の部分はあまりないので、他のフレームワークや生PHPでもいけます。

前提

CRUD処理サンプル

CREATE

    /**
     * SalesForce REST API Create
     * @Route /sample/sf/api/create
     * @Method POST
     */
    public function sfApiCreateRecord()
    {
        // 環境によって違うので自分の環境の値に変更してください
        $sfUrl = 'https://hoge.salesforce.com';
        $path  = '/services/data/v47.0/sobjects/account';

        $url   = $sfUrl . $path;
        $token = 'hogefugapiyo'; //tokenを自動で取得してくるような仕組みはこの記事では省略する

        $postData = [
            'Name' => 'nagi125',
            'Description' => 'REST API test',
        ];

        $params = [
            'headers' => [
                'Content-Type' => 'application/json',
                'Authorization' => 'Bearer '.$token,
            ],
            'body' => json_encode($postData),
        ];

        $client = new \GuzzleHttp\Client();
        $res = $client->request('POST', $url, $params);

        // 結果の確認
        dd($res->getBody()->getContents());
    }

READ

    /**
     * SalesForce REST API Read
     * @Route /sample/sf/api/read
     * @Method GET
     */
    public function sfApiReadRecord()
    {
        $sfUrl = 'https://hoge.salesforce.com';
        $path  = '/services/data/v47.0/sobjects/account/';
        $sfId  = 'hogefuga';

        // このようにSQLの結果も取得できる
        // $path = '/services/data/v47.0/query/?q=SELECT+Id,Name+From+Account';

        $url   = $sfUrl . $path . $sfId;
        $token = 'hogefugapiyo';

        $params = [
            'headers' => [
                'Authorization' => 'Bearer '.$token,
            ],
        ];

        $client = new \GuzzleHttp\Client();
        $res = $client->request('GET', $url, $params);

        // 結果の確認
        dd($res->getBody()->getContents());
    }

UPDATE

    /**
     * SalesForce REST API Update
     * @Route /sample/sf/api/update
     * @Method PATCH
     */
    public function sfApiUpdateRecord()
    {
        $sfUrl = 'https://hoge.salesforce.com';
        $path  = '/services/data/v47.0/sobjects/account/';
        $sfId  = 'hogefuga';

        $url   = $sfUrl . $path . $sfId;
        $token = 'hogefugapiyo';

        $postData = [
            'Name' => 'nagi125_update',
        ];

        $params = [
            'headers' => [
                'Content-Type' => 'application/json',
                'Authorization' => 'Bearer '.$token,
            ],
            'body' => json_encode($postData),
        ];

        $client = new \GuzzleHttp\Client();
        $res = $client->request('PATCH', $url, $params);

        // 結果の確認
        dd($res->getBody()->getContents());
    }

DELETE

    /**
     * SalesForce RestAPI Delete
     * @Route /sample/sf/api/delete
     * @Method DELETE
     */
    public function sfApiDeleteRecord()
    {
        $sfUrl = 'https://hoge.salesforce.com';
        $path  = '/services/data/v47.0/sobjects/account/';
        $sfId  = 'hogefuga';

        $url   = $sfUrl . $path . $sfId;
        $token = 'hogefugapiyo';

        $params = [
            'headers' => [
                'Authorization' => 'Bearer '.$token,
            ],
        ];

        $client = new \GuzzleHttp\Client();
        $res = $client->request('DELETE', $url, $params);

        // 結果の確認(deleteの場合、成功時に文字列が返ってこないのでStatusCodeで確認)
        dd($res->getStatusCode());
    }

その他

残りの操作について詳しく知りたい場合はREST APIの仕様書を見るとよいです。

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

PHP(Laravel)でSalesForceのREST APIを利用する

概要

LaravelでSalesForceのRESTAPIを利用する方法について調査したので記事として残しておく。
ここではよく使いそうなCRUD処理のサンプルを記載する。
※ Laravel特有の部分はあまりないので、他のフレームワークや生PHPでもいけます。

前提

CRUD処理サンプル

CREATE

    /**
     * SalesForce REST API Create
     * @Route /sample/sf/api/create
     * @Method POST
     */
    public function sfApiCreateRecord()
    {
        // 環境によって違うので自分の環境の値に変更してください
        $sfUrl = 'https://hoge.salesforce.com';
        $path  = '/services/data/v47.0/sobjects/account';

        $url   = $sfUrl . $path;
        $token = 'hogefugapiyo'; //tokenを自動で取得してくるような仕組みはこの記事では省略する

        $postData = [
            'Name' => 'nagi125',
            'Description' => 'REST API test',
        ];

        $params = [
            'headers' => [
                'Content-Type' => 'application/json',
                'Authorization' => 'Bearer '.$token,
            ],
            'body' => json_encode($postData),
        ];

        $client = new \GuzzleHttp\Client();
        $res = $client->request('POST', $url, $params);

        // 結果の確認
        dd($res->getBody()->getContents());
    }

READ

    /**
     * SalesForce REST API Read
     * @Route /sample/sf/api/read
     * @Method GET
     */
    public function sfApiReadRecord()
    {
        $sfUrl = 'https://hoge.salesforce.com';
        $path  = '/services/data/v47.0/sobjects/account/';
        $sfId  = 'hogefuga';

        // このようにSQLの結果も取得できる
        // $path = '/services/data/v47.0/query/?q=SELECT+Id,Name+From+Account';

        $url   = $sfUrl . $path . $sfId;
        $token = 'hogefugapiyo';

        $params = [
            'headers' => [
                'Authorization' => 'Bearer '.$token,
            ],
        ];

        $client = new \GuzzleHttp\Client();
        $res = $client->request('GET', $url, $params);

        // 結果の確認
        dd($res->getBody()->getContents());
    }

UPDATE

    /**
     * SalesForce REST API Update
     * @Route /sample/sf/api/update
     * @Method PATCH
     */
    public function sfApiUpdateRecord()
    {
        $sfUrl = 'https://hoge.salesforce.com';
        $path  = '/services/data/v47.0/sobjects/account/';
        $sfId  = 'hogefuga';

        $url   = $sfUrl . $path . $sfId;
        $token = 'hogefugapiyo';

        $postData = [
            'Name' => 'nagi125_update',
        ];

        $params = [
            'headers' => [
                'Content-Type' => 'application/json',
                'Authorization' => 'Bearer '.$token,
            ],
            'body' => json_encode($postData),
        ];

        $client = new \GuzzleHttp\Client();
        $res = $client->request('PATCH', $url, $params);

        // 結果の確認
        dd($res->getBody()->getContents());
    }

DELETE

    /**
     * SalesForce RestAPI Delete
     * @Route /sample/sf/api/delete
     * @Method DELETE
     */
    public function sfApiDeleteRecord()
    {
        $sfUrl = 'https://hoge.salesforce.com';
        $path  = '/services/data/v47.0/sobjects/account/';
        $sfId  = 'hogefuga';

        $url   = $sfUrl . $path . $sfId;
        $token = 'hogefugapiyo';

        $params = [
            'headers' => [
                'Authorization' => 'Bearer '.$token,
            ],
        ];

        $client = new \GuzzleHttp\Client();
        $res = $client->request('DELETE', $url, $params);

        // 結果の確認(deleteの場合、成功時に文字列が返ってこないのでStatusCodeで確認)
        dd($res->getStatusCode());
    }

その他

残りの操作について詳しく知りたい場合はREST APIの仕様書を見るとよいです。

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

SalesForceのaccess_token取得方法

概要

SalesForceのAccessToken取得方法をまとめておく。
※refresh_tokenについてはこちらを参照
同時にLaravel + Guzzuleを利用したPHPのサンプルも載せておく。

結論

取得方法は複数あるがここでは「ユーザ名パスワードフロー」と「更新トークンフロー」の2種類を記載する。
※ 利用しているAPIのバージョンはv47.0です。

共通作業

ユーザー名・パスワードフロー

パラメータをつけて下記のURLに「POST」でアクセスする。
https://login.salesforce.com/services/oauth2/token

パラメータ
grant_type password
client_id *****
client_secret *****
username ****
password ****

01.png

更新トークンフロー

refresh_tokenが必要になるため、refresh_tokenをこちらの記事の方法で取得しておく。
パラメータをつけて下記のURLに「POST」でアクセスする。
https://login.salesforce.com/services/oauth2/token

パラメータ
grant_type refresh_token
client_id *****
client_secret *****
refresh_token *****

02.png

Laravel + Guzzleサンプル

Laravelを利用して書いていますが、機能としてはenvぐらいしか使っていないので生PHPでも普通にいけます。
Guzzleを利用したサンプルと思っていただければと思います。

    private function refreshSFAccessToken(): string
    {
        $path = '/services/oauth2/token';
        $url  = 'https://login.salesforce.com' . $path;

        $params = [
            'form_params' => [
                'grant_type' => 'refresh_token',
                'client_id'  => env('SF_ID'),
                'client_secret' => env('SF_SECRET'),
                'refresh_token' => env('SF_REFRESH_TOKEN'),
            ]
        ];

        $client  = new \GuzzleHttp\Client();
        $resJson = $client->request('POST', $url, $params)->getBody()->getContents();
        $resAry  = json_decode($resJson, true);

        return $resAry['access_token'] ?? '';
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【MySQL】外部キー制約を追加したり確認したり【ふ〜ん】

laravelを勉強していて、マイグレーションファイルから外部キー制約を付けていたのですが、後から付けたい〜!ってなったときにMySQLの方で付ける方法知りたかったのでメモ


ALTER TABLE テーブル名 ADD FOREIGN KEY (カラム名) REFERENCES テーブル名(カラム名);

これでつく!!!

例えば、userとtwitterの関係が1対多だとすると、
twittersテーブルのuser_idと、usersテーブルのidを紐付ける必要があるので、

ALTER TABLE twitters ADD FOREIGN KEY (user_id) REFERENCES users(id);

これでいいはず。

外部キー制約を確認する方法は、

show create table twitters;

これを実行すると、

| twitters | CREATE TABLE `twitters` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  `content` varchar(20) COLLATE utf8_unicode_ci DEFAULT NULL,
  `user_id` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `user_id` (`user_id`),
  CONSTRAINT `tasks_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci |

こんなのが出てくるので、確認できるよ〜!!!!!

初心者すぎてぴえん。

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

Laravel CSVの出力処理を実装する

概要

Laravelを使って、CSVファイルを出力するサンプルを作成します。

背景

データベースやファイルアクセスをしないテストの方法、インターフェースやジェネレータを使ったコードの書き方等、個別に詳しく書かれた記事はあれど実際使うにはどう書き始めたらいいのかベストプラクティスがわかりませんでした。

日々の業務や副業、勉強会を通じてようやく自分の中で少しずつイメージができてきたので現時点で最高のアウトプットをしていこうと思いました。

目的

この記事ではテストを意識したコードかつ、シンプルに書くことを目標にしてます。
より良いコードにしたいのでアドバイスもらえたらうれしいです。

説明不足なところがあったら補足を追記するので気軽に質問等もいただけたら嬉しいです。

環境

  • PHP 7.4.1
  • Laravel 6.14.0
  • MySQL 8.0.19

サンプルコード

https://github.com/ucan-lab/learn-laravel-export-csv

$ git clone git@github.com:ucan-lab/learn-laravel-export-csv.git
$ cd learn-laravel-export-csv
$ make install
$ make app
$ php artisan migrate:fresh --seed

# csv出力コマンド
$ php artisan export:user

# テスト実行
$ ./vendor/bin/phpunit

今回のゴール

スクリーンショット 2020-02-20 12.18.33.png

users テーブルに入ってるデータをcsv出力する処理を作るところまでゴールとします。

名前,メールアドレス,作成日,更新日
PROF. RACHELLE KUHIC I,leola.rath@example.com,2020-02-01 23:59:59,2020-02-01 23:59:59
JAYLON WOLF,osinski.fernando@example.net,2020-02-01 23:59:59,2020-02-01 23:59:59
LELAND DECKOW,bokon@example.org,2020-02-01 23:59:59,2020-02-01 23:59:59

名前の列は大文字に変換して出力する仕様です。

ベースのコード

環境はこちらのコードを丸コピしてます。

追加したファイル一覧

https://github.com/ucan-lab/learn-laravel-export-csv/pull/1

src/app/Console/Commands/ExportUserCommand.php
src/app/Domain/UserRow.php
src/app/Domain/UserRowHeader.php
src/app/Http/Controllers/Auth/RegisterController.php
src/app/Infrastructure/Adapter/DbUserRepository.php
src/app/Infrastructure/Adapter/FileUserCsvExport.php
src/app/Infrastructure/Adapter/InMemoryUserCsvExport.php
src/app/Infrastructure/Adapter/InMemoryUserRepository.php
src/app/Infrastructure/Eloquent/User.php
src/app/Infrastructure/Port/Export.php
src/app/Infrastructure/Port/UserRepository.php
src/app/Providers/AppServiceProvider.php
src/app/UseCase/UserCsvExportUseCase.php
src/database/factories/UserFactory.php
src/database/seeds/DatabaseSeeder.php
src/database/seeds/UsersTableSeeder.php
src/tests/Unit/UserCsvExportUseCaseTest.php

マイグレーション(テーブル)の確認

今回はLaravelが元々用意してくれている users テーブルをそのまま使います。

src/database/migrations/2014_10_12_000000_create_users_table.php
<?php

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

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

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

MySQLのテーブル定義も確認しておきます。

$ make mysql
mysql> desc users;
+-------------------+-----------------+------+-----+---------+----------------+
| Field             | Type            | Null | Key | Default | Extra          |
+-------------------+-----------------+------+-----+---------+----------------+
| id                | bigint unsigned | NO   | PRI | NULL    | auto_increment |
| name              | varchar(255)    | NO   |     | NULL    |                |
| email             | varchar(255)    | NO   | UNI | NULL    |                |
| email_verified_at | timestamp       | YES  |     | NULL    |                |
| password          | varchar(255)    | NO   |     | NULL    |                |
| remember_token    | varchar(100)    | YES  |     | NULL    |                |
| created_at        | timestamp       | YES  |     | NULL    |                |
| updated_at        | timestamp       | YES  |     | NULL    |                |
+-------------------+-----------------+------+-----+---------+----------------+

app/User.php => app/Infrastructure/Eloquent/User.php

LaravelのEloquentモデルはデフォルトだとapp直下に配置されます。
app/Infrastructure/Eloquent/User.php へ移動します。
依存するファイルも合わせて修正します。詳細はコミットログ参照

シーダーの作成

Laravelには、シーディングモデルファクトリFakerが用意されており、ダミーデータを簡単に作成できます。

$ php artisan make:seeder UsersTableSeeder

src/database/seeds/UsersTableSeeder.php シーダーのひな形クラスを作ってくれるので下記のように追記します。

src/database/seeds/UsersTableSeeder.php
<?php declare(strict_types=1);

use App\Infrastructure\Eloquent\User;
use Illuminate\Database\Seeder;

class UsersTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        factory(User::class, 3)->create();
    }
}

上記の用にシーダーを追加するだけで、テストデータを3件作成してくれます。
Userモデルクラスの各プロパティにどんなデータが入るかの定義はモデルファクトリで定義されてます。

src/database/factories/UserFactory.php
<?php declare(strict_types=1);

/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\Infrastructure\Eloquent\User;
use Faker\Generator as Faker;
use Illuminate\Support\Str;

/*
|--------------------------------------------------------------------------
| Model Factories
|--------------------------------------------------------------------------
|
| This directory should contain each of the model factory definitions for
| your application. Factories provide a convenient way to generate new
| model instances for testing / seeding your application's database.
|
*/

$factory->define(User::class, function (Faker $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'email_verified_at' => now(),
        'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
        'remember_token' => Str::random(10),
    ];
});

元々用意されている src/database/seeds/DatabaseSeeder.phpUsersTableSeeder を呼び出す記述を追記します。

src/database/seeds/DatabaseSeeder.php
<?php declare(strict_types=1);

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
         $this->call(UsersTableSeeder::class);
    }
}

UserRow, UserRowHeader ドメインを定義

この辺りから本題です。

App\Domain\UserRowHeader

app/Domain/UserRowHeader.php
<?php declare(strict_types=1);

namespace App\Domain;

final class UserRowHeader
{
    private const EOF = "\n";
    private const HEADER = [
        '名前',
        'メールアドレス',
        '作成日',
        '更新日',
    ];

    public static function toCsv(): string
    {
        return implode(',', self::HEADER) . self::EOF;
    }
}

UserRowHeader ドメインクラスではCSVのヘッダー行となる1行目の定義をしてます。

App\Domain\UserRow

app/Domain/UserRow.php
<?php declare(strict_types=1);

namespace App\Domain;

use Carbon\Carbon;

final class UserRow
{
    private const EOF = "\n";
    private const DATE_FORMAT = 'Y-m-d H:i:s';

    private string $name;
    private string $email;
    private Carbon $createdAt;
    private Carbon $updatedAt;

    public function __construct(
        string $name,
        string $email,
        Carbon $createdAt,
        Carbon $updatedAt
    ) {
        $this->name = $name;
        $this->email = $email;
        $this->createdAt = $createdAt;
        $this->updatedAt = $updatedAt;
    }

    /**
     * @return string
     */
    public function toCsv(): string
    {
        return implode(',', $this->toArray()) . self::EOF;
    }

    /**
     * @return array
     */
    private function toArray(): array
    {
        return [
            $this->getName(),
            $this->email,
            $this->createdAt->format(self::DATE_FORMAT),
            $this->updatedAt->format(self::DATE_FORMAT),
        ];
    }

    /**
     * @return string
     */
    private function getName(): string
    {
        return strtoupper($this->name);
    }
}

UserRow ドメインクラスではCSVの1行の定義をしてます。
ユーザー名は大文字や日付のフォーマット等の業務ロジックはここにまとめます。

UserCsvExportUseCase を定義

app/Infrastructure/Export.php
<?php declare(strict_types=1);

namespace App\Infrastructure\Port;

interface Export
{
    public function prepare(string $header): void;
    public function write(string $row): void;
    public function disorganize(): void;
}

Export インターフェースを継承するクラスは prepare(前処理)、write(書き込み)、disorganize(後処理)のメソッドを契約します。

app/Infrastructure/UserRepository.php
<?php declare(strict_types=1);

namespace App\Infrastructure\Port;

use Generator;

interface UserRepository
{
    public function findAll(): Generator;
}

UserRepository インターフェースを継承するクラスはfindAll(全件取得)のメソッドを契約します。

app/UseCase/UserCsvExportUseCase.php
<?php declare(strict_types=1);

namespace App\UseCase;

use App\Domain\UserRow;
use App\Domain\UserRowHeader;
use App\Infrastructure\Port\Export;
use App\Infrastructure\Port\UserRepository;

final class UserCsvExportUseCase
{
    /**
     * @var UserRepository
     */
    private UserRepository $repository;

    /**
     * @var Export
     */
    private Export $export;

    /**
     * @param UserRepository $repository
     * @param Export $export
     */
    public function __construct(UserRepository $repository, Export $export)
    {
        $this->repository = $repository;
        $this->export = $export;
    }

    /**
     * @return void
     */
    public function handle(): void
    {
        $this->export->prepare(UserRowHeader::toCsv());

        /** @var UserRow $row */
        foreach ($this->repository->findAll() as $row) {
            $this->export->write($row->toCsv());
        }

        $this->export->disorganize();
    }
}

UserCsvExportUseCase ユースケースクラスはUserRepositoryとExportインターフェースに依存します。
前処理して、全件取得して、書き込みして、後処理して終わるシンプルな作りにできました。
各インターフェースを契約するクラスの中身はあとで書きます。

ユースケースのテストを書く

テストを書く際、データベースやファイルに直接読み書きするようなテストを書いてしまうと
最初は問題ないですが、テストが増えるにつれてテストの実行速度がどんどん落ちてしまいます。

そのため、テストを書く際はデータベースやファイルアクセスが発生しないようメモリ内で良い感じのテストを書きます。

InMemoryUserCsvExport

app/Infrastructure/Adapter/InMemoryUserCsvExport.php
<?php declare(strict_types=1);

namespace App\Infrastructure\Adapter;

use App\Infrastructure\Port\Export;

final class InMemoryUserCsvExport implements Export
{
    /**
     * @var string
     */
    public string $file;

    /**
     * @param string $header
     */
    public function prepare(string $header): void
    {
        $this->file = $header;
    }

    /**
     * @param string $row
     */
    public function write(string $row): void
    {
        $this->file .= $row;
    }

    /**
     * @return void
     */
    public function disorganize(): void
    {
    }
}

Exportインターフェースを契約したInMemoryUserCsvExportクラスを実装します。
やってることは簡単で、prepareメソッドで$fileプロパティに文字列を入れてwriteメソッドが呼ばれたらどんどん追記する形です。
実際にファイルアクセスする場合はdisorganizefclose等の処理を入れますが、ファイルアクセスしないので関数だけ定義してます。

InMemoryUserRepository

app/Infrastructure/Adapter/InMemoryUserRepository.php
<?php declare(strict_types=1);

namespace App\Infrastructure\Adapter;

use App\Domain\UserRow;
use App\Infrastructure\Eloquent\User;
use App\Infrastructure\Port\UserRepository;
use Generator;

final class InMemoryUserRepository implements UserRepository
{
    private array $usersAttributes;

    /**
     * @param array $users
     */
    public function __construct(array $users)
    {
        $this->usersAttributes = $users;
    }

    /**
     * @return Generator
     */
    public function findAll(): Generator
    {
        foreach ($this->usersAttributes as $userAttributes) {
            yield $this->makeUserRow(factory(User::class)->make($userAttributes));
        }
    }

    /**
     * @param User $user
     * @return UserRow
     */
    private function makeUserRow(User $user): UserRow
    {
        return new UserRow(
            $user->name,
            $user->email,
            $user->created_at,
            $user->updated_at
        );
    }
}

補足: ジェネレータ

findAllの戻り値の型としてGeneratorオブジェクトを返すと呼び出した側はforeachを使って順に呼び出すことができます。
returnではなくyieldを指定します。
yieldUserRowのインスタンスを返してます。

// UserCsvExportUseCase で findAll を foreach でループ処理できます。
foreach ($this->repository->findAll() as $row) {
    $this->export->write($row->toCsv());
}

ジェネレータのメリットはforeachでループ処理するために巨大な配列を持つ必要がなく1件処理が終わったらメモリを解放して次の処理を実行してくれるので、バッチ処理等のメモリをたくさん使いそうな場合に効果を発揮します。

UserCsvExportUseCaseTest

先ほど作成したInMemoryUserRepositoryInMemoryUserCsvExportを使ってテストコードを書きます。

tests/Unit/UserCsvExportUseCaseTest.php
<?php declare(strict_types=1);

namespace Tests\Unit;

use App\Infrastructure\Adapter\InMemoryUserCsvExport;
use App\Infrastructure\Adapter\InMemoryUserRepository;
use App\UseCase\UserCsvExportUseCase;
use Tests\TestCase;

final class UserCsvExportUseCaseTest extends TestCase
{
    /**
     * @param array $users
     * @param string $expectedCsv
     * @dataProvider dataResolve
     */
    public function testResolve(array $users, string $expectedCsv): void
    {
        $repository = new InMemoryUserRepository($users);
        $export = new InMemoryUserCsvExport();
        $useCase = new UserCsvExportUseCase($repository, $export);
        $useCase->handle();

        $this->assertEquals($expectedCsv, $export->file);
    }

    /**
     * @return array
     */
    public function dataResolve(): array
    {
        return [
            '正常3件' => $this->case正常3件(),
            '正常0件' => $this->case正常0件(),
        ];
    }

    /**
     * @return array
     */
    public function case正常3件(): array
    {
        $usersAttributes = [
            ['name' => 'yamada', 'email' => 'yamada@example.com', 'created_at' => '2020-01-01 00:00:00', 'updated_at' => '2020-01-01 00:00:00'],
            ['name' => 'suzuki', 'email' => 'suzuki@example.com', 'created_at' => '2020-01-01 00:00:00', 'updated_at' => '2020-01-01 00:00:00'],
            ['name' => 'tanaka', 'email' => 'tanaka@example.com', 'created_at' => '2020-01-01 00:00:00', 'updated_at' => '2020-01-01 00:00:00'],
        ];

        $expectedCsv = <<< EOT
        名前,メールアドレス,作成日,更新日
        YAMADA,yamada@example.com,2020-01-01 00:00:00,2020-01-01 00:00:00
        SUZUKI,suzuki@example.com,2020-01-01 00:00:00,2020-01-01 00:00:00
        TANAKA,tanaka@example.com,2020-01-01 00:00:00,2020-01-01 00:00:00

        EOT;

        return [
            $usersAttributes,
            $expectedCsv,
        ];
    }

    /**
     * @return array
     */
    public function case正常0件(): array
    {
        $usersAttributes = [];

        $expectedCsv = <<< EOT
        名前,メールアドレス,作成日,更新日

        EOT;

        return [
            $usersAttributes,
            $expectedCsv,
        ];
    }
}

想定している $expectedCsv の値とユースケースを実行して作成された値 $export->file が一致すればokです。

補足: dataProvider

PHPUnitのdataProviderについて補足です。
PHPUnitを実行する際に --debug オプションを付けると詳細ログが見れます。
dataProvider で作った引数もログに出てくるので分かりやすくなります。

データプロバイダ | phpunit.readthedocs.io

$ ./vendor/bin/phpunit --debug
PHPUnit 8.5.2 by Sebastian Bergmann and contributors.

Test 'Tests\Unit\ExampleTest::testBasicTest' started
Test 'Tests\Unit\ExampleTest::testBasicTest' ended
Test 'Tests\Unit\UserCsvExportUseCaseTest::testResolve with data set "正常3件" (array(array('yamada', 'yamada@example.com', '2020-01-01 00:00:00', '2020-01-01 00:00:00'), array('suzuki', 'suzuki@example.com', '2020-01-01 00:00:00', '2020-01-01 00:00:00'), array('tanaka', 'tanaka@example.com', '2020-01-01 00:00:00', '2020-01-01 00:00:00')), '名前,メールアドレス,作成日,更新日\nYAMADA,ya...0:00\n')' started
Test 'Tests\Unit\UserCsvExportUseCaseTest::testResolve with data set "正常3件" (array(array('yamada', 'yamada@example.com', '2020-01-01 00:00:00', '2020-01-01 00:00:00'), array('suzuki', 'suzuki@example.com', '2020-01-01 00:00:00', '2020-01-01 00:00:00'), array('tanaka', 'tanaka@example.com', '2020-01-01 00:00:00', '2020-01-01 00:00:00')), '名前,メールアドレス,作成日,更新日\nYAMADA,ya...0:00\n')' ended
Test 'Tests\Unit\UserCsvExportUseCaseTest::testResolve with data set "正常0件" (array(), '名前,メールアドレス,作成日,更新日\n')' started
Test 'Tests\Unit\UserCsvExportUseCaseTest::testResolve with data set "正常0件" (array(), '名前,メールアドレス,作成日,更新日\n')' ended
Test 'Tests\Feature\ExampleTest::testBasicTest' started
Test 'Tests\Feature\ExampleTest::testBasicTest' ended


Time: 3.04 seconds, Memory: 20.00 MB

OK (4 tests, 4 assertions)

CSVの出力処理を実装する

テストコードが書けたところで、実際にデータベースから取得してCSVファイルを出力する処理を実装します。

DbUserRepository

app/Infrastructure/Adapter/DbUserRepository.php
<?php declare(strict_types=1);

namespace App\Infrastructure\Adapter;

use App\Domain\UserRow;
use App\Infrastructure\Eloquent\User;
use App\Infrastructure\Port\UserRepository;
use Generator;

final class DbUserRepository implements UserRepository
{
    /**
     * @return Generator
     */
    public function findAll(): Generator
    {
        /** @var User $user */
        foreach (User::query()->cursor() as $user) {
            yield new UserRow(
                $user->name,
                $user->email,
                $user->created_at,
                $user->updated_at
            );
        }
    }
}

UserRepositoryを契約したDbUserRepositoryクラスです。

補足: User::query()->cursor()

cursor() を使うとPDOStatement::fetch
結果セットから1行ずつ取得できます。 cursor() の返り値もジェネレータオブジェクトになります。

User::query()->cursor() ではなく User::all() も動作するかと思いますが、一度に大量のデータを取得するのでデータ件数によってはメモリオーバーになってしまう懸念があります。

FileUserCsvExport

app/Infrastructure/Adapter/FileUserCsvExport.php
<?php declare(strict_types=1);

namespace App\Infrastructure\Adapter;

use App\Infrastructure\Port\Export;

final class FileUserCsvExport implements Export
{
    /**
     * @var string
     */
    private string $streamFilePath;

    /**
     * @var resource
     */
    private $handle;

    /**
     * @param string $header
     * @return void
     */
    public function prepare(string $header): void
    {
        $this->streamFilePath = $this->makeStreamFile();
        $this->handle = fopen($this->streamFilePath, 'wb+');
        $this->write($header);
    }

    /**
     * @param string $row
     * @return void
     */
    public function write(string $row): void
    {
        fwrite($this->handle, $row);
    }

    /**
     * @return void
     */
    public function disorganize(): void
    {
        fclose($this->handle);

        // 後処理 配置したい場所へコピーする等
        dump(file_get_contents($this->streamFilePath));

        unlink($this->streamFilePath);
    }

    /**
     * @return string
     */
    private function makeStreamFile(): string
    {
        return tempnam(sys_get_temp_dir(), config('app.name'));
    }
}

Exportを契約したFileUserCsvExportクラスです。

ExportUserCommand

app/Console/Commands/ExportUserCommand.php
<?php declare(strict_types=1);

namespace App\Console\Commands;

use App\UseCase\UserCsvExportUseCase;
use Illuminate\Console\Command;

class ExportUserCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'export:user';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'export user data.';

    /**
     * @var UserCsvExportUseCase
     */
    private UserCsvExportUseCase $useCase;

    /**
     * ExportUserCommand constructor.
     * @param UserCsvExportUseCase $useCase
     */
    public function __construct(UserCsvExportUseCase $useCase)
    {
        parent::__construct();

        $this->useCase = $useCase;
    }

    /**
     * @return void
     */
    public function handle(): void
    {
        $this->useCase->handle();
    }
}

LaravelにはArtisanコンソールというコマンドラインインターフェイスが用意されてます。
コマンドクラスを作るだけで簡単に自作コマンドを追加できます。

ExportUserCommandでやってることは、UserCsvExportUseCaseのインスタンスを受け取って、handleメソッドを呼び出すだけです。

$ php artisan export:user

上記のコマンドが追加されます。

依存性の注入(DI)

app/Providers/AppServiceProvider.php
<?php declare(strict_types=1);

namespace App\Providers;

use App\Infrastructure\Adapter\DbUserRepository;
use App\Infrastructure\Adapter\FileUserCsvExport;
use App\UseCase\UserCsvExportUseCase;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind(UserCsvExportUseCase::class, function ($app) {
            return new UserCsvExportUseCase(new DbUserRepository(), new FileUserCsvExport());
        });
    }

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

UserCsvExportUseCaseクラスをサービスコンテナに登録します。
ここで登録しているので、ExportUserCommandはコンストラクタインジェクションでUserCsvExportUseCaseクラスのインスタンスを受け取れます。

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

【PHPUnit】Test code or tested code did not (only) close its own output buffers【laravel】

概要

laravelでHTTPテストを行うとリスキーなテストという判定を受けることがある

黄色で言われるのでドキッとする
Test code or tested code did not (only) close its own output buffers

OK, but incomplete, skipped, or risky tests!
Tests: 12, Assertions: 18, Risky: 1.

環境

laravel 6.7.0
PHPUnit 8.5.0

原因

@section ディレクティブに文字列以外が入っているため。
第二引数には文字列以外を入れるとエラーになる。

XXX.blade.php
@section('title', $title)

例えばこのように動的プロパティで求めた値を入れていて、 null が入ってしまっていたりする。

XXX.blade.php
@section('name', $user->name)

解決

@endsectionで囲む

XXX.blade.php
@section('name')
    {{ $user->name}}
@endsection

記述が長くなるので少し嫌ですね。

空文字にしてしまう

XXX.blade.php
@section('name', $user->name ?? '')

こちらの方がスッキリします。

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

laravel migration カラム名変更(初心者向け)

phpについての初投稿になります!

リファレンスにも書いてありますが、つい忘れてしまうのでメモ程度に紹介します

laravel 6.x系です
osはmacです

カラム名を変更、削除するには

ターミナルで

$ composer require doctrine/dbal

↑のコマンドを叩き、composerで"doctrine/dbal"

をインストールしましょう!(リファレンスに書いてあるので臆せずインストールしましょう!)

laravelはデフォルトではカラム名の変更や削除はできません!(なんでやねん!まぁなんか理由があるのでしょう)


で、ターミナルで

$ php artisan make:migration rename_変更前のカラム名_to_変更後のカラム名_on_テーブル名_table --table=テーブル名

とコマンドを叩きます(オプションの"--table=テーブル名"により、migtarionファイル作成時にテーブル名が追加され作成されます。また、migrationファイルの名前は正直適当でいいと思いますが、ファイル名をみて何の内容のmigrationファイルが実行されたかわかりやすい名前をつけた方が後々楽になると思います。)

〇〇_rename_変更前のカラム名_to_変更後のカラム名_on_テーブル名_table.php
<?php

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

class RenameNightSpandingToNightSpendingOnCostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('costs', function (Blueprint $table) {
            $table->renameColumn('night_spanding', 'night_spending');//<-記述
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('costs', function (Blueprint $table) {
            $table->renameColumn('night_spending', 'night_spanding');//<-記述
        });
    }
}

こんな感じでマイグレーションファイルを作成します

upは

$ php artisan migrate

↑で実行される内容

downは

$ php artisan migrate:rollback

↑などで実行される内容を記述します
なので、カラム名の変更や、型の変更などは、基本的にはupで書いた内容の逆を記述すればいいと思います。
(php artisan migrate:refreshなども全てロールバックしてmigrateし直すコマンドなどあるので、downはちゃんと書いておかないとエラーの原因になります!)

で、

$ php artisan migrate

でカラム名を変更することができます!

カラム削除する場合でも、"doctrine/dbal"のインストールが必要みたいですので、個人的にはlaravelプロジェクトを立ち上げたらすぐにインストールしておいた方がいいのかな?と感じてます。

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