【備忘録】Laravel + Vueで作った認証フォームにバリデーション機能を加える

## バージョン等
PHP7.3
Laravel7.30
Vue2.6.12
[ローカル環境(ログイン機能まで実装したもの)](https://qiita.com/kaba_farm/items/9d3a41e7eb2455c07998)


## バリデーションルール

#### 新規登録フォーム

| カラム名 | ルール |
|:-:|:-:|
| name | 必須<br>2文字以上20文字以下 |
| email | 必須<br>メールアドレスの形式 |
| login_id | 必須<br>使える文字は半角英数字、ハイフン「-」、アンダーバー「_」、ピリオド「.」<br>8文字以上20文字以下 |
| password | 必須<br>大文字、小文字、数字をそれぞれ１種類以上含める<br>8文字以上20文字以下 |
| password_confirmation | 必須<br>**password**と一致が必要 |

#### ログインフォーム
| カラム名 | ルール |
|:-:|:-:|
| login_id | 必須 |
| password | 必須 |


## Laravel側

```php:app/Http/Controllers/Auth/RegisterController.php
protected function validator(array $data)
{
  return Validator::make($data, [
    'name' => ['required', 'string', 'min:2', 'max:20'],
    'login_id' => ['required', 'regex:/^[\w\.\-]+$/i', 'min:8', 'max:20', Rule::unique('users', 'login_id')->whereNull('deleted_at')],
    'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users', 'email')->whereNull('deleted_at')],
    'password' => ['required', 'string', 'regex:/^(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\d)[a-zA-Z\d]+$/', 'min:8', 'max:20', 'confirmed'],
  ]);
}
```

### 日本語対応

```php:config/app.php
'locale' => 'ja',
```
resources/langディレクトリ以下にjaディレクトリを作り、バリデーションメッセージに関するファイルを置いておく。
今回は[こちらのページ](https://github.com/minoryorg/laravel-resources-lang-ja)からダウンロードし、zip解凍後jaフォルダごとまるっと移す

```php:resources/lang/ja/validation.php
  // 追加
  'attributes' => [
    'login_id' => 'ログインID',
    'password' => 'パスワード',
    'name' => 'お名前',
    'email' => 'メールアドレス',
  ],

```

```php:resources/lang/ja/auth.php
'failed' => 'ログインIDまたはパスワードが一致しません。',
```

## Vue側

```bash:~/workspace/myapp/laravel
$ touch resources/ts/types/Form.ts
$ mkdir resources/ts/mixins
$ touch resources/ts/mixins/formValidator.ts
```

```ts:resources/ts/types/Form.ts
export type ValidateRule = (inputData: any) => boolean | string

export type Base = {
  name: string
  type: string
  rules: ValidateRule[]
  errorMessages: (string | boolean)[]
  placeholder?: string
  [k: string]: any
}

export type Form = {
  [k: string]: Base
}
```

```ts:resources/ts/mixins/formValidator.ts
import { Mixin, Component } from 'vue-mixin-decorator'
import { Vue, Watch } from 'vue-property-decorator'
import { Form, Base } from '@/types/Form'
import { ValidateRule } from '@/types/Form'
import { ErrorMessages } from '@/types/Error'
@Mixin
export default class FormValidator extends Vue {

  submitData: Form = {}

  getValidatedMessage(rules: ValidateRule[], formVal: string): (string | boolean)[] {
    return rules.map((rule: ValidateRule) => rule(formVal))
      .filter((res: string | boolean) => typeof res === 'string')
  }

  validation(formItem: Base): void {
    formItem.errorMessages = this.getValidatedMessage(formItem.rules, formItem.value)
  }

  get disabled(): boolean {
    return this.errorCount > 0
  }

  get errorCount(): number {
    const count: number[] = Object.keys(this.submitData).map((key: string) =>
    this.getValidatedMessage(this.submitData[key].rules, this.submitData[key].value).length
    )
    return count.reduce((prev, current, i, count) => prev + current)
  }

  get errMessages(): ErrorMessages {
    return this.$store.getters['error/messages']
  }

  @Watch('errMessages', { immediate: true })
  fetchMessage(val: ErrorMessages): void {
    if (val !== null) {
      Object.keys(val).forEach((key: string): void => {
        this.submitData[key].errorMessages = val[key]
      })
    }
  }

}
```

```vue:resources/ts/pages/Register.vue
<template>
<div class="container">
  <h1>会員登録</h1>
  <form @submit.prevent="register">
    <div v-for="(formItem, index) in submitData" :key="index">
      <p v-if="formItem.errorMessages">{{ formItem.errorMessages[0] }}</p>
      <input
        v-if="formItem.type==='text'"
        type="text"
        v-model="formItem.value"
        :placeholder="formItem.placeholder"
        @keyup="validation(formItem)"
        required
      >
      <input
        v-else-if="formItem.type==='email'"
        type="email"
        v-model="formItem.value"
        :placeholder="formItem.placeholder"
        @keyup="validation(formItem)"
        required
      >
      <input
        v-else-if="formItem.type==='password'"
        type="password"
        v-model="formItem.value"
        @keyup="validation(formItem)"
        :placeholder="formItem.placeholder"
        required
        >
    </div>
    <div>
      <button :disabled="disabled">会員登録</button>
    </div>
  </form>
</div>
</template>

<script lang="ts">
import { Component, Mixins} from 'vue-property-decorator'
import { Form } from '@/types/Form'
import FormValidator from '@/mixins/formValidator'

@Component
export default class Register extends Mixins(FormValidator) {

  submitData: Form = {
    name: {
      name: 'お名前',
      type: 'text',
      value: '',
      rules: [
        (val: string) => !!val || '必須項目です',
        (val: string) => (val.length >= 2 && val.length <= 20) || '2文字以上20文字以下で設定してください',
      ],
      errorMessages: [],
      placeholder: 'your nickname',
    },
    email: {
      name: 'メールアドレス',
      type: 'email',
      value: '',
      rules: [
        (val: string) => !!val || '必須項目です',
        (val: string) => !!val.match(/^\w+[\w\.]*\w+@\w+[\w\.]*\.\w+[\w\.]*[A-Za-z]+$/i) || 'メールアドレスの形式が正しくありません'
      ],
      errorMessages: [],
      placeholder: 'sample@example.com',
    },
    login_id: {
      name: 'ログインID',
      type: 'text',
      value: '',
      rules: [
        (val: string) => !!val || '必須項目です',
        (val: string) => !!val.match(/^[\w\.\-]+$/g) || '文字は半角英数字、「.」、「_」、「-」のみ指定できます',
        (val: string) => val.length >= 8 && val.length <= 20 || '8文字以上20文字以下で入力してください'
      ],
      errorMessages: [],
      placeholder: 'login ID',
    },
    password: {
      name: 'パスワード',
      type: 'password',
      value: '',
      rules: [
        (val: string) => !!val || '必須項目です',
        (val: string) => !!val.match(/^(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\d)[a-zA-Z\d]+$/) || '半角小文字、半角大文字、数字をそれぞれ1種類以上含めてください',
        (val: string) => (val.length >= 8 && val.length <= 20) || '8文字以上20文字以下で入力してください',
      ],
      errorMessages: [],
      placeholder: 'at leeast 8 chars.',
    },
    password_confirmation: {
      name: 'パスワード（確認用）',
      type: 'password',
      value: '',
      rules: [
        (val: string) => !!val || '必須項目です',
        (val: string) => this.submitData.password.value === val || 'パスワードが一致しません'
      ],
      errorMessages: [],
      placeholder: 'confirm',
    },
  }

  async register(): Promise<void> {
    if (this.disabled) {
      Object.keys(this.submitData).forEach((key: string): void => {
        this.validation(this.submitData[key])
      })
    } else {
      const formData = new FormData()
      formData.append('name', this.submitData.name.value)
      formData.append('email', this.submitData.email.value)
      formData.append('login_id', this.submitData.login_id.value)
      formData.append('password', this.submitData.password.value)
      formData.append('password_confirmation', this.submitData.password_confirmation.value)
      await this.$store.dispatch('auth/register', formData)
    }
  }

}
</script>
```

```vue:resources/ts/pages/Login.vue
<template>
<div>
  <h1>ログイン</h1>
  <form @submit.prevent="login">
    <div v-for="(formItem, index) in submitData" :key="index">
      <p v-if="formItem.errorMessages">{{ formItem.errorMessages[0] }}</p>
      <input
        v-if="formItem.type==='text'"
        type="text"
        v-model="formItem.value"
        :placeholder="formItem.placeholder"
        @keyup="validation(formItem)"
        required
      >
      <input
        v-else-if="formItem.type==='password'"
        type="password"
        v-model="formItem.value"
        @keyup="validation(formItem)"
        :placeholder="formItem.placeholder"
        required
      >
    </div>
    <div>
      <button :disabled="disabled">ログイン</button>
    </div>
  </form>
</div>
</template>

<script lang="ts">
import { Component, Mixins } from 'vue-property-decorator'
import { Form } from '@/types/Form'
import FormValidator from '@/mixins/formValidator'

@Component
export default class Login extends Mixins(FormValidator) {

  submitData: Form = {
    login_id: {
      name: 'login_id',
      type: 'text',
      value: '',
      rules: [(val: string) => !!val || '必須項目です'],
      errorMessages: [],
      placeholder: 'login_id',
    },
    password: {
      name: 'password',
      type: 'password',
      value: '',
      rules: [(val: string) => !!val || '必須項目です'],
      errorMessages: [],
      placeholder: 'at least 8 chars.',
    },
  }

  async login(): Promise<void> {
    if (this.disabled) {
      Object.keys(this.submitData).forEach((key: string): void => {
        this.validation(this.submitData[key])
      })
    } else {
      const formData = new FormData()
      formData.append('login_id', this.submitData.login_id.value)
      formData.append('password', this.submitData.password.value)
      await this.$store.dispatch('auth/login', formData)
    }
  }

}
</script>
```