20190527のlaravelに関する記事は5件です。

laravel-slack-apiを使用して、Laravelからslackへ通知する

はじめに

Laravelではslack-notification-channelというslackへ通知する仕組みが存在しています。
https://laravel.com/docs/5.8/notifications#slack-notifications

しかし、Incoming Webhookを使用しているためfile upload等をすることができません。
今回自分はアプリケーションログをfile形式でslackへ通知したかったので、laravel-slack-apiライブラリを使用してみました。
今回は使い方の紹介となります。
※最終コミットが2016年でしたが、問題なく動きました。

アプリケーションログをfile形式でslackへ通知した際のイメージです。
スクリーンショット 2019-05-26 12.58.11.png

設定

laravel-slack-apiのreadme.mdに沿って進めています。

また、slack botの設定はこの記事の最後に手順を書いております。

インストール

composer require vluzrmos/slack-api

providers、config等の追加

config/app.php
'providers' => [
     Vluzrmos\SlackApi\SlackApiServiceProvider::class,
 ]

//...

'aliases' => [
    'SlackApi'              => Vluzrmos\SlackApi\Facades\SlackApi::class,
    'SlackChannel'          => Vluzrmos\SlackApi\Facades\SlackChannel::class,
    'SlackChat'             => Vluzrmos\SlackApi\Facades\SlackChat::class,
    'SlackGroup'            => Vluzrmos\SlackApi\Facades\SlackGroup::class,
    'SlackFile'             => Vluzrmos\SlackApi\Facades\SlackFile::class,
    'SlackSearch'           => Vluzrmos\SlackApi\Facades\SlackSearch::class,
    'SlackInstantMessage'   => Vluzrmos\SlackApi\Facades\SlackInstantMessage::class,
    'SlackUser'             => Vluzrmos\SlackApi\Facades\SlackUser::class,
    'SlackStar'             => Vluzrmos\SlackApi\Facades\SlackStar::class,
    'SlackUserAdmin'        => Vluzrmos\SlackApi\Facades\SlackUserAdmin::class,
    'SlackRealTimeMessage'  => Vluzrmos\SlackApi\Facades\SlackRealTimeMessage::class,
    'SlackTeam'             => Vluzrmos\SlackApi\Facades\SlackTeam::class,
]
config/services.php
'slack' => [
    'token' => env('SLACK_TOKEN'),
]

slackで発行されたトークンを記載する

 .env
SLACK_TOKEN='xoxb-xxxxx'

使い方

$php artisan tinkerから簡単に動作確認できます。

シンプルなメッセージを送る

SlackChat::message('#test', 'ピピッカチュウ');

スクリーンショット 2019-05-27 22.25.20.png

テキストをfileとして送る

SlackFile::upload([
    'filename' => 'ピーカ.txt',
    'initial_comment' => 'ピカピカーチュ',
    'content' => "ピーカ、チューウ!ピーカ、チューウ!ピーカ、チューウ!ピーカ、チューウ!\n",
    'channels' => '#test'
]);

スクリーンショット 2019-05-27 22.14.42.png


slackボットの設定

下記はslackボットの設定方法です。

slack Appの作成

slack apiから設定を行う。
スクリーンショット 2019-05-27 21.48.53.png

Bot Userの作成

スクリーンショット 2019-05-27 21.50.54.png

scopeの設定

botから投稿する場合はchat:write:botを付与する。
スクリーンショット 2019-05-27 21.51.36.png

連携許可

スクリーンショット 2019-05-27 21.51.46.png

Bot User OAuth Access Token

Bot User OAuth Access Tokenをlaravel側のslack tokenに設定してください。
スクリーンショット 2019-05-27 21.52.00.png

Botを対象のチャンネルに追加

スクリーンショット 2019-05-27 21.52.29.png

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

Policyが紐づいていないModelに対してGate::deniesを実行したらtrueが返ってきた

環境

Laravel: 5.5
PHP: 7.2

前提

同じModelでもグローバルスコープをかけたい場合とかけたくない場合で2つModelを作っていた。

グローバルスコープをかけている方はかけていない方を継承したModelになっている。

グローバルスコープをかけている方は、権限やdeleted_atの関係でかけていたのでもちろんPolicyも登録されていた。

