tanaka's Programming Memo

プログラミングについてのメモ。

Arduinoの電源関連メモ(追記あり)

(解決策を3回の移動平均に変更しました。2016/10/6)

今回の誤動作は、サーボモーターとは無関係でした。原因は、電源電圧の不安定さ。常に5Vが流れてくる訳ではなく、ちょくちょく、4.5Vとかのことがあります。センサーに利用しているフォトトランジスタは、入力電圧によって戻ってくる値が変わるので、この電圧の変化によって不安定な結果を返していました。

Arduinoで、電圧の変化とフォトトランジスタの値の変化を確認するスケッチを作成して確認したところ、次のような状況でした。

  • 5Vを下回る少し前に、フォトトランジスタの値が急激に下がる
  • 5Vを下回ったタイミングで、フォトトランジスタの値が大幅に大きくなる
  • 5Vに戻ったあと、やや不安定だが、ほぼ平時に戻る

以上から、移動平均を行うことで、極端な値の変化を抑制することができそうでした。そこで、3回の移動平均を求めて、その値で回転の判定を行ったところ、2日間連続で動かしても、誤動作は発生しませんでした。

3個分のint型の配列を用意して、インデックスを表すint型の変数も用意します。インデックスを変化させて、一番古い値の場所に、新しいセンサー値を記録して、配列の平均を求めれば、移動平均が求まります。

    • -

モーターの電源は他からとった方が安全なので、以下はやった方がより良いので、情報は残しておきます。

サーボモーター(SG90)をArduinoの5Vに接続すると、時々電力不足になるようで、センサーが誤動作することがあります。プログラム側でセンサーが0の値や突発的な変化を無視することで殆ど誤動作はなくせましたが、完全ではありませんでした。より安定して動かすには、サーボモーターの電源はArduino以外から取った方がよさそうです。

SG90用には以下のものを検討しています。動作電圧が4.8-5.0Vなので、USBの電源がちょうどよいです。普通の5Vのアダプターでもいいのですが、いざという時、USBならPCからも電源もらえるかもなので。

以上2つで、ブレッドボードに5Vの電源を用意して、サーボモーターを接続すればよさそうです。


Arduinoは消費電力が少ないので、電池駆動でも長持ちしそうな気がします。充電池2つを以下で昇圧すれば動きそうです。

ちなみに、今はArduinoの電源用に以下のACアダプターを使っています。


ACアダプターの電源を、Arduino用とモーター用で分ければ1つのACアダプターで済みそうな気もしていますが、今のところその知識がないので、一先ず上記で一旦やっつけようと思います。

多摩市で電子工作する時に重宝するお店

電子工作の材料を集める時に助けてもらったり、これから役立ちそうなお店情報です。電子工作は初心者のため、開拓している途中です。良い情報をお持ちの方は教えていただければ幸いです。


あとは近所の百円ショップやAmazonさんも外せません。

フォトトランジスタ 7502L を Arduino で使う(大間違いあり)

(モーターのノイズについて、補足しました)
(抵抗の考え方を完全に間違えてました。直しました。2016/10/1)

akizukidenshi.com

データシートを見ると、標準値として「33μA 標準 条件:白色LED, 100Lux 」とあります。100Luxの白色LEDがどのぐらい明るいか分かりませんが、とりあえずこの33μAという値を基準に考えれば良さそうです。

Arduinoの回路に流して良いのは20mA以下ですが、仕様書の絶対最大定格を見ると10mAなので、どんなに明るくても電流の心配はなさそうです。

利用する抵抗値を検討します。根拠はありませんが、標準値の3倍ぐらいの値がアナログ入力の半分の512になるなら安心ではないだろうか、ということで、100μAの時に、2.5Vを発生する抵抗値を計算してみます。

「抵抗値(Ω) = 電圧(V) / 電流(A)」ですから、

2.5(V) / 0.0001(A) = 25000Ω

つまり、25KΩ程度の抵抗が良さそうです。

手元にあった中で一番近い10KΩを使って室内で試したところ、CdSセルよりは小さい値でしたが、最大で70程度の値が得られました。脇にLEDを光源としておくことで、フォトトランジスタの前に白いものを置くと100程度の数値が得られました。実際に使う場所の明るさにもよりますが、この環境であれば、事前に計算した25KΩから、50KΩ程度の抵抗が良さそうです。

こういう時に、可変抵抗器があると便利そうです。では、今回試した配線とスケッチを以下に示します。

以上、全部誤りです。電圧はコレクターにかかっているものなので、上記の計算は全く意味がありませんでした。後日、改めて整理します。

フォトトランジスタ配線

LED用配線

  • デジタルピン13番 - LED(+)
  • 1KΩ抵抗 - LED(-)
  • 1KΩ抵抗 - グランド

モーターのノイズ対策

上記のままだと、時々モーターが変な音を発生して、Arduinoのランプが不意についたりします。これはモーターのノイズのようです。Netduino: サーボモーターをコントロールしよう - Build Insiderを参考にして、5VとGNDの間に、0.1μFのセラミックコンデンサーを入れた方が良さそうです。(モーターやLEDの誤動作は、電源からの電圧の変化によって、フォトトランジスタの値が変化してしまうのが原因でした。電圧をアナログ入力で監視して、1000以下になる場合はセンサーの値を無視して対応しました。2016/9/28追記)

スケッチ

const int SENSOR = 0;
int val = 0;
const int LED = 13;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  pinMode(LED, OUTPUT);
  digitalWrite(LED, HIGH);
}

void loop() {
  // put your main code here, to run repeatedly:
  val = analogRead(SENSOR);
  Serial.println(val);
  delay(50);
}

マイクロサーボ SG-90 を Arduino で制御する

akizukidenshi.com

akizukidenshi.com

以上を接続して、制御してみます。

駆動にはArduinoの標準ライブラリであるServoを利用します。

配線

シンプルなので、直接Arduinoのpinにサーボモーターのコードをさします。

動作の決定

