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

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

DBが複数存在する場合のRefreshDatabaseトレイトを使ったテスト

はじめに

こんばんは。

Laravelでテスト書く際に 最初に必要なSeederを読み込んで 各テストを行いたい場合ってよくあると思いますが、その際は必ず RefreshDatabase トレイトをuseする必要があります。

そのさいDBが複数存在する場合に限りうまくいきません。

色々調べたのですが以下の例のように RefreshDatabase の処理をオーバライドする必要がありそうです。

qiita.com

本題

<?php

namespace Tests;

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

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

    protected string $seeder = TestCaseSeeder::class;

    protected array $connectionsToTransact = ['mysql', 'mysql2', 'mysql3'];

    ↓以下をオーバライド
    protected function refreshTestDatabase()
    {
        if (! RefreshDatabaseState::$migrated) {
            foreach ($this->connectionsToTransact() as $database) {
                $this->artisan('db:wipe', array_filter([
                    '--database' => $database,
                    '--drop-views' => $this->shouldDropViews(),
                    '--drop-types' => $this->shouldDropTypes(),
                    '--force' => true,
                ]));
            }

            $this->artisan('migrate', [
                '--seeder' => $this->seeder,
                '--force'  => true,
            ]);

            $this->app[Kernel::class]->setArtisan(null);

            RefreshDatabaseState::$migrated = true;
        }

        $this->beginDatabaseTransaction();
    }
}

db:wipeはDB数分実行が必要で、migrateに関してはmigrationsテーブルを作成するためにデフォルトの指定のみでOK。

終わりに

なんかもっと良さげな解決法ありそうですがね。。。。

なにか知ってたら誰か教えてください。

Laravelでファイルダウンロード

はじめに

こんばんは。

あんまりダウンロード機能とか作ってなかったのですが、久々にLaravelで作ったので備忘録っときます。

めちゃ短いですが...

本題

<?php

namespace App\Http\Controllers;

use App\Model\File;
use Illuminate\Http\Response;
use Illuminate\Routing\ResponseFactory;

class DownloadAction
{
    public function __construct(private readonly ResponseFactory $responseFactory) 
    {
    }

    public function __invoke(string $id): Response
    {
        $binary = Storage::get("{$id}");
        return $this->responseFactory->make(
            $binary,
            200,
            [
                'Content-Type' => 'octet-stream',
                'Content-Disposition' => 'attachment; filename="ダウンロードファイル"',
                'Content-Length' => strlen($binary)
            ]
        );
    }
}

ResponseFactoryのmakeで ヘッダーを生成してダウンロードできます。

終わりに

昔ながらの header関数 とかは使わなくてもいいって素敵ですね。

現場からは以上です。

phpでQRコード作成

はじめに

こんばんは。

QRコード生成ライブラリはJSでメインで使っていて、あとはGo言語で少しだけやった記憶があったんですが、今回PHPでも使う機会があったので、備忘録残しておきます。

本題

使用したのはこちら

github.com

今回のユースケースは、 Laravelでメール通知の際に特定のURLのQRコードを作成して、メールに添付して送りたいってことでした。

<?php

use Endroid\QrCode\QrCode;
use Endroid\QrCode\Writer\PngWriter;

$qrCode      = QrCode::create(route('users.answer'))->setSize(200);
$urlBase64 = (new PngWriter())
    ->write($qrCode)
    ->getDataUri();


dd($urlBase64);
    

これで対象のURLのQRコード画像のbase64が取れます。

あとは <img src='{$urlBase64}' /> とかしてあげれば表示されます。

らくー。

PngWriter以外にも PdfWriterや SvgWriterなどもあります。

用途で使い分けれるみたいです。

終わりに

phpではやったことなかった?んですが、JSと同じくめちゃ簡単でした。

現場からは以上です。

docker-composeでminioが起動したときにバケットを作る

はじめに

こんばんは。

minio使用する際にバケットがないと言われてイラッとしたので、調べてみました。

本題

方法1

  minio:
    image: minio/minio:latest
    ports:
      - '${FORWARD_MINIO_PORT:-9001}:9001'
      - '${FORWARD_MINIO_CONSOLE_PORT:-8900}:8900'
    environment:
      MINIO_ROOT_USER: root
      MINIO_ROOT_PASSWORD: password

    entrypoint: >
      /bin/sh -c "
      mkdir -p /data/minio/bucket1;
      mkdir -p /data/minio/bucket2;
      mkdir -p /data/minio/bucket3;
      /usr/bin/minio server /data/minio --console-address ':8900';
      exit 0;
      "
    deploy:
      restart_policy:
        condition: on-failure

