カテゴリーアーカイブ Laravel

takahashi 著者:takahashi

LaravelのREST APIの話

素で書こうとするとそこそこ苦労するREST API周りの実装ですが、Laravelにはこのあたりに必要な機能が始めから入っているためかなり楽に作りこむことができます。

Laravelのコントローラーで文字列、もしくは配列をreturnするとjson_encode()しなくても勝手にjsonに変換してくれたり、ステータスコードを付加するのがかなり簡単だったりとほぼそのまま使える状態になっています。

また、URLとコントローラーのメソッドを対応させるルーティングについても、デフォルトでAPI用のルーティングファイル(routes/api.php)が別で用意されており、APIのルーティングだけ、Web部分のルーティングと分離して書くことができます。

api.phpに書かれたルートにアクセスするには

example.com/api/...

のように、urlにapiというディレクトリ名を挟むだけです。

ここまでは非常によくできていて、APIを作る開発者にとっては非常にありがたいのですが、個人的に難点を感じたのがREST API使用時の認証の部分。

簡易的な認証であれば、DBのユーザー情報テーブルに”api_token”というカラムを追加して、このカラムに対してユーザー登録時にapiトークンを自動生成するようにしておけばLaravelのデフォルトのメソッドを使用してapiトークンによる認証を行わせることができるようになっているのですが、このapi_tokenにあたるカラム名をカスタマイズしたりだとか、Twitterのように、トークン+セキュリティトークンの2つで認証させるようにカスタマイズしたい場合などにカスタマイズが簡単にできない点があり、独自に実装してしまった方が早い…という側面があります。

このあたりがもっと改良されてこればより実用的になると思うのですが…

かゆいところに手が届かない感があってつらいところです。

[Laravel]レスポンスをJSON形式で返す方法2つ – Qiita

基本的なルーティング – Laravel 日本語ドキュメント

API認証 – Laravel 日本語ドキュメント

  • この記事いいね! (0)
著者:杉浦

.envファイルの読み方色々

 .envは環境変数を定義するファイルとしてよく使われているファイル名です。この記事はいくつかの言語、フレームワークにおける.envの読み方の紹介をします。

今どきの環境は.envがあることを前提として作られている機能があります。例えば、PHPのフレームワークであるLaravelやJavaScript実行環境であるNode.jsです。

// Laravel
$version = env('VERSION');
// Node.js
const version = process.env.VERSION;

 dockerに至っては暗黙の裡に取り込んでしまいます

# laradockから引用
# .env
PHP_VERSION=7.2

# docker-compose.yml
### PHP Worker ############################################
    php-worker:
      build:
        context: ./php-worker
        args:
          - PHP_VERSION=${PHP_VERSION}

 AltJSをコンパイルする環境ならば特定の接頭辞の環境変数を定数をあてはめる形で実装されていることが多いです。

// Vue用開発ツールVue CLIならばVUE_APP_ほげほげ
const version = process.env.VUE_APP_VERSION
// Laravel用webpack拡張のLaravel MixならばMIX_ほげほげ
const version = process.env.MIX_VERSION;

 Pythonの様にdotenvというパッケージがある場合もあります(Rubyもそうらしい)。

dotenv_path = join(dirname(__file__), '.env') # .envへのパスを定義
load_dotenv(dotenv_path) # 環境変数に.env中の値を追加
version = environ.get("VERSION") # 環境変数から値を取得

 ちょっと手間ですがbashの中で読むこともできます。

script_dir=$(cd "$(dirname "${BASH_SOURCE:-$0}")" || exit; pwd)
cd "${script_dir}" || exit

eval "$(cat .env <(echo) <(declare -x))"
  • この記事いいね! (0)
著者:杉浦

【Laravel】6.xからの新しいデバッグ用関数ddd

 Laravelにはdump(), dd()の様なデバッグ用にリッチな画面出力関数が用意されています。dddはそれに加わる新たなデバッグ用関数です。
ヘルパ 6.0 Laravel#dd
Laravel Ignition Introduces the ddd() Helper – Laravel News
 Laravel6.xではエラーページ用の機能がLaravel/Ignitionになっています。dddはこのIgnitionに付属する機能です。Ignitionのエラーページは画像の様な白を基調とした画面で今までの黒画面とはガラッと変わりました。タブ毎にデータを分けることで肥大化したデバッグ用情報を読みやすくしています。