かけていない方は、権限を気にせず参照したい場合があるという理由でかけておらずPolicyもつけていなかった。

起こったこと

URLに特定のパラメータ(hoge_id=1のような形)が入っているかでModelの持ってくる方法を変えている三項演算子があった。

$hoge = $request->has('hoge_id') ? Hoge::findOrFail($request->get('hoge_id')) : Auth::user()->fuga->hoge;

この三項演算子の:の右側に入った場合にAuth::user()で取り出される認証系のModelがグローバルスコープをかけていない方になっており、そのリレーションなので取り出されたfugaもhogeもかけていない方(=Policyがついていない)になっていた。

その場合に、Gate::deniesの第2引数に$hogeを渡すと、第1引数で指定したPolicy内で作成した関数にたどり着かずtrueで返ってきた。

Gateクラスの中身を覗いてみるとGate::deniesはGate::allowsの結果を反転させたものが返されていた。

Gate::allowsの中身は以下の通り

public function allows($ability, $arguments = [])
{
    return $this->check($ability, $arguments);
}

呼び出されているcheckの中身はこんな感じ

public function check($abilities, $arguments = [])
{
    return collect($abilities)->every(function ($ability) use ($arguments) {
        try {
            return (bool) $this->raw($ability, $arguments);
        } catch (AuthorizationException $e) {
            return false;
        }
    });
}

tryの中のrawはこんな感じ(コメントアウトは見やすさのために一旦除けました)

protected function raw($ability, $arguments = [])
{
    if (! $user = $this->resolveUser()) {
        return false;
    }

    $arguments = Arr::wrap($arguments);

    $result = $this->callBeforeCallbacks(
        $user, $ability, $arguments
    );

    if (is_null($result)) {
        $result = $this->callAuthCallback($user, $ability, $arguments);
    }

    $this->callAfterCallbacks(
        $user, $ability, $arguments, $result
    );

    return $result;
}

rawの中で呼ばれているresolveUser()を条件にしたif文の中のreturn false;が怪しいと踏んでresolveUser()を見に行く

こんな感じ

protected function resolveUser()
{
    return call_user_func($this->userResolver);
}

ここのcall_user_funcで追いかけられなくなったので終了。

以下推測

call_user_funcのphp公式ドキュメントを見に行ったが存在しない関数名を渡した場合はどうなるかはわからず。ただ一番ありそうな気がする部分なので、call_user_funcでfalseが返り、

if (! $user = $this->resolveUser()) {
        return false;
}

のif文の中に入ったことでfalseがallowsの結果として返りdeniesがtrueになったのかなと思います。

振り返り

結局のところ型の意識が抜けた状態でプログラムを書いていたのが根本の原因かなと思うのでこういう(何らかの原因で同じModelを二つ作らざるを得ない)場合には自分が今どっちのModelを扱っているのか気にしたり後からそれが明示的にわかるような書き方をする必要があるなと学びました。

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

マイグレーション(migration)ファイルのupとdownとchangeについて

up

マイグレート(migrate)をした時に、実行される処理です。

down

ロールバック(rollback)した時に、実行される処理です。

ロールバックでまだ存在しないカラムを作ることも実はできる。

change

これは、upとdownの合体版です。

changeの中にテーブル構成を記述します。
migrateすると、記述を元にテーブル、カラムの作成等が行われます。

rollbackは基本的にmigrate前の状態に戻してくれます。

取り消しではありません。

change一つでupとdownの両方ができるので便利です。

changeの注意点

既に存在するカラムを削除したい場合ちょっと注意です。

1, カラムを削除するためにマイグレーションファイルを作成

2, change内にテーブル名とカラム名の2つだけを記述して、migrateを実行し、カラムを削除。

3, 消すもの間違えた!ので、rollbackして今消したカラムを復活だ!
rollback!!
とするとエラーが出る可能性があります。

理由はrollback時におけるカラム作成のための情報不足です。

どういうことかというと、rollbackは、基本的にmigrateの反転です。
migrateが削除なら、rollbackは作成になります。

カラム作成の時にテーブル名とカラム名だけでは不十分ですよね。

なので、型も定義してあげなければいけません。

他にありがちなミス