上記のような感じでコマンドを実行してあげればデフォルトのバケットが作成されています。

方法2

  minio:
    image: minio/minio:latest
    ports:
      - 19000:9000
    volumes:
      - ./data/.minio/data:/export
      - ./test/.minio/config:/root/.minio
    data:
      MINIO_ROOT_USER: root
      MINIO_ROOT_PASSWORD: password
    command: server /export
  createbuckets:
    image: minio/mc
    depends_on:
      - minio
    entrypoint: >
      /bin/sh -c "
      /usr/bin/mc config host add myminio http://minio:9000 root password
      /usr/bin/mc rm -r --force myminio/bucket1;
      /usr/bin/mc mb myminio/bucket1;
      /usr/bin/mc policy download myminio/bucket1;
      exit 0;
      "

という感じで、 minio/mc コンテナを別で起動してそちらから minioコンテナへ向けてコマンドを打つことで作ることができます。

終わりに

ざっくりしたがこの2パターンがあるみたいです。

ちなみにボクは方法1でやってます。 今のところ問題は出てないです。

現場からは以上です。

laravelでファイルアップロードのテスト

はじめに

こんばんは。

今回もlaravelネタで簡単な備忘録です。

画像をアップロードするようなアクションのテストを行いたい場合の対応です。

本題

アップロードするアクションは以下のような感じだとします。

<?php

namespace App\Http\Controllers;

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

class UploadController extends Controller
{
    public function __invoke(UploadRequest $request) 
    {
        $request->file('image')->storeAs(
            '/path/to/uploaded.png',
            's3'
        );

        return redirect()->route('list');
    }
}

このテストは以下の通り

<?php

namespace Tests\Feature;

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;

class UploadControllerTest extends TestCase
{
    /**
     * @test
     */
    public function invoke_upload_success(): void
    {
        Storage::fake('s3');

        $image = UploadedFile::fake()->image('dummy.png');
        $response = $this->post(route('upload'), [
            'image' => $image,
        ]);

        $response->assertStatus(302);
        $response->assertRedirect(route('list'));
    }
}

重要なのは 2つ

Storage::fake('s3');

Storage::fakestorage/framework/testing/disks/ 以下にアップロードされるようになります。

UploadedFile::fake()->image('dummy.png');

UploadedFile::fake() でアップロード処理のダミーを再現できるようになります。

終わりに

結構アップロードのテストはめんどくさいから後回しにしがちですけど、めっちゃ簡単ですね。

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

laravelのuuid()をmockする

はじめに

こんばんは。

ちょっとまえに、laravelの uuid() を使用しているコードのテストを行う必要があったので、対応してみました。

簡単ですが備忘録です。

本題

uuid()のコード自体は Illuminate\Support\Str::uuid() を使ってます。

    /**
     * Generate a UUID (version 4).
     *
     * @return \Ramsey\Uuid\UuidInterface
     */
    public static function uuid()
    {
        return static::$uuidFactory
                    ? call_user_func(static::$uuidFactory)
                    : Uuid::uuid4();
    }

UuidFactoryInterfaceをimplementsした無名クラスとかでも良さそうなんですが、mockでテストのときに固定値を返すようにするほうが楽そう?なのでやってみました。

namespace Tests\Unit;

use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidFactory;
use Ramsey\Uuid\UuidFactoryInterface;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        /** @var UuidFactoryInterface $factory */
        $factory = $this->mock(
            UuidFactoryInterface::class,
            static fn ($mock) => $mock->shouldReceive('uuid4')->andReturn(
                Uuid::fromString('99999999-9999-9999-9999-999999999999')
            )
        );
        Uuid::setFactory($factory);
    }

    protected function tearDown(): void
    {
        parent::tearDown();
        Uuid::setFactory(new UuidFactory());
    }

    public function test_invoke(): void
    {
        dd(Uuid::uuid4()->toString());
        // "99999999-9999-9999-9999-999999999999"
    }
}

終わりに

こんな感じで上書きできました。

Illuminate\Support\Str をmockするのでもよさそうでしたねw

簡単ですが以上です。

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() を使います。

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

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

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

終わりに

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

現場からは以上です。