Ignition Is the New Error Page for Laravel – Laravel Newsから引用
 ddd関数は画面出力をこのエラーページ同様にリッチなものにします。

 ddd関数を使うことでリクエスト、ログイン情報、発行されたSQLなども合わせて見ることができます。

  • この記事いいね! (1)
takahashi 著者:takahashi

LaravelでIntervention/image で圧縮した画像をファイルに保存せずにバイナリで取得する方法

Laravelで画像ファイルの変換処理を行いたい場合、Intervention/image というComposerパッケージを使用すると非常に楽です。

詳しいセットアップ方法は、下記のサイトを参考にしてみてください。

さて、このIntervention/imageですが、使い方を解説している多くのサイトで、下記のように変換した画像を Intervention/image のsave()メソッドを使った保存方法を紹介しているところが多いです。

...
$image = Image::make(画像ファイルのパス); //画像をパスから取得
$image->resize(500, 500);

...

しかし今回僕が行いたかった処理は”元ファイルを変換して保存する”のではなく、”元画像を圧縮してサムネイル画像を作成してRestAPI経由でbase64でクライアントに投げる”ということをしたかったため、ファイルの保存は不要でした。

そこで色々調べたところ、 Intervention/image にファイルへ保存ではなく、バイナリストリームをそのまま吐き出すメソッドが存在していることを知りました。

Cannot save image intervention image. NotWritableException in Image.php line 138 -StackOverflow

$img = Image::make(画像ファイルのパス)->resize(500,500)->stream('jpg', 50)

このようにすると、$img変数に変換後の写真バイナリデータが吐き出されるため、あとはこのバイナリデータを

$img_base64 = base64_encode ( $img  );

のようにすると、base64に変換されたデータが入手できます。

これで、目的の動作をさせることがきました。

なお、Laravelでは

$image->save(画像の保存先パス);

すると権限の問題でエラーになってしまいます。

 NotWritableException in Hoge.php line 138: Can't write image data to path path/to/image.jpg 

Laravelで変換後のファイルを保存したい場合は、上の方法でバイナリデータを取り出した後、

Storage::put('保存先ファイルパス', $img);

のようにすれば、 Laravel上でも Intervention/image を使用して データを保存することができます。

  • この記事いいね! (0)
takahashi 著者:takahashi

Laravel Eloquent の”fillable”の設定がめんどくさいときは”guarded”が便利

PHPフレームワークのLaravelには、DBと接続してデータをやり取りする仕組みとして”Eloquent”が実装されています。

Eloquentを使用すると、 基本的なDB操作であれば始めからメソッドが用意されているため、Modelに自分でメソッドを書かなくても簡単にデータが取得、保存ができるためとても便利です。

ただし、上記の備え付けのメソッドを使用する際は、DBの設定(テーブル名や主キーなど)をModelに記述する必要があります。

この設定の一つに、どのテーブルへのアクセスを許可するか、という指定があります。

この指定を行っておくことで、本来データを入れてはいけないテーブルに対して、誤ってModelからアクセスしてしまうことを防ぐことができます。

必須の項目なので、何らかの指定は必要になります。

個人的によくサイトで見かける方法で、Model内のクラスのブロックの頭に$fillableという変数を置くものがあります。

class Hoge extends Model {
    ....
    protected $fillable = ['user_name','mail_address','password'];
    ....

fillableでは何を指定すればいいのかというと、”Laravel側から触ってもよいカラム”を指定します。

これでも動作としては問題がないのですが、カラム数の多いテーブルなどが多かったりすると、正直めんどくさくなってきます…

実はもう一つ、操作可能なカラムを指定する方法があります。

$guarded 変数を用いる方法です。

class Hoge extends Model {
    ....
    protected $guarded = ['id'];
    ....

違いとして、fillableが所謂ホワイトリスト方式なのに対し、 guarded はブラックリスト方式です。

つまり、 guarded を指定した場合は”アクセスしてはいけない”カラムだけ記述すればOKです。

アクセスしてはいけないカラムが、アクセス可能なカラムよりも数が少ない場合はこちらの方が楽ですね。

LaravelのModelでは、fillableかguardedのいずれかが指定されていればOKとなっていますので、うまく使い分けて記述量を減らしていきましょう。