Servoライブラリでは、以下の2つの関数でサーボモーターを制御できます。

  • write(0〜180)
    • サーボの角度が指示の度数になるように回転させる
    • 連続回転タイプのサーボの場合は、0で振るスピード回転。180で逆方向に振るスピード回転。90で静止
    • SG-90は連続回転タイプではないので、角度のしていになる
  • writeMicroseconds(約1000〜2000)
    • マイクロ秒単位で、角度を指定
    • 標準的なサーボモーターでは、1000で反時計回りいっぱい、1500が中央,2000が時計回りいっぱい。ただし、この範囲外が使えるものもある

writeの0と180の方向や、writeMicrosecondsの意味がよくわからないので、その辺りを試せるスケッチを作成しましょう。以下のステップで動かすことにします。

  • 開始したら、write(0)を実行
  • 2秒待つ
  • write(180)を実行
  • 2秒待つ
  • write(90)を実行
  • 2秒待つ
  • writeMicroseconds(1000)を実行
  • 2秒待つ
  • writeMicroseconds(2000)を実行
  • 2秒待つ
  • writeMicroseconds(1500)を実行
  • 4秒待つ

スケッチの作成

Arduinoアプリを起動して、以下のコードを書きます。

#include <Servo.h>

// サーボのピン番号
const int SERVO_PIN = 9;
// サーボのインスタンス
Servo servo;

// INITがtrueの時は、モーターの位置を0にするだけ
const bool INIT = true;

void setup() {
  // put your setup code here, to run once:
  servo.attach(SERVO_PIN);
  servo.write(0);
}

void loop() {
  // put your main code here, to run repeatedly:
  if (INIT) return;

  servo.write(0);
  delay(2000);
  servo.write(180);
  delay(2000);
  servo.write(90);
  delay(2000);
  servo.writeMicroseconds(1000);
  delay(2000);
  servo.writeMicroseconds(2000);
  delay(2000);
  servo.writeMicroseconds(1500);
  delay(4000);
}

実行

上記のスケッチをビルドして、プログラムを書き込むと、サーボモーターが0の位置に移動して停止します。この状態で、プロペラをわかりやすい位置になるように取り付けましょう。

プロペラが取り付けられたら、以下のようにスケッチ内のINITの宣言をfalseに変更して、ビルドしてプログラムを書き込みます。

const bool INIT = false;

これで、テスト内容が動きます。これにより、SG-90の挙動として以下のことが把握できました。

  • write(0)は、時計回りで端まで動く
  • write(180)は、反時計回りの端まで動く
  • write(90)は、上記の中央に動く
  • writeMicroseconds(1000)は、時計回りで45度ぐらいのところに動く
  • writeMicroseconds(2000)は、反時計回りで135度ぐらいのところに動く
  • writeMicroseconds(1500)は、write(90)と同じ場所になる


以上で、テスト完了です。回転速度が変わるわけでもなさそうで、writeMicroseconds()の使い所がよく分かりませんが、まあそれはいいでしょうということで。write()で進めます。

Arduinoの勉強メモ

息をマイクに吹きかけると、ナースコールボタンを押せるようにする機械が作りたく、Arduinoを勉強することにしました。

手が自由に使えない患者向けにそのような医療用機器があったのですが、設備がない病院もあります。息を吹きかけることでサーボモーターを動かして、ボタンを押すような仕組みが作れないかと考えています。

Raspberry Piの方が性能が高くて音声認識も可能なのですが、今回の用途であればArduinoでも問題がなさそうなので、安さを優先してArduinoにしました。

勉強材料

教材として、以下を購入しました。

Arduinoをはじめよう 第3版 (Make:PROJECTS)

Arduinoをはじめよう 第3版 (Make:PROJECTS)

Arduinoをはじめようキット

Arduinoをはじめようキット

開発は、WindowsmacLinuxのどれでも大丈夫なので、mac book airにしました。

IDEのインストール

  • Arduino - Software から、自分の環境に合わせたソフトウェアをクリック
  • 資金提供をすることが可能。まず使ってみたい場合は、[JUST DOWNLOAD]をクリックして、ソフトウェアを実行する
  • 実行したら、[Arduino]という名前でアプリが解凍されるので、アプリケーションフォルダーなどに移動。zipファイルは削除して良い
  • macとUSBケーブルで接続すると、ONのLEDが点灯、LというオレンジのLEDが点滅する
  • インストールした[Arduino]アプリを起動
  • [ツール]メニューから[シリアルポート]を選択して、[/dev/cc.usbmodem・・・]を選択
  • [ツール]メニューから[ボード]を選択して、[Arduino/Genuino Uno]を選択

最初のスケッチ

スケッチとは、Arduinoを実行するためのプログラムのことです。

LEDを光らせます。点滅しているLのLEDは、簡単に実験できるように最初から乗っている13番ピンに接続されていることになっているLEDです。

LEDの短い足(カソード)をGNDに、長い足(アノード)を13番に指すと、Lと同じ振る舞いをさせることができます。

ポイント

  • 開発言語は、C言語ベースなので、C言語JavaC#などに馴染みがあればすぐに使える
  • pinMode(ピン番号, ) で、ピンの入出力を設定
  • digitalWrite(ピン番号, ) で、ピンの出力をONかOFFに切り替え
  • HIGHは1、LOWは0
  • Example4-2 は、以下のように整理できる
const int LED = 13;
const int BUTTON = 7;

void setup() {
  pinMode(LED, OUTPUT);
  pinMode(BUTTON, INPUT);
}

void loop() {
  digitalWrite(LED, digitalRead(BUTTON));
}
  • 物理ボタンは、仕組みの関係でボタンを押した直後にONとOFFが繰り返される場合がある。これをバウンシングという。delay(10)などで遅延させることで、この症状を軽減できる
    • 自分の環境では、delay(100)を入れても時々失敗する。そういうものなのか、自分のボタンの不良なのか、今の所判断つかず。まあ、そんなものということで進める

