Laravel (CSV・イベント)

Request

<?php

namespace Oms\Http\Requests\Admin;

use Illuminate\Validation\ValidationException;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\UploadedFile;

class CustomerCsvImportRequest extends FormRequest
{
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'csv' => 'required|file|mimes:csv,txt|max:10',
        ];
    }

    public function getCsv(): UploadedFile
    {
        $uploadedFile = $this->file('csv');
        if (!($uploadedFile instanceof UploadedFile)) {
            throw ValidationException::withMessages(['error' => 'CSVファイルを選択してください。']);
        }
        return $uploadedFile;
    }
}

Controller

    public function importCsv(CustomerCsvImportRequest $request): RedirectResponse
    {
        $uploadedFile = $request->getCsv();

        try {
            $importer = new CustomerCsvImporter($uploadedFile->getPathname());
            $importer->import();
        } catch (ValidationException $e) {
            return redirect()->back()->withInput()->withErrors($e->validator);
        }

        $this->session->flash('success', '顧客データをインポートしました');

        return redirect()->back();
    }

Usucase(importer)

<?php

declare(strict_types=1);

namespace Oms\Domain\CustomerRegistration;

use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use League\Csv\Reader;
use League\Csv\Statement;
use League\Csv\ResultSet;
use Oms\Domain\Front\CustomerMyPage\EditProfile\Entity\FullName;
use Oms\Events\CustomerCsvImportCompleted;
use Oms\Models\Address;
use Oms\Models\Customer;
use Oms\Models\Prefecture;

class CustomerCsvImporter
{
    /**
     * @var array<array{
     *     "メールアドレス": string,
     *     "勤務先名": string,
     *     "部署名": string,
     *     "職種": string,
     *     "名前(姓)": string,
     *     "名前(名)": string,
     *     "カナ(姓)": string,
     *     "カナ(名)": string,
     *     "郵便番号": string,
     *     "都道府県": string,
     *     "市区町村": string,
     *     "番地・ビル名": string,
     *     "電話番号": string,
     *     "メルマガ受信": string
     * }>
     */
    private array $records;

    private const EMAIL = 'メールアドレス';
    private const COMPANY = '勤務先名';
    private const UNIT = '部署名';
    private const JOB = '職種';
    private const LAST_NAME = '名前(姓)';
    private const FIRST_NAME = '名前(名)';
    private const LAST_NAME_KANA = 'カナ(姓)';
    private const FIRST_NAME_KANA = 'カナ(名)';
    private const POSTAL_CODE = '郵便番号';
    private const PREFECTURE = '都道府県';
    private const CITY = '市区町村';
    private const ADDRESS = '番地・ビル名';
    private const PHONE = '電話番号';
    private const MAILMAGA_FLAG = 'メルマガ受信';

    private const HEADER = [
        self::EMAIL,
        self::COMPANY,
        self::UNIT,
        self::JOB,
        self::LAST_NAME,
        self::FIRST_NAME,
        self::LAST_NAME_KANA,
        self::FIRST_NAME_KANA,
        self::POSTAL_CODE,
        self::PREFECTURE,
        self::CITY,
        self::ADDRESS,
        self::PHONE,
        self::MAILMAGA_FLAG,
    ];

    /**
     * CustomerCsvImport constructor
     *
     * @param string $pathName
     */
    public function __construct(
        string $pathName
    ) {
        $csvReader = Reader::createFromPath($pathName, 'r')->setHeaderOffset(0);
        $resultSet = (new Statement())->process($csvReader, self::HEADER);
        $this->records = $this->validate($csvReader, $resultSet);
    }

    private function validate(Reader $csvReader, ResultSet $resultSet): array
    {
        if (count(self::HEADER) !== count($csvReader->getHeader())) {
            throw ValidationException::withMessages(['missing_items' => '項目の数が不正です。']);
        }

        $records = [];
        foreach ($resultSet as $row => $record) {
            // validationのために入力文字列を小文字に変換する
            $record[self::EMAIL] = Str::lower($record[self::EMAIL]);

            $validator = Validator::make($record, [
                self::EMAIL => ['required', 'email', 'max:255'],
                self::COMPANY => ['max:255'],
                self::UNIT => ['max:255'],
                self::JOB => ['max:255'],
                self::LAST_NAME => ['required', 'max:255'],
                self::FIRST_NAME => ['required', 'max:255'],
                self::LAST_NAME_KANA => ['required', 'max:255'],
                self::FIRST_NAME_KANA => ['required', 'max:255'],
                self::POSTAL_CODE => ['max:255'],
                self::PREFECTURE => ['max:255'],
                self::CITY => ['max:255'],
                self::ADDRESS => ['max:255'],
                self::PHONE => ['numeric_dash', 'between:9,17'],
                self::MAILMAGA_FLAG => ['max:255'],
            ]);

            // エラーが発生した最初の行のみvalidatorを返す
            if ($validator->fails()) {
                $customValidator = Validator::make([], []);
                foreach ($validator->errors()->all() as $key => $message) {
                    $customValidator->errors()->add($key, $row . '行目:' . $message);
                }
                throw new ValidationException($customValidator);
            }
            $records[] = $record;
        }

        return $records;
    }