  • この記事いいね! (0)
著者:杉浦

【PHP】提案中のObject Initializerとオブジェクト初期化の小技

 PHPでObject Initializerという記法が現在提案されています。
 PHP: rfc:object-initializer
 [RFC] Object Initializer – Externals
 [VOTE] Object Initializer – Externals
 その名の通り、オブジェクトの初期化に関する記法です。具体的には次の様な記述をします。

<?php
 
class Customer
{
  public $id;
  public $name;
  private DateTimeImmutable $createdAt;
 
  public function __construct()
  {
    $this->createdAt = new DateTimeImmutable("now");
  }
}
 
class Car
{
  public int $yearOfProduction;
  public string $vin;
}

<?php
 
$customer = new Customer {
  id = 123,
  name = "John Doe",
};
 
$car = new Car {
  yearOfProduction = 2019,
  vin = "1FTFW1CVXAFD54385",
};
// <a href="https://wiki.php.net/rfc/object-initializer">PHP: rfc:object-initializer</a>から引用

 単なる代入によるpublicプロパティの初期化をコンストラクタ外で行おうとする方法です。これによりコンストラクタのコーディング、プロパティの代入忘れ、引数の順番間違いから解放されます。
 これだけだと良いやり方に見えますが、提案の賛成:反対は2019/10/08 17:30(JST)時点で3:17です。投票に理由をコメントする必要はないのでスレを読み取って想像するしかありませんが、コンストラクタと共存すると怪しかったり、__setを介する直接代入でない部分の扱いだったり、今のPHPでも手間をかければ似たようなことができたりで賛成者は少ないです。

 PHPの既にある仕様で多数のプロパティを初期化する方法は多々あります。フレームワークを利用するとさらに増えます。自分がよく使うのは連想配列を用いたパターンです。

<?php
class Pos {
    public function __construct($attributes)
    {
        $this->lat = $attributes['lat'];
        $this->lng = $attributes['lng'];
        $this->x = $attributes['x'] ?? null;
        $this->y = $attributes['y'] ?? null;
    }
}

new Pos([
    'lat' => 35.658577,
    'lng' => 139.745451
]);

 連想配列にまとめることで引数を抑え、連想配列であるため引数の意味を理解しやすい、というものです。このやり方にはIDEの自動補完がまるで働かないという点で問題がありますが、程よく手軽に使える点と他クラスのarray変換ですぐ結び付けられる点で使用しています。他にもstdClassを使ったパターンやインタフェースとDTO(DataTransfarObject)を使うやり方もあります。後者は大がかりなものを扱う時、重用できます。

<?php
class Pos {
    public function __construct($attributes)
    {
        $this->lat = $attributes->lat;
        $this->lng = $attributes->lng;
    }
}

$args = (object)[
    'lat' => 35.658577,
    'lng' => 139.745451
];
new Pos($args);
interface PosArgsContract {
    public $lat;
    public $lng;
}
class Pos {
    public function __construct(PosArgsContract $attributes)
    {
        $this->lat = $attributes->lat;
        $this->lng = $attributes->lng;
    }
}

 初期化とは少し違いますがLaravelのFormRequestとEloquentをつなぐやり方として次のワンライナーがあります。

public function create(UserCreateRequest $request) {
    $user = User::create($request->validated());
}

 validatedメソッドの返り値はバリデーションを通過したリクエストのボディ全ての連想配列です。リクエストのキーをテーブルのキーと一致させることによって上記の記述でブラウザのリクエストからデータベースへのSQL発行まで一気に繋げられます。しかもバリデーション通過済みなので安全な値です。

  • この記事いいね! (1)
著者:杉浦

【Laravel】EloquentモデルのObserverの活用