高度な入力と出力

  • ティルトスイッチ(傾きセンサー)は、「チルト」で探した方が見つかる
  • パルス幅変調(PWM)を使って、LEDの明るさ調整や、モーターの速度調整ができる
    • ONとOFFを細かく繰り返して調整する方法
    • デジタルピンのチルダが描かれているもの(3,5,6,9,10,11)は、ハードウェアのPWMが対応していて、設定をするとloopが停止していても作動する
    • analogWrite(ピン番号, 0~255) で設定。0が0%。255が100%
  • millis()で、Arduinoの時計ボードがリセットされてからの経過ミリ秒を取得できる。型は unsigned long
  • ArduinoRaspberry Piに比べて圧倒的に低速とはいえ、1秒間に1,600万ステップ(16MHzなので)も実行できる。delayやmillis()を使わないとすごい速さでループを繰り返す。単純なセンサーやアクチュエーター相手であれば、十分な速度を持っている
  • Arduino基盤に流す電流は20mA以下にする

アナログ入力

  • アナログピンに刺して、 analogRead(ピン番号)で取得する
  • 0〜5Vの電圧を、0〜1023の値で返す
  • デジタルのようにpinModeによる初期化は不要
  • Unoでは、pinModeでOUTPUTを指定すると、アナログピンをデジタル出力として利用できる。また、digitalRead(A0)などとすると、デジタル入力としても利用できる

PCとシリアル通信

  • Serial.begin(bps数。9600など)で開始
  • Serial.println(値)で送信
  • PC側で、シリアルモニターを表示すれば、値が表示される

モーターや電球などを利用する

  • Arduinoの通常の5Vよりも大きい電圧が必要な場合は、MOSFETを利用する
  • Vinピンで外部電源の電圧が利用できる。VinとGNDと信号をMOSFETに接続して、信号を与えると外部電源の入力を制御することがPWMで利用できるようになる。これで、よくあるモーターや電球を駆動する
  • サーボモーターは4.8Vで動くので、MOSFETは使わず、Arduinoの5Vで良さそう

サーボモーターを動かす

SG-90を利用する場合のヒントです。 その9 Arduinoでサーボモータをキュイキュイ動かす! を参考にしました。

  • コネクタはJRタイプ
    • 茶色:GND
    • 赤:Vcc(+電源)
    • オレンジ:信号線
  • 信号線に、PWMで指示を与えれば良い
  • Servoライブラリで制御可能

まとめ

ここまで、二日もあれば把握できました。書籍の残り半分ぐらいは、仕様解説やリファレンスマニュアルになっているので、知りたいことを索引で調べて見つけることができます。サーボモーターを駆動するServoや、センサやアクチュエーターの通信で出てくるI2C通信するためのWireなど、あると便利な多くのライブラリが最初から使えます。また、外部ライブラリを組み込みむこともできます。

ライブラリが充実しているため、予想していたよりも手間がかかずに簡潔に開発ができる環境でした。ここで紹介している書籍とキットがあれば、基礎を身につけることができると思います。秋月電子さんのページなどに掲載されている多彩なセンサーやアクチュエーターと組み合わせて、大いに楽しめそうです。

Laravel5.2でのテストのメモ

Laravel5.3のものをこちらに書きました → Laravel5.3でのテストのメモ - tanaka's Programming Memo

    • -


自分向けのメモです。

LaravelにはPHPUnitによるテストの設定が組み込まれています。公式マニュアルで概要を確認して、データベースのチェックや、URLごとのテストについて調べました。

現時点で、Laravelは5.3がリリースされていますが、Sentinelがまだ対応していないので、5.2の方を調べました。

実行時の注意点

以下のようなエラーが表示された場合の対処方法です。

PHP Fatal error:  Call to undefined method PHPUnit_Framework_TestResult::warnings() in /Users/yutanaka/.composer/vendor/phpunit/phpunit/src/TextUI/ResultPrinter.php on line 297

Laravelに組み込まれているphpunitはやや古い可能性があります。グローバルのphpunitとバージョンが異なる場合に、上記のようなエラーが表示される場合があります。その際は、Laravelに組み込まれているphpunitを呼び出すように package.json に以下のようなスクリプト呼び出しを追加して、 npm test でテストを開始すると良いでしょう。

  "scripts": {
    // :
    "test": "vendor/phpunit/phpunit/phpunit"
  },

公式マニュアル概要

Testing - Laravel - The PHP Framework For Web Artisans を参照して、概要を確認します。

Introduction

  • Laravelアプリフォルダー直下のphpunit.xmlと、testsフォルダー内にサンプルが設定されている
  • アプリフォルダー内で phpunit で実行できる
テスト環境
  • テストを開始すると、Laravelが自動的に環境設定をtestingに設定する
  • Laravelは、テスト中はarrayドライバーでセッションやキャッシュを設定するので、テスト中のセッションやキャッシュは後に残ることがない
  • テスト環境は、phpunit.xmlファイルのtesting環境変数で変更して構わない。変更した後は、artisanコマンドのconfig:clearを実行して、設定のキャッシュをクリアすること
新しいテストの定義と実行
  • php artisan make:test UserTest などで、新しいテストを定義できる
  • testsフォルダー内に、新しいUserTestファイルが作成されるので、phpunitのコマンドを書いてテスト内容を記述する
  • 実行は、phpunitを呼び出す
  • setUpメソッドをオーバーライドする時は、親のsetUpを呼び出すこと

アプリケーションのテスト

  • Laravelは、HTTPリクエストを生成したり、出力をテストしたり、フォームを埋めたりするのが楽になるAPIを提供する
  • visit()メソッドは、GETリクエストをアプリケーションに発行する
  • see()メソッドは、アプリケーションの戻り値から指定した文字列が含まれるかをアサートする
  • dontSee()メソッドは、指定の文字列が戻り値に含まれないことをアサートする
アプリケーションとの連携
  • リンクをクリックする
    • visit()で画面を表示
    • click('<クリックしたい文字列>')
    • seePageIs('遷移先のURL')
  • フォームの操作
    • type(<入力内容>, <フォームのname>)で、指定のnameの入力フォームに指定のテキストを入力する
    • select(<選択内容>, <フォームのname>)で、ラジオボタンやドロップダウンの選択
    • check(<フォームのname>)で、指定のチェックボックスにチェック
    • uncheck(<フォームのname>)で、指定のチェックボックスのチェックを外す
    • attach(<ファイルのパス>, <フォームのname>)で、指定のファイルをアップロード対象にする
    • press(<ボタンやテキストや要素のname>)で、該当するものを押す