    public function import(): void
    {
        DB::transaction(function () {
            $prefectureNameToId = Prefecture::pluck('id', 'name');
            $jobIds  = array_flip(Customer::JOBS);
            $mailmagaFlgs  = array_flip(Customer::MAILMAGA_FLAGS);

            $customers = collect();
            foreach ($this->records as $record) {
                if (Customer::where('email', $record[self::EMAIL])->exists()) {
                    // すでに同じメールアドレスのユーザーが存在する場合は処理をスキップ
                    continue;
                }
                $customer = Customer::create([
                    'name' => (new FullName($record[self::LAST_NAME], $record[self::FIRST_NAME]))->getFullName(),
                    'password' => bcrypt(''),
                    'email' => $record[self::EMAIL],
                    'company' => $record[self::COMPANY],
                    'unit' => $record[self::UNIT],
                    'job' => $jobIds[$record[self::JOB]] ?? null,
                    'name01' => $record[self::LAST_NAME],
                    'name02' => $record[self::FIRST_NAME],
                    'kana01' => $record[self::LAST_NAME_KANA],
                    'kana02' => $record[self::FIRST_NAME_KANA],
                    'mailmaga_flg' => $this->getMailmagaFlag($mailmagaFlgs, $record),
                ]);
                Address::create([
                    'customer_id' => $customer->id,
                    'postcode' => $record[self::POSTAL_CODE],
                    'prefecture_id' => $prefectureNameToId[$record[self::PREFECTURE]] ?? null,
                    'city' => $record[self::CITY],
                    'line1' => $record[self::ADDRESS],
                    'tel' => $record[self::PHONE],
                ]);
                $customers->push($customer);
            }
            // 通常の会員登録は未認証会員を経て行われるが、csvインポートの場合直接会員を生成するので
            // それに伴って必要な処理を実行させる為
            event(new CustomerCsvImportCompleted($customers));
        });
    }

    /**
     * @param array $mailmagaFlgs
     * @param array $record
     * @return void
     */
    private function getMailmagaFlag(array $mailmagaFlgs, array $record): int
    {
        return $mailmagaFlgs[$record[self::MAILMAGA_FLAG]] ?? Customer::MAILMAGA_FLG_RECIEVE;
    }
}
Laravelでネストが深いバリデーションエラーメッセージを返す - Qiita
今回はLaravelのバリデーションのエラーメッセージの話です。Laravel で確認していますがでも同じだと思います。…

イベント(機能同士を直接依存させたくないとき)

eventオブジェクト

<?php

namespace Oms\Events;

use Illuminate\Queue\SerializesModels;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Support\Collection;
use Oms\Models\Customer;

class CustomerCsvImportCompleted
{
    use Dispatchable;
    use InteractsWithSockets;
    use SerializesModels;

    public Collection $customers;

    /**
     * Create a new event instance.
     *
     * @param Collection<Customer>
     * @return void
     */
    public function __construct(Collection $customers)
    {
        $this->customers = $customers->map(function ($customer) {
            // Collection<Customer>の型付けができないため、型チェックを行う
            if (!($customer instanceof Customer)) {
                throw new \InvalidArgumentException('All items in $customers must be instances of Customer.');
            }
            return $customer;
        });
    }
}

EventServiceProvider

<?php

namespace Oms\Providers;

use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        //
    ];

    /**
     * Register any events for your application
     *
     * @var array
     */
    protected $subscribe = [
        'Oms\Listeners\SendEmailToImportedCustomersListener',
        'Oms\Listeners\StoreSearchTargetPage',
        'Oms\Listeners\StoreSearchTargetPost',
        'Oms\Listeners\UserEventListener',
    ];
}

Listener

<?php

declare(strict_types=1);

namespace Oms\Listeners;

use Illuminate\Events\Dispatcher;
use Illuminate\Mail\Mailer;
use Illuminate\Support\Str;
use Oms\Events\CustomerCsvImportCompleted;
use Oms\UseCase\Admin\Mail\CusomerPasswordResetEmail;
use Illuminate\Support\Collection;
use Oms\Repositories\Interfaces\CustomerRepositoryInterface;

class SendEmailToImportedCustomersListener
{
    private Mailer $mailer;

    private CustomerRepositoryInterface $customerRepository;

    /**
     * @return void
     */
    public function __construct(
        Mailer $mailer,
        CustomerRepositoryInterface $customerRepository
    ) {
        $this->mailer = $mailer;
        $this->customerRepository = $customerRepository;
    }