 LaravelのEloquentにはObserverという仕組みがあります。ObserverはEloquentのイベント発生によって発火するメソッドの集まりです。
Eloquent:利用の開始 6.0 Laravel#オブザーバ
 Observerは次のようにartisanでボイラープレート的なモノを生成できます。

php artisan make:observer UserObserver --model=User

 以下の様に生成されたクラスをLaravelの起動時処理で登録することによってイベント監視が動作します。

<?php

namespace App\Providers;

use App\Observers\UserObserver;
use App\User;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    //省略
    /**
     * 全アプリケーションサービスの初期起動
     *
     * @return void
     */
    public function boot()
    {
        User::observe(UserObserver::class); // ここでモデル::observe(オブザーバクラス::class)が基本
        Post::observe([PostObserver::class, BBSObserver::class); // 複数でもOK
        foreach (['Post', 'Comment'] as $className) {// 共通のオブザーバを使うならこんな書き方もあり
            /* @var Eloquent $className */
            $className::observe(UpdateTalkedAtObserver::class);
        }
    }
}

 オブザーバの使い時は色々あります。例えば、ある集約全体における更新日時を記録する場合です。
 LaravelのEloquentにはタイムスタンプを自動記録する機能があり、あるレコードを更新した際にそのレコード内の特定のカラム(デフォはupdated_at)に日時を記録してくれます。これだけでも便利なのですが、これだけではシステムの要求を満たせない場合もあります。例えば、あるユーザの情報が複数テーブルに分割されており、いずれかの情報が更新された時にユーザ情報が更新されたとし、その更新日時を記録する必要がある、という場合です。より具体的に次のER図のテーブル構成があるとします。

 ブログへの投稿かコメントを行った時にユーザの最終発言日時を記録する、とします。このような時、次のObserverを作ると要求を満たせます。

class UpdateTalkedAtObserver
{
    public function created(Eloquent $postOrComment) // 宣言した型に依らず、登録したEloquentクラスのインスタンスが渡される
    {
        $postOrComment->user->talked_at = $postOrComment->created_at;
        $postOrComment->user->save();
    }

    public function updated(Eloquent $postOrComment) // 宣言した型に依らず、登録したEloquentクラスのインスタンスが渡される
    {
        $postOrComment->user->talked_at = $postOrComment->updated_at;
        $postOrComment->user->save();
    }
}

 他にもログ取り、検索テーブル用の同期、削除や復元の連鎖など使うと楽になる場面がいくらかあります。また単にモデルのコードが膨れ上がらないための分割としても使えます。

  • この記事いいね! (1)
著者:杉浦

【Laravel】ログファイルのPermission deniedを恒久的に防ぐ

 Laravelに限った話でないですが、あるプログラムを異なるユーザが実行し同じ宛先のログファイルに追記をしようとする場合、書き込み権限を持っておらずPermission deniedで弾かれることがあります。この記事ではLaravelにおけるこのエラーの防ぎ方を書きます。
 前提としてsudo php artisan hogehoge(root以外が触れられるべきでないローカル領域に触れる処理とか)とwebサーバによるpublic/index.php(webページの表示)の二つが走るものとします。この前提の鬼門はrootユーザ製ログファイルにapache等のwebサーバがログを追記しようとする場合です。デフォルトでは次のようなファイルができあがり、webサーバに書き込み権限がないままwebサーバがログを書こうとしてこけます。

-rw-r--r-- 1 root    root          1368 10月  1 17:13 laravel-2019-10-01.log

 いくつか試した対応策で私的に最も有効なのは次です。

// config/logging.php
<?php

use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;

// whoamiコマンドを実行して現在のPHPプロセスの所有者を得る
// whoamiはwhoamiを実行したユーザの有効なuserid(ユーザ名)を表示するコマンド
$__current_process_user = trim(shell_exec('whoami'));
return [

/// 省略 ///

    'channels' => [
        /// 省略 ///
        'daily' => [
            'driver' => 'daily',
            // whoamiから得た名前別にログファイルを分ける
            'path' => storage_path('logs/laravel-'.$__current_process_user.'.log'),
            'level' => 'debug',
        ],
        /// 省略 ///
    ],
];