JSONテスト
  • get, post, put, patch, deleteメソッドで、指定のURLに、指定のパラメータを渡した呼び出しができる
  • seeJson()で、指定のデータが戻り値に含まれるかをチェックする。「含むか」なので、完全一致じゃなくてもテストは成功する
  • JSONの完全な一致をチェックしたい場合は、seeJsonEquals()メソッドを利用する
  • 戻り値の内容ではなく、構造をチェックしたい場合は、seeJsonStructure()メソッドを使う。指定していないキーがあっても、指定した構造が含まれていればテストは成功する
  • 何らかのキーに、指定の構造が含まれるかをチェックする場合は、 * を使う。ネストも可能
SessionとAuthentication
  • withSession()メソッドで、セッションを設定できる。ページを訪れる前に、テストしたい値をセッションに設定しておくことができる
  • actingAs()で、カレントユーザーを設定できる。事前に、ModelFactoryで、新規のユーザーモデルを作成して、それを引数に渡してユーザーを作成できる
  • actingAs()の第2引数を指定すると、毎回認証が必要な保護認証に対して、保護名を設定できる
ミドルウェアの無効化
  • WithoutMiddlewareトレイトを使うと、ミドルウェアを無効化して、テストを簡易にできる
    • クラス内に、use WithoutMiddleware;を宣言すると、そのテスト全てでミドルウェアが無効になる
    • 特定のテストのみで無効にしたい場合は、テストメソッドの中で、$this-.withoutMiddleware();を呼び出す
カスタムHTTPリクエスト
  • カスタムリクエストを作成して、戻り値のIlluminate\Http\Responseオブジェクトを取得したい場合は、call()メソッドを使う
  • POST, PUT, PATCHリクエストに必要な入力データは、配列で渡す
PHPUnitアサーション

以下、Laravelが提供するPHPUnitテスト用のメソッド。

  • assertResponseOk();
    • クライアントのレスポンスコードがOkかを判定
  • assertResponseStatus($code);
  • assertViewHas($key, $value = null);
    • 戻り値のビューに対して、$keyに$valueが設定されているかを判定
  • assertViewHasAll(array);
    • 戻り値が、指定の配列を持つかを判定
  • assertViewMissing($key);
    • 戻り値のビューに指定のkeyが含まれないことを判定
  • assertRedirectedTo($uri, $with = []);
    • 指定のURIにリダイレクトしたかを判定
  • assertRedirectedToRoute($name, $parameters = [], $with = []);
    • 指定のルートにリダイレクトされたかを判定
  • assertRedirectedToAction($name, $parameters = [], $with = []);
    • 指定のアクションにリダイレクトされたかを判定
  • assertSessionHas($key, $value = null);
    • セッションが指定のkeyとvalueを持つかを判定
  • assertSessionHasAll(array $bindings);
    • セッションが指定の配列の値を持つかを判定
  • assertSessionHasErrors($bindings = [], $format = null);
    • セッションが指定のエラーになっていないかを判定
  • assertHasOldInput();
    • セッションがold inputを持っていないかを判定
  • ssertSessionMissing($key);
    • セッションが指定のkeyを持っていないことを判定

Databaseのテスト

Laravelは、データベース駆動のアプリケーション向けのテスト環境も提供しています。

  • seeInDatabase()メソッドで、指定のデータが、データベースの指定のテーブルに含まれるかを判定
テスト後のデータベースのリセット

各テスト後に、テスト時のデータベースへの変更をもとに戻す方法です。

  • Migrationを使う
    • 次のテストの前に、DatabaseMigrationsトレイトを使って、Migrationする
    • テストクラス内で、 use DatabaseMigrations; を定義すれば、テストごとに自動的にマイグレーションを実施する
  • トランザクションを使う
    • 全てのテストを、データベースのトランザクションで囲む方法もある
    • DatabaseTransactionsトレイトを使えば、Migrationsと同様にテストごとに自動的にこれを行う
    • このトレイトは、デフォルトのデータベース接続にのみ対応
モデルファクトリー

複数のテストにまたがって、共通の幾つかのレコードをデータベースに登録したい場合、手動で特定の値を列ごとに設定するのではなく、Eloquentのモデルファクトリーを利用することができます。

  • database/factories/ModelFactory.php ファイルに、データの定義例がある
  • $factory->define()メソッドに、デフォルトデータを戻すクロージャーを渡す
  • クロージャーは、Faker PHP ライブラリのインスタンスを返し、テストのための乱数を設定できる
  • ModelFactory.phpファイルには、自由にファクトリーを追加できる
  • UserFactory.phpやCommentFactory.phpなど、database/factoriesディレクトリーに加えることができる

同じEloquentモデルクラスに対して、複数のファクトリーを利用するには、ベースとなるユーザーファクトリーを複製して、raw()メソッドでベースファクトリーを変更する方法があります。$factory->defineAs()メソッドでファクトリーを定義して、クロージャー内で$factoryを受け取って、$factory->row()でモデルクラスを受け取って、array_merge()で配列を結合します。

  • ファクトリーのテストでの使い方
    • ファクトリーを定義したら、テスト関数でfactory(モデルクラス)->make();とすれば、ファクトリーから生成したモデルのインスタンスを得られる
    • ファクトリーで作成したデフォルト値を書き換えたい場合は、make()メソッドに連想配列で変更要素を渡す
    • factory()の第2引数に数を渡すと、指定の数のインスタンスを生成する
    • 同じく、ファクトリー名を渡すと、該当するファクトリーを生成する
  • ファクトリーモデルを持続させる
    • create()メソッドは、モデルのインスタンスを作成した上で、データベースに値を保存する
    • create()メソッドに連想配列を渡すと、属性を上書きできる
  • リレーションの追加
    • create()してモデルを作成後、each()メソッドを呼び出して、posts()->save(factory(モデル暮らす)->make()); で設定できる
  • ファクトリーを定義するファクトリーに渡すクロージャーでモデルのリレーションを設定することもできる
    • Postを作成する時に、新しいユーザーを作成して、idをリレーションさせることができる
    • 生成したユーザーのIDを、その後の属性で利用することも可能

