もがき系プログラマの日常

もがき系エンジニアの勉強したこと、日常のこと、気になっている技術、備忘録などを紹介するブログです。

Laravel Precognitionを試してみた

はじめに

こんばんは。

今回は Laravel Precognitionを試してみました。

readouble.com

試してみたいと思っててなかなか試せずいました。

本題

基本ドキュメントどおりに進めていきます。

自分がやっていたプロジェクトでは vue + inertiaをつかってたので、まずnpmでライブラリをインストールします。

コマンド

$ npm install laravel-precognition-vue-inertia

バックエンド側では HandlePrecognitiveRequests を使えるように対応します。

route.php

use App\Http\Controllers\UserStoreAction;
use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;

Route::post('/users', UserStoreAction::class)
  ->middleware([HandlePrecognitiveRequests::class]);

対象のコントローラはシンプルにバリデートが通ったやつを保存するだけです。

UserStoreAction.php

<?php

namespace App\Http\Controllers;

use App\Http\Requests\UserStoreRequest;

class UserStoreAction
{
    public function __invoke(UserStoreRequest $request)
    {
       $validatedRequest = $request->validated();
       User::create($validatedRequest);
    }
}

フォームリクエストはドキュメントの中身とほぼおんなじです。

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;

class UserStoreRequest extends FormRequest
{
    protected function rules()
    {
        return [
            'password' => [
                'required',
                Password::min(8)
            ],
        ];
    }
}

sample.vue

<script setup>
import { useForm } from 'laravel-precognition-vue-inertia';

const form = useForm('post', '/users', {
    password: '',
});

const submit = () => form.submit({
    onSuccess: () => form.reset(),
});
</script>
<template>
    <form @submit.prevent="submit">
        <label for="email">Password</label>
        <input
            id="password"
            type="password"
            v-model="form.password"
            @change="form.validate('password')"
        />
        <div v-if="form.invalid('password')">
            {{ form.errors.password }}
        </div>

        <button :disabled="form.processing">
            Create User
        </button>
    </form>
</template>

こんな感じでスクリプトは useFormを使うだけ。

あとは フォームのchangeイベントで form.validate() を使います。

これでリアルタイムバリデーションが再現できます。

ただ、だいぶ手前とはいえ、当然ながらバックエンドにバリデーション毎にリクエストが飛んでしまいます。

そこが懸念といえば懸念ですね..

終わりに

使う場所は結構慎重にならないとだめな印象でした。

現場からは以上です。

laravelのパスワードバリデーションのuncompromisedを初めて知った

はじめに

こんばんは。

恥ずかしながらシリーズで laravelのパスワードバリデーションの uncompromised を初めて知りました...

readouble.com

本題

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;

class StoreUserRequest extends FormRequest
{
    protected function rules()
    {
        return [
            'password' => [
                'required',
                 Password::min(8)->uncompromised(), ← これ
            ],
        ];
    }
}

このバリデーションはAPIを使って以下のサイトでパスワードの漏洩をチェックしています。

haveibeenpwned.com

コードはこちら

github.com

自分が試してみたパスワードでは漏洩は検知されなかったですが、簡単なパスワード「1111」とかでやると引っかかりました。

終わりに

こんな面白いバリデーションあるんですね...

知らなかった。

現場からは以上です。

Laravelのテストで初回にSeeder読み込みをしたい

はじめに

こんばんは。

またまたLaravelネタです。

RefreshDatabase$seedプロパティ or $seederプロパティ を使います。

公式にも記載があります。

readouble.com

本題

tests/TestCase.php

<?php

namespace Tests;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;
    use RefreshDatabase;

    protected string $seed = true;
}

$seedプロパティ を trueで指定すると Database\Seeders\DatabaseSeeder を実行してくれます。

<?php

namespace Tests;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;
    use RefreshDatabase;

    protected string $seeder = \Database\Seeders\TestCaseSeeder::class;
}

$seederプロパティ に 特定のSeederを指定することで対象のSeederを実行してくれます。

終わりに

コレ全然知らなかったです。。。