1, マイグレーションファイルを作成する。
2, 間違えてdown()メソッドの中にカラム作成の記述をする。
3, 知らずにmigrateしテーブルを確認する。
4, テーブルにカラムがないことに気づき、rollbackする。
(この時点でテーブルにカラムが作成されるが大体気づかない。)
5, 記述を直して、再びmigrateする。
6, 記述はあっているはずなのに、既にそのカラムがあります的なエラーを吐かれて詰む。

といったこともあります。

初心者の方は特に気をつけてください。

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

Laravel git cloneした後にすること

laravelをGit管理するときにつまずいたこ

コマンドプロンプトで少し操作しないと動かない

copy .env.sample .env
composer install
php artisan key:generate

以下のように
composer update
コマンドを打つ場合は
composer.jsonファイルを書きかえた時だけ。

実際の手順は
作業者A composer.json修正
作業者A composer update
作業者A GITにプッシュ->マージ
他の作業者 GITからプル
他の作業者 composer install

(参考)
https://qiita.com/waonn/items/43298d66f7cc59c64a2d

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

swagger-uiでAPIを叩いて結果をjsonで見る

ローカルで開発中にUI上からAPIを実行して結果をjson形式で見たいと思ってLaravelプロジェクトで実現した件をまとめておきます。
Laravelは5.5を使っています。

最終的にはこういう事ができます

API実行画面
スクリーンショット 2019-05-26 22.51.13.png

レスポンス
スクリーンショット 2019-05-27 8.52.45.png

必要な作業

必要な対応は下記の通りです。

  • swagger-uiの環境を作る
  • swagger-phpを使ってopenapi.yamlを自動生成できるようにする
  • Cross Originなリクエストをswagger-uiから実行可能にする
  • swagger-uiからのリクエストの時はレスポンスをjsonで返す
  • swagger-uiからのリクエストの時は認証を無効にする
  • swagger-uiからのPOSTリクエスト時のトークンチェックを無効にする

swagger-uiの環境を作る

swager-ui用の階層をプロジェクトのrootに作って下記のファイルを作ります。
ポートは衝突しない何かしらを選んで指定します。

docker-compose.yml
swagger-ui:
  image: swaggerapi/swagger-ui
  container_name: "swagger-ui"
  ports:
    - "8001:8080"
  volumes:
    - ./openapi.yaml:/openapi.yaml
  environment:
    SWAGGER_JSON: /openapi.yaml

SWAGGER_JSON という環境変数で、apiを定義するファイルを指定します。
openapi.yamlは同階層に作成し、マウントしておきます。

swagger-phpを使ってopenapi.yamlを自動生成できるようにする

openapi.yamlを手で書くこともできますが、コントローラに記述したPHPDocから自動生成するツールがあるのでそれを使います。どの道何かしらを追記する必要がありますが、編集対象をphpファイルに統一できるのでこれを使います。

下記の手順でパッケージを追加します。

composer require zircote/swagger-php

これで生成コマンドが使えるようになるので、Makefileに生成コマンドを作っておきます。
実行するとプロジェクト内のPHPDocを見てopenapi.yamlが生成されます。

build: ## openapiの定義ファイルを更新する
    ./vendor/bin/openapi app -o ./swagger-ui/

swagger-phpの記法については下記URLを参考にします。
http://zircote.com/swagger-php

GETリクエストの例

    /**
     * @OA\Get(
     *     path="/sample",
     *     tags={"SampleController"},
     *     @OA\Parameter(name="some_id",
     *         in="query",
     *         description="何かしらのid",
     *         @OA\Schema(type="integer")
     *     ),
     *     @OA\Response(
     *         response="200",
     *         description="レスポンス内容の説明"
     *     )
     * )
     */

POSTリクエストの例

   /**
     * @OA\Post(
     *     path="/sample/register",
     *     tags={"SampleController"},
     *     description="何かしらの登録処理",
     *     @OA\RequestBody(
     *         required=true,
     *         @OA\MediaType(
     *             mediaType="application/x-www-form-urlencoded",
     *             @OA\Schema(
     *                 type="object",
     *                 @OA\Property(
     *                     property="name",
     *                     description="登録する何かの名前",
     *                     type="string"
     *                 ),
     *                 @OA\Property(
     *                     property="user_id",
     *                     description="ユーザid",
     *                     type="integer"
     *                 )
     *             )
     *         )
     *     ),
     *     @OA\Response(
     *         response="200",
     *         description="レスポンス内容の説明",
     *     )
     * )
     */