モック

イベントのモック
  • Laravelで大量にイベントシステムを構築していた場合、テスト中にイベントをモック化できる
  • ユーザー登録をした時に、登録が完了したイベント(例えばUserRegistered)が発行すると、登録完了のメールが送信されてりする。それをキャンセルするとテストが楽である
  • $this->expectsEvents(App\Events\UserRegistered::class); とすると、UserRegisteredイベントをキャンセルできる
  • doesntExpectEvents()メソッドを使うと、指定のイベントが発動しなかったことを確認できる
  • withoutEvents()メソッドを呼び出すと、すべてのイベントハンドラを抑制できる
ジョブをモック

アプリケーションがリクエストを作成した時に、作成したコントローラーが特定のジョブを呼び出すかを試す簡単なテストをしたい場合があるでしょう。そのような時の方法です。これにより、ルートとコントローラーのテストを分離することができます。

  • $this->expectsJobs(App\Jobs\PurchasePodcast::class);などとすることで、指定のジョブが呼び出されたことを確認する。行うのは確認のみで、ジョブ自体を実行することはしない
  • このメソッドは、DispatchesJobsトレイトか、dispatch()メソッドによって発行されたジョブにのみ反応する。Queue::push()で直に送信されたジョブは対象外
Facadeのモック

Facade(ファサード)は、Laravelのサービスを静的に呼び出せるようにしたもの。これをモックにします。例えば、Cacheファサードをモックに入れ替えてテストができます。

  • shouldReceive()メソッドを呼び出すと、Mockeryクラスのモックのインスタンスが返される
  • Laravelのservice containerにより管理されるので、そのままクラスを利用するよりもテストしやすい
  • Requestファサードをモックにすると、テストを実行する時にcall()やpost()メソッドのようなHTTPヘルパーメソッドもキャンセルされてしまうので、モックにするべきではない

Laravel+Sentinel ロール管理機能を作成する

ユーザー管理の実装からの続きです。

ロール管理機能を追加していきます。

ロール管理画面の概要

機能

ロール管理画面も、基本的にはユーザー管理と同じように作成します。必要な機能は以下の通りです。

  • 新しいロールの作成
    • ロール名
    • slug
    • 追加ボタン
  • 新しい権限の追加
    • 権限の文字列のみ
    • 追加ボタン
  • ロール一覧
    • ロール名
    • slug
    • 権限リスト(全ての権限を並べたチェックボックス)
    • 修正ボタン
    • 削除ボタン

アクセス権限

ユーザーが持つrole.view, role.create, role.update, role.delete の権限でチェックします。

ロール管理用のコントローラーの作成

ユーザー管理の時と同様に、resourceフラグでコントローラーを作成して、ルートに適用します。

ロール用のコントローラーを作成

  • ターミナルから、以下を実行して、ロール用のリソースコントローラーを作成
php artisan make:controller Sentinel/RoleController --resource

パーミッション用のコントローラーを作成

パーミッションは、追加と削除のみなので、手動でコントローラーとルートを作成します。

  • app/Http/Controllers/Sentinel/PermissionController.php を新規に作成してエディターで開く
  • 以下のコードを追加
<?php

namespace App\Http\Controllers\Sentinel;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;

class PermissionController extends Controller
{
    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id)
    {
        //
    }
}

ルートを設定

作成したコントローラーへのアクセスをルートに追加します。

  • app/Http/routes.php をエディターで開く
  • 以下の行をusersの定義の下に追加して、ロールのためのルートを追加
Route::resource('roles', 'Sentinel\RoleController');

Route::post('permissions', 'Sentinel\PermissionController@store');
Route::delete('permissions/{name}', 'Sentinel\PermissionController@destroy');

ロール管理画面の開発

ビューを表示するためのコードを作成

ロール管理画面用のビューを呼び出すルートを追加します。

  • app/Http/Controllers/Sentinel/RoleController.php をエディターで開く
  • あらかじめ作成されているindex()メソッドを、以下のようにコード追加する
    public function index()
    {
        return view('sentinel.roles', ['permissions' => self::getPermissionList()]);
    }

パーミッション一覧を取得するためのコードを追加

パーミッション一覧を表示するために、現在登録されている全てのロールが持っている、全てのパーミッション名のリストが必要なので、それを取得するための関数を作成します。

  • 引き続き、app/Http/Controllers/Sentinel/RoleController.php で作業する
  • Sentinelを参照するために、コードの上の方に以下を追加する
use Sentinel;
  • RoleControllerクラスに以下の関数を追加する
    /**
     * 全ロールに設定されているパーミッションのリストを作成
     */
    public static function getPermissionList() {
        $permissions = [];
        foreach(Sentinel::getRoleRepository()->all() as $role) {
            foreach($role->permissions as $k => $v) {
                if (!in_array($k, $permissions)) {
                    $permissions[] = $k;
                }
            }
        }
        return $permissions;
    }

ビューの作成

ビューを作成します。パーミッションは単純な追加と削除のみなので、今回はロールとパーミッションの両方をまとめて管理するページにします。

エラー表示ブロックの共有化

エラーが発生した要素ごとにエラーメッセージを表示させようと思います。各要素で必要なコードは共通ですので、共通のビューを作成して、includeで読み込ませて使えるようにします。

  • resources/views/parts/error-block.blade.php ファイルを作成して、エディターで開く
  • 以下のコードを入力
@if ($errors->has($name))
    <span class="help-block">
        <strong>{{ $errors->first($name) }}</strong>
    </span>
@endif

これを使うには、入力要素を以下のような形にして、inputタグに続けて@includeでerror-blockを読み込み、引数としてinputタグのnameを指定します。

*** 使用例

<div class="form-group{{ $errors->has('入力要素のname') ? ' has-error' : '' }}">
    <input name="入力要素のname" class="form-control" ・・・
    @include('parts.error-block', ['name' => '入力要素のname'])