 コードのコメントにある通りwhoamiコマンドからプログラムの実行ユーザを得て、名前別にログファイルを分けるやり方です。
 これを用いると次のようにログファイルが分割され、権限による問題が起きなくなります。

-rw-rw-r-- 1 apache   apache     234 10月  1 17:25 laravel-apache-2019-10-01.log
-rw-rw-r-- 1 cplab    cplab     1332 10月  1 17:25 laravel-cplab-2019-10-01.log
-rw-rw-r-- 1 root     root      1368 10月  1 17:13 laravel-root-2019-10-01.log

 ログを時系列順に見たいならcat, sortコマンドがいくらか役に立ちます。

cat laravel-*-2019-10-01.log | sort

とすれば、Laravelログ共通の[2019-10-01 17:19:00] local.DEBUG: を用いて各行が時系列順に結合された結果を見れます。いくらか、というのはスタックトレース等の複数行に渡るメッセージが壊れるからです。残念ながらこれの対策は個別にファイルを見るぐらいしか見つけられていません。

 以下の二つは余談的な他のPermission denied対策です。状況によっては有効かもしれません。
 一つ目はログファイルのデフォルトアクセス権限を変えるやり方です。

// config/logging.php
        'daily' => [
            'driver' => 'daily',
            'path' => storage_path('logs/laravel.log'),
            'level' => 'debug',
            'permission' => 0666,// 権限が設定できる
        ],

 デフォルトのアクセス権限を誰でも読み書き可にすることでroot:rootなログファイルがあってもwebサーバから追記できます。誰でも既存のログを破壊できる点が気になって没にしました。
 二つ目はPHP組み込み関数get_current_userを使うやり方です。

// config/logging.php
        'daily' => [
            'driver' => 'daily',
            'path' => storage_path('logs/laravel-'.get_current_user().'.log'),
            'level' => 'debug',
        ],

 一見shell_exec(‘whoami’)と同様の結果を得られそうな関数名ですが異なる動作をします。get_current_userは現在のPHPスクリプトの所有者の名前を返します。どういうことかというと

-rw-r--r--    1 cplab     cplab       1686  9月 19 12:02 artisan

というファイルがあって

sudo php artisan hoge

とするとget_current_userはcplabを返します。しかしプロセス所有者はrootなので生成されるログファイルの所有者はrootです。artisanとwebページを分ける意味では適していますが実行ユーザ基準でログを分離するwhoamiの方がより安全と考えて没になりました。

  • この記事いいね! (1)
著者:杉浦

【Laravel】6.0.4リリースとLaravel製APIテストの拡張

 Laravel6.0.4がリリースされました。
 Releases · laravel/framework
 機能の追加は次の通りです。

Added