Cross Originなリクエストをswagger-uiから実行可能にする

異なるhostからのリクエストはデフォルトでは禁じられているため、ローカルでかつswagger-uiからのアクセスのみ許可する設定をします。

下記のページにあるように、許可するhostを指定する必要があります。
https://fetch.spec.whatwg.org/#http-cors-protocol

Middlewareとして下記のクラスを追加します。

Cors.php
<?php

namespace App\Http\Middleware;

use Closure;

class Cors
{
    // from swagger-ui
    const ACCESS_ALLOW_ORIGIN = 'http://localhost:8001';

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $response = $next($request);

        if (app()->isLocal()) {
            $response
                ->header("Access-Control-Allow-Origin" , self::ACCESS_ALLOW_ORIGIN)
                ->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
        }

        return $response;
    }
}

swagger-uiからのリクエストの時はレスポンスをjsonで返す

これでswagger-uiでAPIの一覧を見れるようになりました。
ただし、このままだとレスポンスがhtmlになってしまうので、形式をjsonに変更します。

LaravelはデフォルトでBaseControllerを継承していますが、その間に一つ中間クラスを作ってそこに共通処理を書いていきます。

まずはswagger-uiからのリクエスト判定です。
ローカルからのアクセスであることと、headerの中のアクセスoriginを取得して判定します。

use Illuminate\Routing\Controller as BaseController;
use App\Http\Middleware\Cors;
class Controller extends BaseController
{
    protected function isSwagger() {
        return app()->isLocal() && request()->headers->get('origin') === Cors::ACCESS_ALLOW_ORIGIN;
}

次にこれを使ってresponseの形式を変換します。
実装に手を入れることになり嫌な気持ちがありますが、responseをviewのパラメータを渡す処理をラップする関数を作り、Controllerの記述を書き換えます。

    protected function response($view, $response) {
        if ($this->isSwagger()) {
            return response()->json($response);
        }
        return view($view)->with($response);
    }

これでswagger-uiからのアクセス時にのみresponse形式をjsonにすることができました。
他にうまい方法があればそうしたいところ。

swagger-uiからのリクエストの時は認証を無効にする

何かしらの仕組みで認証を使っている場合は無効にしないとswagger-ui上から突破するのは困難です。
これは認証の使い方次第ですが、標準搭載されているMiddlewareを使っている場合は下記のように対応することで無効化できます。

    public function __construct()
    {
        if (!$this->isSwagger()) {
            $this->middleware('auth')->only(['getUpdate', 'postRegister', 'postUpdate']);
        }
    }

swagger-uiからのPOSTリクエスト時のトークンチェックを無効にする

ここまででGETリクエスウトについては動くようになりますが、まだPOSTリクエスト時にのCSRFトークンのチェックに引っかかります。

これもswagger-ui上から適切なtokenを送るのは困難であるため無効化します。
VerifyCsrfToken.phpというファイルがあるので、handleをオーバーライドし、判定条件を変えます。
UnitTest時に無効にしてるのと挙動としては近いですね。

isSwaggerと同じ条件を書いてしまっているのは良くないですが、雑に作るとこうなります。

VerifyCsrfToken.php
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
    /**
     * @return bool
     */
    protected function runningSwaggerUi()
    {
        return app()->isLocal() && request()->headers->get('origin') === Cors::ACCESS_ALLOW_ORIGIN;
    }

    /**
     * @param \Illuminate\Http\Request $request
     * @param Closure $next
     * @return mixed|\Symfony\Component\HttpFoundation\Response
     * @throws TokenMismatchException
     */
    public function handle($request, Closure $next)
    {
        if (
            $this->runningSwaggerUi() ||
            $this->isReading($request) ||
            $this->runningUnitTests() ||
            $this->inExceptArray($request) ||
            $this->tokensMatch($request)
        ) {
            return $this->addCookieToResponse($request, $next($request));
        }

        throw new TokenMismatchException;
    }

最後に

判定処理が共通化できてないのと、Corsクラスの定数に依存してる辺りはなんとかしたいところです。その辺りは次の課題ということで。いい方法知ってる人がいたら教えてほしいです。
とりあえずswagger-uiからAPIを実行してjsonで結果を見る環境はこれでできました。

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