</div>
モーダルウィンドウの作成

ロールの変更や削除の選択をする時のBootstrapのモーダルウィンドウ用のビューを作成します。

  • resources/views/parts/modal.blade.php を作成して、以下のコードを入力する
{{-- 以下のように呼び出しボタンを用意する
<button type="button" class="btn" data-toggle="modal" data-target="#ターゲット">
    ボタン名
</button>

id 呼び出すID
title モーダルに表示するタイトル
body モーダルの本文
action 実行時のURL
method 実行時のメソッド
--}}
<div class="modal fade" id="{{$id}}" tabindex="-1" role="dialog">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
            <span aria-hidden="true">&times;</span>
        </button>
        <h4 class="modal-title">{{$title}}</h4>
      </div>
      <div class="modal-body">
          {{$body}}
      </div>
      <div class="modal-footer">
          <form action="{{$action}}" method="POST">
              {{ csrf_field() }}
              {{ method_field($method) }}

              <button type="submit" class="btn btn-primary">
                  <i class="fa fa-btn fa-check"></i>  はい
              </button>
              <button type="button" class="btn btn-default" data-dismiss="modal">
                  <i class="fa fa-btn fa-close"></i>いいえ</button>
          </form>
      </div>
    </div><!-- /.modal-content -->
  </div><!-- /.modal-dialog -->
</div><!-- /.modal -->

ボタンごとにFormがないものも用意します。

  • resources/views/parts/modal-no-form.blade.php を作成して、以下のコードを入力する
{{-- モーダルのform未組み込み版
    以下のように呼び出しボタンを用意する
<button type="button" class="btn" data-toggle="modal" data-target="#ターゲット">
    ボタン名
</button>
id 呼び出すID
title モーダルに表示するタイトル
body モーダルの本文
--}}
<div class="modal fade" id="{{$id}}" tabindex="-1" role="dialog">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
            <span aria-hidden="true">&times;</span>
        </button>
        <h4 class="modal-title">{{$title}}</h4>
      </div>
      <div class="modal-body">
          {{$body}}
      </div>
      <div class="modal-footer">
          <button type="submit" class="btn btn-primary">
              <i class="fa fa-btn fa-check"></i>  はい
          </button>
          <button type="button" class="btn btn-default" data-dismiss="modal">
              <i class="fa fa-btn fa-close"></i>いいえ
          </button>
      </div>
    </div><!-- /.modal-content -->
  </div><!-- /.modal-dialog -->
</div><!-- /.modal -->
ロール管理ビューの作成
  • resources/views/sentinel/roles.blade.php ファイルを作成して、エディターで開く
  • 以下のコードを入力
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-12">
            @include('parts.info')
            @include('common.errors')

            <h4>パーミッション</h4>

            <table class="table table-striped table-hover table-bordered">
                <thead>
                    <tr>
                        <th>パーミッション名</th>
                        <th>操作</th>
                    </tr>
                </thead>
                <tbody>
                    <!-- 新規登録-->
                    <form class="col" role="form" method="POST" action="{{url('permissions')}}">
                        <tr>
                            {{ csrf_field() }}
                            <td>
                                <div class="form-group{{ $errors->has('new_permission') ? ' has-error' : '' }}">
                                    <input id="new_permission" type="text" class="form-control" name="new_permission" value="{{ old('new_permission') }}">
                                    @include('parts.error-block', ['name' => 'new_permission'])
                                </div>
                            </td>
                            <td>
                                <button type="submit" class="btn btn-primary">
                                    <i class="fa fa-btn fa-plus"></i> 新規登録
                                </button>
                            </td>
                        </tr>
                    </form>
                </tbody>
            </table>

            <h4>パーミッションの削除</h4>

            <div class="row">
                <div class="col-md-12">
                    @foreach($permissions as $permission)
                        <button type="button" class="btn btn-default" aria-label="Close" data-toggle="modal"
                            data-target="#delete-permission-{{array_search($permission, $permissions)}}">
                            <i class="fa fa-btn fa-remove"></i> {{$permission}}
                        </button>
                        @include('parts.modal', [
                            'id' => 'delete-permission-'.array_search($permission, $permissions),
                            'title' => trans('sentinel.delete_permission_title'),
                            'body' => trans('sentinel.confirm_delete_permission').' : '.$permission,
                            'action' => url('permissions', base64_encode($permission)),
                            'method' => 'DELETE'
                        ])
                    @endforeach
                </div>
            </div>

            <hr>

            <h4>ロール追加</h4>

            <table class="table table-striped table-hover table-bordered">
                <thead>
                    <tr>
                        <th>ロール名(日本語可)</th>
                        <th>Slug</th>
                        <th>パーミッション</th>
                        <th>操作</th>
                    </tr>
                </thead>

                <tbody>
                    <!-- 新規登録-->
                    <form class="col" role="form" method="POST" action="{{url('roles')}}">
                        {{ csrf_field() }}

                        <tr>
                            <td>
                                <div class="form-group{{ $errors->has('new_role') ? ' has-error' : '' }}">
                                    <input id="new_role" type="text" class="form-control" name="new_role" value="{{ old('new_role') }}">
                                    @include('parts.error-block', ['name' => 'new_role'])
                                </div>
                            </td>
                            <td>
                                <div class="form-group{{ $errors->has('new_slug') ? ' has-error' : '' }}">
                                    <input id="new_slug" type="text" class="form-control" name="new_slug" value="{{ old('new_slug') }}">
                                    @include('parts.error-block', ['name' => 'new_slug'])
                                </div>
                            </td>
                            <td>
                                @foreach ($permissions as $per)
                                    <div>
                                        <input type="checkbox" name="new_per_{{str_replace(".", "-", $per)}}"
                                            {{old("new_per_".str_replace(".", "-", $per))=="on" ? 'checked="true"' : ''}}
                                        > {{$per}}
                                    </div>
                                @endforeach
                            </td>
                            <td>
                                <button type="submit" class="btn btn-primary">
                                    <i class="fa fa-btn fa-plus"></i> 新規登録
                                </button>
                            </td>
                        </tr>

                        </div>

                    </form>

                </tbody>
            </table>

            <h4>ロール一覧</h4>
            <table class="table table-striped table-hover table-bordered">
                <thead>
                    <tr>
                        <th>ロール名(日本語可)</th>
                        <th>Slug</th>
                        <th>パーミッション</th>
                        <th colspan="2">操作</th>
                    </tr>
                </thead>

                <tbody>
                    @foreach(Sentinel::getRoleRepository()->all() as $role)
                        <tr>
                            <form class="col" role="form" method="POST" action="{{url('roles', $role->id)}}">
                                {{ csrf_field() }}
                                {{ method_field('PUT') }}
                                <td>
                                    <div class="form-group{{ $errors->has('role_'.$role->id.'_name') ? ' has-error' : '' }}">
                                        <input type="text"
                                            class="form-control"
                                            name="role_{{$role->id}}_name"
                                            id="role_{{$role->id}}_name"
                                            value="{{empty(old('role_'.$role->id.'_name')) ? $role->name : old('role_'.$role->id.'_name')}}">
                                        @include('parts.error-block', ['name' => 'role_'.$role->id.'_name'])
                                    </div>
                                </td>
                                <td>
                                    <div class="form-group{{ $errors->has('role_'.$role->id.'_slug') ? ' has-error' : '' }}">
                                        <input type="text"
                                            class="form_control"
                                            name="role_{{$role->id}}_slug"
                                            id="role_{{$role->id}}_slug"
                                            value="{{empty(old('role_'.$role->id.'_slug')) ? $role->slug : old('role_'.$role->id.'_slug')}}">
                                        @include('parts.error-block', ['name' => 'role_'.$role->id.'_slug'])
                                    </div>
                                </td>
                                <td>
                                    @foreach ($permissions as $per)
                                        <div>
                                            <input type="checkbox"
                                                name="role_{{$role->id}}_per_{{str_replace(".", "-", $per)}}"
                                                @if (old("role_".$role->id."_per_".str_replace(".", "-", $per))=="on")
                                                    checked="true"
                                                @elseif ($role->hasAccess($per))
                                                    checked="true"
                                                @endif
                                            > {{$per}}
                                        </div>
                                    @endforeach
                                </td>
                                <td>
                                    <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#role_{{$role->id}}_update">
                                        <i class="fa fa-btn fa-refresh"></i> 変更
                                    </button>

                                    @include('parts.modal-no-form', [
                                        'id' => 'role_'.$role->id.'_update',
                                        'title' => 'ロールの更新',
                                        'body' => 'ロール['.$role->name.']の情報を更新しますか?',
                                    ])
                                </td>
                            </form>
                            <td>
                                <button type="submit" class="btn btn-danger" data-toggle="modal" data-target="#role-{{$role->id}}-delete">
                                    <i class="fa fa-btn fa-remove"></i> 削除
                                </button>

                                @include('parts.modal', [
                                    'id' => 'role-'.$role->id.'-delete',
                                    'title' => 'ロールの削除',
                                    'body' => 'ロール['.$role->name.']を削除しますか?',
                                    'action' => url('roles', $role->id),
                                    'method' => 'DELETE',
                                ])
                            </td>
                        </tr>
                    @endforeach
                </tbody>

            </table>

        </div>
    </div>
