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));
}
}
コメント