  • Added TestResponse::assertJsonPath() method (#29957)
  • Added hasMacro \ getGlobalMacro \ hasGlobalMacro methods to Eloquent Builder (#30008)
  • Added Illuminate\Database\Eloquent\Relations\BelongsToMany::getPivotColumns() method (#30049)
  • Added ScheduledTaskFinished \ ScheduledTaskStarting events to signal when scheduled task runs (#29888)
  • Allowing adding command arguments and options with InputArgument \ InputOption objects (#29987)


 自分にとって特別な追加はTestResponse::assertJsonPath()です。これのおかげでAPIのミドルウェア込みテスト(疑似的なリクエストを投げて、ルーターとコントローラを通して、レスポンスを返してもらって、レスポンスをチェック)をより簡潔に必要な分だけ作れる様になりました。
 LaravelのTestCaseではAPI用にJSONレスポンス用のassertが用意されています。もともと用意されていたのは大規模にならざるを得ないメソッドや素朴すぎるメソッドです。例えば、assertExactJsonがあります。これは完全一致のJSONを期待するassertで次のように使います。

$this->getJson(route('api.post.show', [$post->id]))
	->assertExactJson([
	    'title' => 'My blog post',
	    'body' => 'Lorem ipsum ...',
	    'tags' => [],
	    'comments' => [
	        [
	            'body' => 'First!',
	            'user_id' => 42,
	            'user' => [
	                'id' => 42,
	                'username' => 'ecrmnn',
	            ],
	        ],
	        [
	            'body' => 'This is my comment',
	            'user_id' => 731,
	            'user' => [
	                'id' => 731,
	                'username' => 'ventrec',
	            ],
	        ],
	    ],
	]);

 これはこれで便利なのですが、細かい単位でテストし難いです。また、同じコントローラメソッドを繰り返しテストすると微妙な差分の期待するJSON定義が膨らみます。もっと言えば、何をテストしたいのかぱっと見分かりません。Laravel6.0.4で追加されたassertJsonPathはこれを解決します。次のコードの様に、JSONの特定のパスについてのみassertをすることで簡潔に必要な分だけ検査をできます。先述のassertExactJsonをassertJsonPathに書き直すと次のようになります。

$this->getJson(route('api.post.show', [$post->id]))
    ->assertJsonPath('tags', [])
    ->assertJsonPath('comments.0.user.username', 'ecrmnn')
    ->assertJsonPath('comments.*.body', [
        'First!',
        'This is my comment',
    ]);

 ちなみにJSONレスポンスを期待する疑似リクエストを投げる時のコードは次のようになります。get, postのみならずput等HTTP2.0で定義されるメソッドにも対応しています。

class HogeControllerTest extends TestCase
{
    public function HogeTest(){
        $this->getJson(route('hoge'));
        $this->postJson(route('hoge'), ['id' => 0, 'password' => '']);
        $this->deleteJson(route('hoge'), ['id' => 0]);
    }
}
  • この記事いいね! (1)
著者:杉浦

【Laravel】public static function hogehoge()とpublic function scopeHogehoge()の使い分け

 LaravelのEloquentにはクエリビルダでチェーンを組むための命名規則に従ったメソッド作成方法があります。scopeHogehoge()とするとModel::hogehoge()とするとクエリビルダが走ります。この状態のまま次のようにチェーンを組めるのが利点です。

User::hogehoge()->orderBy('fuga')->limit(30);

 検索などでクエリの一部を使いまわしたい時などとても助かります。助かるのですが、この手法はモデルにも継承元であるEloquentにも記述されていないメソッドを呼び出しており、IDEのヘルパが効かず、警告も出力されます。次のようにコメントを記述することで対策できますが、コメントを経由して改めてscopeHogehoge()を検索する必要があります。

/**
 * @method static Builder|User newModelQuery()
 * @method static Builder|User newQuery()
 * @method static Builder|User query()
 * @mixin Eloquent
 *
 * @method static Builder|User Hogehoge()
 */
class User extends Model
{
    public function scopeHogehoge($query)
    {
        return $query->where('なんやかんや');
    }
}

 クエリが増えるにつれどんどんコメント→実際の記述と追うのが面倒になりだします。自分の知る使えそうな小技は二つです。一つはコメントの長大化の弊害がありますが次のようにコメントすることです。@seeはによってIDEはジャンプができる様になります。PhpStormならCtrl+Bです。

/**
 * @method static Builder|User newModelQuery()
 * @method static Builder|User newQuery()
 * @method static Builder|User query()
 * @mixin Eloquent
 *
 * @method static Builder|User Hogehoge()
 * @see   \App\Models\Eloquents\User::scopeHogehoge
 */
class User extends Model
{
    public function scopeHogehoge($query)
    {
        return $query->where('なんやかんや');
    }
}

 もう一つはstaticなクエリビルドメソッドを作ることです。Model::からクエリビルドする時はEloquentクラス中の次のメソッドが呼ばれています。これを使うことで任意のクエリをグローバルスコープ等欠けさせることなく使えます。staticなのでクエリビルド用の別クラスを用意することも簡単です。

class Eloquent
{
...
    /**
     * Begin querying the model.
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public static function query()
    {
        return (new static)->newQuery();
    }
    /**
     * Get a new query builder for the model's table.
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function newQuery()
    {
        return $this->registerGlobalScopes($this->newQueryWithoutScopes());
    }
...
}

 次が例です。再利用するあてのないメソッドならあと腐れなくstaticにできます。またこのstaticメソッドでビルダーを返せば、後にクエリビルド用のメソッドチェーンを続けることもできます。

public static function hogehoge($name)
{
    return self::query()->whereName($name)->first();
}
  • この記事いいね! (1)