</div>
@endsection

以上で、ロール管理画面のフォームの表示まで出来ました。 http://0.0.0.0:8080/roles にアクセスしてみてください。画面が表示されて、利用されているパーミッションリストやロール一覧が確認できます。

まだ、ボタンの処理は実装していないので、何かを押してもエラーが発生します。それぞれの機能を追加していきます。

パーミッションの新規登録処理

ロールに設定できるパーミッションの種類を追加する処理を追加するために、 PermissionController.php のstore関数にコードを追加します。

  • app/Http/Controllers/Sentinel/PermissionController.php をエディターで開く
  • ロールコントローラーやSentinel、リダイレクトを利用するために以下の3つのuseを最初の方に追加
use App\Http\Controllers\Sentinel\RoleController;
use Sentinel;
use Redirect;
  • 先に作成したstore関数を以下のように中身を実装する
    public function store(Request $request)
    {
        // バリデーション
        $this->validate($request, [
           // nameは必須で、255文字まで
           'new_permission' => 'required|max:255',
       ]);

       // 既存なら何もしない
       $nowper = RoleController::getPermissionList();

       if (in_array($request->new_permission, $nowper)) {
           // すでにあるので、エラーで返す
           return Redirect::back()->withInput()->withErrors(['new_permission' => trans('sentinel.same_permission')]);
       }

       // 作成実行
       foreach(Sentinel::getRoleRepository()->all() as $role) {
           $role->addPermission($request->new_permission, false)->save();
       }

       // 成功
       return Redirect::back()->with(['info' => trans('sentinel.permission_add_done').":".$request->new_permission]);
    }

以上で、パーミッションの追加ができるようになりました。ロール管理画面を表示して、「test」などの名前のパーミッションの登録をしてみてください。

パーミッションの削除

パーミッションを削除する機能を追加します。引き続き、PermissionController.php で作業をします。

  • あらかじめ作成していた PermissionController.php の destroy関数を、以下のように実装する
    public function destroy($name)
    {
        $per = base64_decode($name);
        $permissions = RoleController::getPermissionList();
        if (!in_array($per, $permissions)) {
            return Redirect::back()->withErrors(['delete_permission' => trans('sentinel.invalid_permission')]);
        }

        // 削除
        foreach (Sentinel::getRoleRepository()->all() as $role) {
            $role->removePermission($per)->save();
        }

        return Redirect::back()->with(['info' => trans('sentinel.permission_delete_done').":".$per]);
    }

以上で削除が動作するようになります。先ほど追加した「test」などのパーミッションを、削除欄から選択して消してみてください。

ロールの追加

ロールを追加する処理をRoleController.phpに追加します。

  • app/Http/Controllers/Sentinel/RoleController.php をエディターで開く
  • Redirectを使えるように、ファイルの最初の方に以下のuse文を追加