今までは BaseTestCaseの setupをオーバライドして、 if (! $this->app) { 内で 無理やり artisanコマンドでマイグレーション実行したりしてたので、これでめっちゃシンプルになりました。

現場からは以上です。

laravelのpaginateでgroupbyもしたい

はじめに

こんばんは。

今回もlaravelネタです。

paginateしつつデータをgroupbyしたいなってときがあって、それを調べた対応です。

本題

元データはこちらでやってみます。

$paginator = new \Illuminate\Pagination\Paginator(collect([
    ['userId' => 10,  'type' => 1, 'name' => 'AAA'],
    ['userId' => 20,  'type' => 2, 'name' => 'BBB'],
    ['userId' => 30,  'type' => 3, 'name' => 'CCC'],
    ['userId' => 40,  'type' => 1, 'name' => 'DDD'],
    ['userId' => 50,  'type' => 2, 'name' => 'EEE'],
    ['userId' => 60,  'type' => 3, 'name' => 'FFF'],
    ['userId' => 70,  'type' => 1, 'name' => 'GGG'],
    ['userId' => 80,  'type' => 2, 'name' => 'HHH'],
    ['userId' => 90,  'type' => 3, 'name' => 'III'],
    ['userId' => 100, 'type' => 1, 'name' => 'JJJ'],
    ['userId' => 110, 'type' => 2, 'name' => 'KKK'],
    ['userId' => 120, 'type' => 3, 'name' => 'LLL'],
    ['userId' => 130, 'type' => 1, 'name' => 'MMM'],
    ['userId' => 140, 'type' => 2, 'name' => 'NNN'],
    ['userId' => 150, 'type' => 3, 'name' => 'OOO'],
    ['userId' => 160, 'type' => 1, 'name' => 'PPP'],
    ['userId' => 170, 'type' => 2, 'name' => 'QQQ'],
    ['userId' => 180, 'type' => 3, 'name' => 'RRR'],
    ['userId' => 190, 'type' => 1, 'name' => 'SSS'],
    ['userId' => 200, 'type' => 2, 'name' => 'TTT'],
    ['userId' => 210, 'type' => 3, 'name' => 'UUU'],
    ['userId' => 220, 'type' => 1, 'name' => 'VVV'],
    ['userId' => 230, 'type' => 2, 'name' => 'WWW'],
    ['userId' => 240, 'type' => 3, 'name' => 'XXX'],
    ['userId' => 250, 'type' => 1, 'name' => 'YYY'],
    ['userId' => 260, 'type' => 2, 'name' => 'ZZZ'],
]), 5);

通常で取得するとこうなります。

dd($paginator->items());


/**
 * array:5 [▼
 *   0 => array:3 [▼
 *     "userId" => 10
 *     "type" => 1
 *     "name" => "AAA"
 *   ]
 *   1 => array:3 [▼
 *     "userId" => 20
 *     "type" => 2
 *     "name" => "BBB"
 *   ]
 *   2 => array:3 [▼
 *     "userId" => 30
 *     "type" => 3
 *     "name" => "CCC"
 *   ]
 *   3 => array:3 [▼
 *     "userId" => 40
 *     "type" => 1
 *     "name" => "DDD"
 *   ]
 *   4 => array:3 [▼
 *     "userId" => 50
 *     "type" => 2
 *     "name" => "EEE"
 *   ]
 * ]
 */

これをtypeでgroupbyしてみます。

$paginator->setCollection($paginator->groupBy('type'));
dd($paginator->items());


/**
 * array:3 [▼
 *   1 => Illuminate\Support\Collection {#1370 ▼
 *     #items: array:2 [▼
 *       0 => array:3 [▼
 *         "userId" => 10
 *         "type" => 1
 *         "name" => "AAA"
 *       ]
 *       1 => array:3 [▼
 *         "userId" => 40
 *         "type" => 1
 *         "name" => "DDD"
 *       ]
 *     ]
 *     #escapeWhenCastingToString: false
 *   }
 *   2 => Illuminate\Support\Collection {#1388 ▼
 *     #items: array:2 [▼
 *       0 => array:3 [▼
 *         "userId" => 20
 *         "type" => 2
 *         "name" => "BBB"
 *       ]
 *       1 => array:3 [▼
 *         "userId" => 50
 *         "type" => 2
 *         "name" => "EEE"
 *       ]
 *     ]
 *     #escapeWhenCastingToString: false
 *   }
 *   3 => Illuminate\Support\Collection {#1374 ▼
 *     #items: array:1 [▼
 *       0 => array:3 [▼
 *         "userId" => 30
 *         "type" => 3
 *         "name" => "CCC"
 *       ]
 *     ]
 *     #escapeWhenCastingToString: false
 *   }
 * ]
 */

重要なのは setCollectionで内部のitemsを上書きすることみたいです。

    /**
     * Set the paginator's underlying collection.
     *
     * @param  \Illuminate\Support\Collection  $collection
     * @return $this
     */
    public function setCollection(Collection $collection)
    {
        $this->items = $collection;

        return $this;
    }

終わりに

以外に簡単でした。

laravelのFormRequestバリデーションのテスト方法にはassertInvalidを使う

はじめに

こんばんは。

めちゃめちゃ簡単ですが、知らなかったので備忘録です。

本題

readouble.com

今までは assertSessionHasErrors を使って以下みたいに書いてました。

        $article = Article::factory()->create();
        $response = $this->actingAs(User::factory()->create())
            ->post(route('article.update', $article->id), [
                'name' => null,
            ]);

        $response->assertStatus(302);
        $response->assertRedirect(route('article.create'));
        $response->assertSessionHasErrors(['name' => '名前が入力されていません。']);

assertSessionHasErrors の部分が assertInvalid に書き換えできるみたいです。

        $article = Article::factory()->create();
        $response = $this->actingAs(User::factory()->create())
            ->post(route('article.update', $article->id), [
                'name' => null,
            ]);

        $response->assertStatus(302);
        $response->assertRedirect(route('article.create'));
        $response->assertInvalid(['name' => '名前が入力されていません。']); ← assertInvalidに書き換えができる

最近かな?と思ったら laravel8からできるみたいでした...

知らないことより知ったかぶりしないことが大事ということで。

終わりに

laravel結構使ってますがまだまだ知らないこと多いです。

簡単でしたが、現場からは以上です。

laravelのRedirectorのintendedメソッド

はじめに

こんばんは。

何気なく使ってた intended メソッドですが、ふわっと使ってたので、きちんとドキュメントよんでみました。

本題

readouble.com

Laravelのリダイレクタが提供するintendedメソッドは、認証ミドルウェアによってインターセプトされる前に、アクセスしようとしたURLへユーザーをリダイレクトします。目的の行き先が使用できない場合のために、このメソッドにはフォールバックURIが指定できます。

実際のメソッドはこちら

    /**
     * Create a new redirect response to the previously intended location.
     *
     * @param  mixed  $default
     * @param  int  $status
     * @param  array  $headers
     * @param  bool|null  $secure
     * @return \Illuminate\Http\RedirectResponse
     */
    public function intended($default = '/', $status = 302, $headers = [], $secure = null)
    {
        $path = $this->session->pull('url.intended', $default);

        return $this->to($path, $status, $headers, $secure);
    }

事前にセッションに保存している行き先へ飛ばそうとしているみたいです。

このセッションを保存してる場所は同クラス内の guest() メソッドでした。

    /**
     * Create a new redirect response, while putting the current URL in the session.
     *
     * @param  string  $path
     * @param  int  $status
     * @param  array  $headers
     * @param  bool|null  $secure
     * @return \Illuminate\Http\RedirectResponse
     */
    public function guest($path, $status = 302, $headers = [], $secure = null)
    {
        $request = $this->generator->getRequest();

        $intended = $request->isMethod('GET') && $request->route() && ! $request->expectsJson()
                        ? $this->generator->full()
                        : $this->generator->previous();

        if ($intended) {
            $this->setIntendedUrl($intended);
        }

        return $this->to($path, $status, $headers, $secure);
    }

guest() 自体はどこで呼ばれているかというと

ExceptionHandler クラス内で AuthenticationException が発生した際に実行される

unauthenticated() で呼ばれてます。

    /**
     * Convert an authentication exception into a response.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Illuminate\Auth\AuthenticationException  $exception
     * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse
     */
    protected function unauthenticated($request, AuthenticationException $exception)
    {
        return $this->shouldReturnJson($request, $exception)
                    ? response()->json(['message' => $exception->getMessage()], 401)
                    : redirect()->guest($exception->redirectTo() ?? route('login'));
    }

終わりに

以前全部のredirectで intended() を指定してたプロジェクトがあったので、使い方間違えてたんだなぁと思いました。

現場からは以上です。

おまけ

なんとか読書グセをつけようと、現在読んでいるのはこちら

1日2ページとか10分だけとか

毎日少しだけでもいいからと思いながら、頑張って読んでいます。

犯罪心理学者が教える子どもを乗ろう言葉・救う言葉を読んだ

はじめに

こんばんは。

積極的な勉強はまだ手つかずですが、少しでも机に向かって勉強する姿勢を取り戻すために、本を読むようにしています。

とりあえず今回読んだのはこちら。

犯罪等を犯した未成年の子たちをモデルケースに、子どもと親の関わり方で、危険な関わり方等を教えてくれます。

紹介されているモデルケースでは、親子の関わり方がおかしかった結果、窃盗や詐欺、果ては殺人未遂まで犯してしまった

という、かなり極端というか、特殊なケースでした。

ただ、節々で 「あぁ。こういう言い方自分もしちゃっているなぁ」

と思うことがありました。

気になるところを何点かピックアップしたいなと思います。

本題

子育ての方針を変える際は、子どもに黙って勝手に変えることはだめ

子どもが不信感をもたないように、今まではなぜだめだったか。

そしてこれからはどう変えるかをきちんと子どもに説明する事が大事ということでした。

「みんなと仲良くしなさい」という言葉に親の都合が隠れていないかを考える

みんなと仲良く協調性を合わせておけば、よけいなトラブルが生まれないから

など、親が安心するためにその言葉を利用していないかを考えなければならないようです。

また、「みんな仲良く」というのは大人である親でも実際にはできいないのに、それを子どもだけに求めることも子どもに不信感を与えてしまうようです。

「頑張って」ではなく「頑張ってるね」「頑張ったね」というプロセスを評価すべき

これは他の本でも読みましたが、結果を褒めることもありだが、大事なのはプロセスを評価することが重要ということでした。

特になるほどと感じたのは、「頑張って」という言葉は子どもの心身的状態によっては、貶していると捉えられる可能性もあるということでした。

結局はやはり、きちんと伝えることが大事ということです。

アンダーマイニング効果に気をつけるべき

達成感や満足感を得るために行っていたが報酬を受けた結果、「報酬を受けること」そのものが目的になり、結果として本来の内的な動機が失われてしまう心理状態のこと

たとえば

「次のテストで100点とったらゲーム買ってあげるね!」とかが該当するそうです。

これはまさに子どもにやってしまっていました。

親心てきに頑張った子供に対して報酬(ご褒美)をあげたくなるのですが、本人が頑張っていたプロセスの評価と、自分たち(親)がとっても嬉しいから、あなたに報酬を渡したいということをきちんと説明することが大事のようです。

原因追求ではなく希望の光を

子どもが何かを失敗して落ち込んでいるときに「◯◯が✗✗だったから間違えちゃったね」というような原因を追求することは意味がないということです。

親が原因を追求するのではなく、これは子どもが本来考えるべきことのようです。

「あなたはきっと大丈夫。次は必ずうまくいく」

など、本人を気持ちをあげる言葉をかけるべきということでした。

めっちゃ響きました。

親が先回りをせず、安全な範囲で失敗する経験を積ませる

ついつい自分も「これをやっちゃだめだよ」と先回りして子どもを誘導してしまっていました。

ただ、生命に関わることや、怪我するようなこと以外、子どもに任せてそして失敗しちゃうことが大事ということです。

失敗からの学びで本人は成長するからです。

終わりに

結構読み応えありました。

読み切るのもけっこう大変なくらい、気分的につらいんですが、まだもう少し技術的なものを勉強する姿勢が取れないので、少しずつ慣らしていきます。

多分次も技術書ではなく子育て本読むかも...

頑張って慣れて、技術系にも手が届くようにしたいです。

「やる気はやらないと出ない!」

現場からは以上です。