    /**
     * Subscription
     *
     * @param Dispatcher $event
     */
    public function subscribe(Dispatcher $event)
    {
        $event->listen(
            CustomerCsvImportCompleted::class,
            'Oms\Listeners\SendEmailToImportedCustomersListener@onCreatedCustomers'
        );
    }

    /**
     * @param CustomerCsvImportCompleted $event
     */
    public function onCreatedCustomers(CustomerCsvImportCompleted $event): void
    {
        $customers = $event->customers;

        $this->sendEmailsTo($customers);
    }

    /**
     * @param Collection<Customer> $customers
     */
    private function sendEmailsTo(Collection $customers): void
    {
        foreach ($customers as $customer) {
            // 仮パスワード生成
            $newPassword = Str::random(13);
            $attributes['password'] = $newPassword;
            $this->customerRepository->update($customer, $attributes);

            $this->mailer->to($customer)->send(new CusomerPasswordResetEmail($customer, $newPassword));
        }
    }
}

テスト

<?php

namespace Tests\Unit\Domain\CustomerRegistration;

use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Illuminate\Validation\ValidationException;
use Oms\Domain\CustomerRegistration\CustomerCsvImporter;
use Oms\Events\CustomerCsvImportCompleted;
use Oms\Models\Address;
use Oms\Models\Customer;
use PrefectureSeeder;

class CustomerCsvImporterTest extends TestCase
{
    use RefreshDatabase;

    private $importer;

    public function setUp(): void
    {
        parent::setUp();
        $this->seed(PrefectureSeeder::class);
    }

    /**
     * @test
     */
    public function CSVを読み込めて、中のデータがバリデーションを通過する()
    {
        try {
            new CustomerCsvImporter(__DIR__ . '/csv/customers.csv');

            $this->assertTrue(true);
        } catch (ValidationException $e) {
            $this->fail('バリデーションエラー');
        }
    }

    /**
     * @test
     */
    public function DBに値が保存されている()
    {
        $importer = new CustomerCsvImporter(__DIR__ . '/csv/customers.csv');

        // import()内で発火するイベントをモック
        Event::fake([CustomerCsvImportCompleted::class]);
        $importer->import();

        $this->assertDatabaseHas('customers', [
            'email' => 'sample0@example.com',
        ]);
        $this->assertDatabaseHas('addresses', [
            'line1' => '111',
        ]);
        $this->assertCount(4, Customer::get()->toArray());
        $this->assertCount(4, Address::get()->toArray());
    }

    /**
     * @test
     */
    public function 項目数が異なる場合、期待するメッセージが例外に含まれること()
    {
        try {
            new CustomerCsvImporter(__DIR__ . '/csv/missing_items_customers.csv');
        } catch (ValidationException $e) {
            $messages = $e->validator->messages('missing_items');

            $this->assertTrue(in_array('項目の数が不正です。', $messages->get('missing_items'), true));
        }
    }

    /**
     * @test
     */
    public function 必須の値がない場合、期待するメッセージが例外に含まれること()
    {
        try {
            new CustomerCsvImporter(__DIR__ . '/csv/invalid_value_customers.csv');
        } catch (ValidationException $e) {
            $messages = $e->validator->messages();

            $this->assertTrue(in_array('4行目:メールアドレスは必ず指定してください。', $messages->get(0), true));
            $this->assertTrue(in_array('4行目:名前(姓)は必ず指定してください。', $messages->get(1), true));
        }
    }
}

フォームリクエスト

<?php

namespace Tests\Unit\Requests\Admin\Customer;

use Illuminate\Http\Testing\File;
use Illuminate\Support\Facades\Validator;
use Oms\Http\Requests\Admin\CustomerCsvImportRequest;
use Tests\TestCase;

class CustomerCsvImportRequestTest extends TestCase
{
    /**
     * @test
     */
    public function バリデーションが通る()
    {
        $request = new CustomerCsvImportRequest();
        $uploadedFile = new File(
            'customers.csv',
            fopen(__DIR__ . '/csv/customers.csv', 'r')
        );
        $request->merge(['csv' => $uploadedFile]);

        $rules = $request->rules();

        $validator = Validator::make($request->all(), $rules);
        $this->assertTrue($validator->passes());
    }

    /**
     * @test
     */
    public function ファイルが存在しない場合、バリデーションエラーが出る()
    {
        $request = new CustomerCsvImportRequest();
        $uploadedFile = '';
        $request->merge(['csv' => $uploadedFile]);

        $rules = $request->rules();

        $validator = Validator::make($request->all(), $rules);
        $message = $validator->messages();

        $this->assertTrue(in_array('csvファイルは必ず指定してください。', $message->get('csv'), true));
    }
}

コメント

タイトルとURLをコピーしました