use Redirect;
  • あらかじめ作成されている store 関数を以下のように実装する
    public function store(Request $request)
    {
        $this->validate($request, [
            'new_role' => 'required|max:255',
            'new_slug' => 'required|max:255',
        ]);

        // パーミッションリストの作成
        $permissions = [];
        $pers = self::getPermissionList();
        foreach($pers as $per) {
            $permissions[$per] = $request['new_per_'.str_replace(".", "-", $per)] == "on";
        }

        $role = Sentinel::getRoleRepository()->createModel()->create([
            'name' => $request->new_role,
            'slug' => $request->new_slug,
            'permissions' => $permissions
        ]);

        return Redirect::back()->with(['info' => trans('sentinel.role_create_done')]);
    }

以上でロールが追加できるようになります。「testroll」などの名前で登録してみてください。

ロールの変更

ロールの変更処理を実装します。

  • app/Http/Controllers/Sentinel/RoleController.php をエディターで開く
  • あらかじめ作成されている update 関数を以下のように実装する
    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, $id)
    {
        $idx = 'role_'.$id.'_';

        // バリデーションを実施
        $this->validate($request, [
            $idx.'name' => 'required|max:255',
            $idx.'slug' => 'required|max:255',
        ]);

        // 修正をチェック
        $updates = [];
        $updates[] = trans('sentinel.role_update_done');

        // 現ロールを取得
        $role = Sentinel::findRoleById($id);
        if ($role == null) {
            return Redirect::back()->withInput()->withErrors(['invalid_role' => trans('sentinel.invalid_role')]);
        }

        // 名前チェック
        if ($role->name !== $request[$idx."name"]) {
            $updates[] = "ロール名: ".$role->name." > ".$request[$idx."name"];
            $role->name = $request[$idx."name"];
        }
        // slugチェック
        if ($role->slug !== $request[$idx."slug"]) {
            $updates[] = "Slug: ".$role->slug." > ".$request[$idx."slug"];
            $role->slug = $request[$idx."slug"];
        }

        // パーミッションの設定
        $permissions = self::getPermissionList();
        foreach($permissions as $per) {
            if ($role->hasAccess($per) && (!$request[$idx."per_".str_replace(".", "-", $per)])) {
                $updates[] = $per." > off";
                $role->updatePermission($per, false);
            }
            else if (!$role->hasAccess($per) && ($request[$idx."per_".str_replace(".","-",$per)])) {
                $updates[] = $per." > on";
                $role->updatePermission($per, true);
            }
        }

        // 更新
        if (count($updates) > 1) {
            $role->save();
            return Redirect::back()->with(['info' => $updates]);
        }

        // 更新なし
        return Redirect::back()->with(['info' => trans('sentinel.no_changed')]);
    }

info.blade.phpでエラーが発生したら、以下のように修正する

@if (session('info') || isset($info))
<div class="alert alert-info">
    @if (session('info'))
        @if (is_array(session('info')))
            <ul>
            @foreach(session('info') as $ln)
                <li>{{$ln}}</li>
            @endforeach
            </ul>
        @else
            {{ session('info') }}
        @endif
    @endif
    @if (isset($info))
        {{ $info }}
    @endif
</div>
@endif

以上で完了です。登録した「test」ロールの名前やslug、パーミッションを変更して、「変更」を押して、正しく情報が更新されることを確認してください。

ロールの削除

最後に、ロールを削除する処理を実装します。

  • app/Http/Controller/Sentinel/RoleController.php をエディターで開く
  • あらかじめ作成されている destroy 関数を以下のように実装する
    public function destroy($id)
    {
        // 削除実行
        $role = Sentinel::findRoleById($id);
        if ($role === null) {
            return Redirect::back()->withInput()->withErrors(['role-'.$id.'_delete', trans('sentinel.invalid_role')]);
        }
        $name = $role->name;

        // 削除実行
        $role->delete();

        return Redirect::back()->with(['info' => trans('sentinel.role_delete_done')." : ".$name]);
    }

以上で完了です。「test」ロールを削除してみてください。

パーミッションの確認

機能が実装できたので、各操作がパーミッションを持つユーザーのみ操作できるように、コントローラーのコンストラクターにコードを追加します。

  • app/Http/Controllers/Sentinel/RoleController.php をエディターで開く
  • 以下のコンストラクターをクラスに追加する
     /**
      * コンストラクター
      * 処理に権限チェックのミドルウェアを設定
      */
     public function __construct() {
         $this->middleware('permission:role.view', [
             'only' => [
                 'index'
             ]
         ]);
         $this->middleware('permission:role.update', [
             'only' => [
                 'update'
             ]
         ]);
         $this->middleware('permission:role.create', [
             'only' => [
                 'store'
             ]
         ]);
         $this->middleware('permission:role.delete', [
             'only' => [
                 'destroy'
             ]
         ]);
    }
  • app/Http/Controllers/Sentinel/PermissionController.php をエディターで開く
  • 以下のコンストラクターをクラスに追加する
    /**
     * コンストラクター
     * 処理に権限チェックのミドルウェアを設定。パーミッションの権限はロールに準じる
     */
    public function __construct() {
        $this->middleware('permission:role.create', [
            'only' => [
                'store'
            ]
        ]);
        $this->middleware('permission:role.delete', [
            'only' => [
                'destroy'
            ]
        ]);
    }

これで、ロールやパーミッションの各操作は、以下のパーミッションが有効なロールを持ったユーザーのみが実行できるようになります。

できたら、権限のないユーザーで http://0.0.0.0:8080/roles にアクセスしてみましょう。権限がないので、しばらく元の画面に戻ろうとしたのちにエラーが表示されます。通常はrolesに直接アクセスすることはないので、遷移元のページに戻ります。

最後に

最適な例にはなっていないと思いますが、LaravelとSentinelのサンプルとして公開しました。必要な作業が思ったより多かったですが、それでもこれらのフレームワークを使わないとさらに膨大な手間がかかりますので、とてもありがたい環境です。PHPもまだまだいけてます。