テクメモ

備忘録

Vue3 Composition APIのReactivity APIまとめ

vue3のComposition APIのrefとreactiveについて調べた際に、他のReactivity APIについても気になったので今回まとめました。
自分が理解できた箇所のみをまとめた浅い記事ですがご容赦ください。

reactive

オブジェクトをリアクティブなProxyオブジェクトにして返す関数です。
この際のリアクティブへの変換はディープコピーのようですが、返されるプロキシは元のオブジェクトと同一ではないようです。

const obj = reactive({count: 1})
console.log(obj.count) // 1

obj.count++

console.log(obj.count) // 2

ref

プリミティブな値をアクティブで可変なrefオブジェクトを返す関数です。
refオブジェクトにはvalueというプロパティがあり、これによってアクセスできるようです。

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

computed

ゲッター関数を受け取り、ゲッターからの戻り値に対して不変のリアクティブなrefオブジェクトを返す関数です。

const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2
count.value++
console.log(plusOne.value) // 3

plusOne.value++ // error

ゲッター関数とセッター関数を使えば、書き込み可能なrefオブジェクトを作成することもできます。

const count = ref(1)
const plusOne = computed({
  get: () => count.value + 100,
  set: val => {
    count.value = val - 1
  }
})

console.log(plusOne.value) // 101
plusOne.value = 100
console.log(count.value) // 99

readonly

オブジェクトまたはrefを受け取り、読み取り専用プロキシを返す関数です。
ネストされたプロパティも読み取り専用になるようです。

const original = reactive({ count: 0 })
const copy = readonly(original)

original.count++
console.log(copy.count) // 1

// 読み取り専用だから書き換えられない
copy.count++ // error

watchEffect

依存関係を事後的に追跡しながら関数をすぐに実行し、依存関係が変更されたときに関数を再実行します。

const count = ref(0)

watchEffect(() => console.log(count.value)) // 0

setTimeout(() => {
  count.value++ // 1
}, 100)

watch

Vue2でのwatchとほぼ変わらないようです。

const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count) => {
    sampleFunction(count)
  }
)

複数を監視対象にすることもできるようです。

watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})

unref

引数がrefの場合は内部値を返し、それ以外の場合は引数自体を返す関数です。

const refSample = ref(100)
const notRefSample = 10

unref(refSample) // 100
unref(notRefSample) // 10

toRef

リアクティブなオブジェクトのある特定のプロパティをrefオブジェクトに変換する関数です。

const state = reactive({
  hoge: 1,
  fuga: 2
})

const sampleRef = toRef(state, 'hoge')

sampleRef.value++
console.log(state.hoge) // 2

state.hoge++
console.log(sampleRef.value) // 3

toRefs

リアクティブなオブジェクトの全てのプロパティをrefオブジェクトに変換する関数です。

const state = reactive({
  hoge: 1,
  fuga: 2
})

const sampleRefs = toRefs(state)

state.hoge++
console.log(sampleRefs.hoge.value) // 2

sampleRefs.hoge.value++
console.log(state.hoge) // 3

sampleRefs.fuga.value++
console.log(state.fuga) // 3

isRef

値がrefオブジェクトであるかどうかを確認する関数です。

const sampleRef = ref(1)
const sampleReactive = reactive({hoge: 1})
const sample = 1

console.log(isRef(sampleRef)) // true
console.log(isRef(sampleReactive)) // false
console.log(isRef(sample)) // false

isReactive

値がreactiveによって作成されたProxyオブジェクトであるかどうかを確認する関数です。
readonlyによってラップしたreactiveオブジェクトでもtrueを返すようです。

const sampleRef = ref('hoge');
const sampleReactive = reactive({
  fuga: 'fuga'
});
const sample = 'piyo'
const sampleRefAsReadonly = readonly(sampleRef);

isReactive(sampleRef) // false
isReactive(sampleReactive) // true
isReactive(sample) // false
isReactive(sampleRefAsReadonly) // true

isReadonly

値がreadonlyかどうかを確認する関数です。

const sampleRef = ref("hoge");
const sampleReactive = reactive({
  fuga: "fuga"
});
const sampleReadonly = readonly(sampleReactive);

isReadonly(sampleRef) // false
isReadonly(sampleReactive) // false
isReadonly(sampleReadonly) // true

customRef

更新のトリガーを明示的に制御し、カスタムしたrefオブジェクトを返すことができる関数らしいです。

const outputLogRef = (value: string) => {
  return customRef((track, trigger) => ({
    get() {
      console.log(value)
      track()
      return value
    },
    set(newValue: string) {
      console.log(newValue)
      value = newValue
      trigger()
    }
  }))
}

const hoge = outputLogRef("")

hoge.value = "hoge" // hogeが出力される
hoge.value = "fuga" // fugaが出力される
const fuga = hoge.value // fugaが出力される

markRaw

オブジェクトをマークして、reactiveなProxyオブジェクトに変換しないようにする関数らしいです。

const hoge = markRaw({})
console.log(isReactive(reactive(hoge))) // false

shallowReactive

ネストされたプロパティ以外をリアクティブにして返す関数です。

const state = shallowReactive({
  hoge: 1,
  fuga: {
    piyo: 2
  }
})

// これはリアクティブ
isReactive(state.hoge)
// ネストされたプロパティはリアクティブじゃない
isReactive(state.fuga) // false

shallowReadonly

ネストされたプロパティ以外をreadonlyにして返す関数です。

const state = shallowReadonly({
  hoge: 1,
  fuga: {
    piyo: 2
  }
})

// これはreadonly
isReadonly(state.hoge)
// ネストされたプロパティはreadonlyじゃない
isReadonly(state.fuga) // false

toRaw

reactiveまたはreadonlyオブジェクトの元のオブジェクトを返す関数です。

const sample = {}
const reactiveSample = reactive(sample)

console.log(toRaw(reactiveSample) === sample) // true

終わりに

Compotion APIのReactivity APIをまとめてみました。
Vue2のComposition APIプラグインではまだ対応しておらず、全く見たことないものや
いつ使うのか全く分からないものもありましたが、適宜用途に合わせて使っていきたいと思います。

参考文献

API Reference | Vue Composition API

CompositionAPIのwatchとwatchEffectの違い - Qiita

Vue3 Composition APIのrefとreactiveをざっくりと理解する

導入

今週、社内でVue3についての勉強会がありました。
その中で、一番気になったComposition APIのrefとreactiveについて調べることにしました
自分が理解できた箇所のみをまとめた浅い記事ですがご容赦ください。

リアクティブとは?

refとreactiveの話をする前に、リアクティブとは何かということについて理解する必要があります。
リアクティブとは、「ある変数を書き換えた際に既に定められた関係性によって、他の変数が更新される / 事前に定めた動作が実行される」だと僕は理解しています。

簡単な例だと以下になります。

const sample = reactive({a: 1})
const sample2 = computed(() => sample.a + 100)

console.log(sample2.value) // 101

sample.a = 10

console.log(sample2.value)// 110

リアクティブなデータなので、sample.aを書きかえた際にsample2が更新されています。

refとreactiveの基本

ref

プリミティブな値をアクティブで可変なrefオブジェクトを返す関数です。 refオブジェクトにはvalueというプロパティがあり、これによってアクセスできるようです。

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

template内で使う場合は、refがレンダーコンテキストのプロパティとして返されるようで、valueプロパティでアクセスする必要はないようです。

<template>
  <div>{{ count }}</div>
</template>

<script>
  export default {
    setup() {
      return {
        count: ref(0)
      }
    }
  }
</script>

reactive

オブジェクトをリアクティブなProxyオブジェクトにして返す関数です。 この際のリアクティブへの変換はディープコピーのようですが、返されるプロキシは元のオブジェクトと同一ではないようです。

    const obj = reactive({count: 1})
    console.log(obj.count) // 1

    obj.count++

    console.log(obj.count) // 2

refとreactiveの違い

リアクティブ化できる値

reativeはプリミティブな値はリアクティブにできませんが
refは、プリミティブな値以外(配列やオブジェクト等)でもリアクティブにできるようです。

// reactiveはプリミティブな値をリアクティブにすることはできない
    const number = reactive(0)
    console.log(number) // undefined
    
// refはプリミティブな値以外(配列やオブジェクト等)でもリアクティブにできる
    const str = ref('hoge')
    const array = ref([1, 2, 3, 4])
    const obj = ref({a:1, b:1})

    console.log(isRef(str)) //true
    console.log(isRef(array)) //true
    console.log(isRef(obj)) //true

    console.log(obj.value.a) // 1
    obj.value.a = 100
    const sample =  obj.value.a = 100 + 1
    console.log(sample) // 101

リアクティブの消失

reactiveでリアクティブにしたデータを、リアクティブのまま分割して使うことは出来ないようです。 文章だとわかりにくいので、以下にサンプルのコードを書きます

count.ts(reactiveなオブジェクトをセットする関数)

import { reactive } from '@vue/composition-api'

export function setCountReactive() {
  const obj = reactive({
    count: 1
  })
  return obj
}

home.vue

<template>
  <div>
    <button @click="incrementA()">足す</button>
    {{ obj.count }}
    <button @click="incrementB()">足す</button>
    {{ count }}
  </div>
</template>

<script lang="ts">
import { setCountReactive } from './count'

export default {
  setup() {
    /// 通常パターン
    const obj = setCountReactive()

    function incrementA() {
      return obj.count++
    }

    // リアクティブが消失してしまうパターン
    let { count } = setCountReactive()

    function incrementB() {
      return count++
    }

    return {
      incrementA,
      incrementB,
      obj,
      count,
    }
  }
}
</script>

こういう場合に、リアクティブに扱いたい場合はToRefsを仕様して、refでラップしてあげればいいようです。

count.ts(reactiveなオブジェクトをセットする関数)

import { reactive, toRefs } from '@vue/composition-api'

export function setCountReactive() {
  const obj = reactive({
    count: 1
  })
  return toRefs(obj)
}

ただし、refでラップする場合はvalueプロパティでアクセスする必要があります。
home.vue

<template>
  <div>
    <button @click="incrementA()">足す</button>
    {{ obj.count }}
    <button @click="incrementB()">足す</button>
    {{ count }}
  </div>
</template>

<script lang="ts">
import {setCountReactive} from './count'

export default {
  setup() {
    /// 通常パターン
    const obj = setCountReactive()

    function incrementA() {
      return obj.count.value++
    }

    // リアクティブが消失してしまうパターン
    let {count} = setCountReactive()

    function incrementB() {
      return count.value++
    }

    return {
      incrementA,
      incrementB,
      obj,
      count,
    }
  }
}
</script>

refとreactiveの使い分け

ここまで調べて、refとreactiveはどのように使い分けるのか?という疑問がわきました。
まだ有用な使い分けの仕方はわかりませんが、refとreactiveの違いでも書いたように
プリミティブな値を使う場合はref、それ以外の場合はreactiveを使っておけば間違いはないのかなと思います。
ただ、「オブジェクトのある特定のプロパティだけリアクティブに扱いたい」などの場合はreactiveでtoRefsを使えばいいようです。

終わりに

今回は、refとreactiveについて書きました。
Vue3での変更点や便利そうな新機能など、まだまだキャッチアップできていない情報がたくさんあるので 今後も機会があれば、アウトプットしていきたいと思います。

参考

API Reference | Vue Composition API

Ref vs Reactive Vue3 Composition APIのリアクティブ関数の探究 / ref vs reactive Vue Composition API Deep in - Speaker Deck

Object.assignの挙動で勘違いしていたこと

知ってる人からしたら、なにをいまさらという感じかもしれませんが Object.assignの挙動に関して勘違いしていたことがあり、業務中にハマったので備忘録程度に書きます。

勘違い1:Object.assignの返り値

Object.assignの返り値について勘違いしていました。 引数で渡したオブジェクトを合成した新しいオブジェクトを返す

のではなく

第2引数以降に渡したオブジェクトを、第1引数に渡したオブジェクトに合成して返す

ものでした。 MDNにも

Object.assign() メソッドは、すべての列挙可能なプロパティの値を、1つ以上のコピー元オブジェクトからコピー先オブジェクトにコピーするために使用されます。戻り値としてコピー先オブジェクトを返します。

Object.assign() - JavaScript | MDN

と書いてありました。

よって、以下のように

const obj1 = { a: 1, b: 2 };
const obj2 = { b: 4, c: 5 };

const obj3 = Object.assign(obj1, obj2);

console.log(obj1); // { a: 1, b: 4, c: 5 }

console.log(obj3); // { a: 1, b: 4, c: 5 }

obj1もobj3も中身が書き換わった状態になっています。

勘違い2:引数で渡したオブジェクトがネストされているときの挙動

Object.assignはシャローコピーのため、第2引数以降に渡したオブジェクトがネストされていた場合、オブジェクト参照がコピーされるようです。

const obj1 = {a:1, b:2};
const obj2 = {b:3, c:4, d:{a:5, b:6}};   

Object.assign(obj1, obj2);

console.log(obj1);  // {a:1, b:3, c:4, d:{a:5, b:6}}

obj1.d.a = 999;  // obj1.d.aの値を更新する

console.log(obj1);  // {a:1, b:3, c:4, d:{a:999, b:6}}
console.log(obj2);  // {b:3, c:4, d:{a:999, b:6}} →オブジェクト参照がコピーされているため、obj2.d.aの値が変わっている

コピー先の変更についてコピー元に影響を及ぼしたくないときは ディープコピーしたオブジェクトをコピー元として渡すなりすればよさそうです。

おわりに

初歩的な内容にはなりましたが、このような勘違いしていたこと、業務中ハマった内容は 小さなことでも書いていきたいと思います。

参考

developer.mozilla.org

microCMS使ってみた

今回は以前から気になっていた、日本製Headless CMSであるmicroCMSを触ってみたのでそのことについて書きます

Headles CMSとは

Head (ビュー = 表示画面) less(ない) CMS(コンテンツ管理システム)で、表示画面がないCMSのことです。
今までのCMSは表示画面とコンテンツ管理が合わさったものでしたが、HeadlessCMSは表示画面が存在しないので、自分で自由に作成することができます。
また、サイトのある一部分のみコンテンツ管理することも可能です。 Contentfulなどが有名だと思います。

microCMSとは

導入でも書きましたが、microCMSは日本製のCMSです。
ドキュメントや、サポートまですべて日本語対応しているため、初めてHeadlessCMSを触ってみる場合には取り扱いやすいと思います。
また、完全に主観ですが管理画面が分かりやすいため、エンジニアだけでなくコンテンツを管理する編集者も使いやすいと思います。

実際に使ってみる

それでは実際にブログ記事を管理する想定で使ってみます。
アカウント作成などは省きます。

APIの作成

まずはAPIを作ります。
API名とエンドポイントを指定します。 f:id:ryonnsui1201:20200528230033p:plain

APIで取得できるデータの形式を指定します。
今回は、記事一覧を取得するAPI作成するためリスト形式にします。 f:id:ryonnsui1201:20200528232932p:plain

APIで管理する各フィールドを作成します。
シンプルに

  • 記事タイトル
  • 記事内容
  • 投稿日時

を作成しました。

f:id:ryonnsui1201:20200528233356p:plain

フィールドの種類はたくさんあるため、コンテンツの種類によって選択します。 f:id:ryonnsui1201:20200528233752p:plain

フィールドに対して、複数のコンテンツを参照させることができます。
例えば、記事に対して複数タグなどをつけたい場合は、タグを返すAPIを作成しコンテンツを公開することで、記事一覧のAPIのタグのフィールドに複数のコンテンツを参照させることができます。
f:id:ryonnsui1201:20200529005010p:plain

コンテンツ作成

APIは作成したので、コンテンツを作成します。
先程作成した、タグも複数紐付けることができています。
コンテンツは、予約公開することもできます。 f:id:ryonnsui1201:20200529005724p:plain

APIプレビューで、APIのレスポンスを簡単にみることができます。 f:id:ryonnsui1201:20200529010307p:plain

画面での表示

画面で表示させます。

APIキーを保護する

APIキーをenvファイルで保護します。

.envにAPIキーを記述

API_KEY=key

記事一覧ページ

記事を一覧表示するだけのページを作ります tailwindcssを使っています。

<template>
  <div class="max-w-screen-lg mx-auto mt-5 flex flex-col items-center">
    <div>
      <article-card
        v-for="article in articles"
        :key="article.id"
        :article="article"
      />
    </div>
  </div>
</template>

<script>
import axios from 'axios'
import ArticleCard from '~/components/ArticleCard.vue'

export default {
  components: {
    ArticleCard
  },
  async asyncData() {
    const { data } = await axios.get(
      'https://bloglog.microcms.io/api/v1/blogs',
      {
        headers: { 'X-API-KEY': process.env.API_KEY }
      }
    )
    return {
      articles: data.contents
    }
  },
  data() {
    return {
      articles: [],
    }
  }
}
</script>

記事タイトル、投稿日、タグ、記事の内容が表示されていることが確認できました。 f:id:ryonnsui1201:20200529022345p:plain

さいごに

紹介の部分でも書きましたが、microCMSは日本語のドキュメントが充実しているので、扱いやすいです。
早いペースで新機能が追加されたり、改善されているため今後もっと使いやすくなっていくのでは、と思います。

参考

microcms.io

microcms.io

LaraveのEloquentについて

LaravelのEloquentとは?

EloquentはActive RecordライクなORMで、DBとモデルを関連付けてデータ操作をすることが可能。

※Active Recordとは Active RecordはDBからデータを読み出すための手法。DBのtableまたはviewの1行が1つのクラスにラップされ、オブジェクトのインスタンスがそのデータベースの1つの行に結合される。SQLを意識せずにデータベースアクセスを行うことができる。

モデルの作成

Eloquentを利用するにはモデルが必要なのでモデルを作成する

php artisan make:model Task

モデルとDBのテーブルとの関連付けは、テーブル名を複数形、モデルを単数形で作成すると暗黙的に関連付けられる。

例) テーブル名 → tasks モデル名 → Task

テーブル名がスネークケースの場合は、モデルをキャメルケースで作成すると関連付けられる。

例) テーブル名 → task_sample モデル名 → TaskSample

上記2つを適用しない場合は、$tableプロパティで指定し関連付けることができる。

データ検索/更新などの基本メソッド

よく使う基本的なメソッドを以下にまとめる。

全件抽出 all

テーブルの全レコードを取得するメソッド 戻り値はCollectionで、Collectionの要素はModelクラスのインスタンス

$tasks = Task::all();

PrimaryKey指定での抽出 find,findOrFail

find

引数にPrimaryKeyを指定して、合致するレコードのみ取得する。 戻り値はModelのインスタンス

$task = Task:find(10);

findOrFail

findと似てるが、該当レコードが見つからなかった場合、ModelNotFoundExceptionを投げる

try {
       $task = Task::findOrFail(10);
} catch (ModelNotFoundException $e) {
        // 見つからなかった場合の処理
}

条件指定による抽出 whereXX

SQLのwhere区に相当する条件を引数に指定し、絞り込みを行えるメソッド。 XXにはテーブルのカラム名が入る。

$task = Task::whereTitle('タスクタイトル')->get();

レコードの登録 create save firstOrCreatre

create

配列を引数に指定して、レコードを登録できる。

Task::create([
    'title'  =>  'タスクタイトル',
    'content'  =>  'タスクの内容'
]);

save

対象のモデルのインスタンスを新規作成し、各カラムの値を設定し登録できる。

$task = new Task;
$task->title = 'タスクタイトル';
$task->content = 'タスクの内容';

firstOrCreatre

ある条件でレコードを抽出し、レコードが見つからない場合のみ新規登録する。

$task = Task::firstOrCreatre(['title' => 'タスクタイトル']);

レコードの更新 update

更新対象のインスタンスに対して、更新したいカラムと値の配列を引数に指定しレコードを更新できる。

$task = Task::find(1)->update(['title' => 'タスクタイトル2']);

レコード削除 delete, destroy

delete

削除対象のインスタンスに使用し、レコードを削除できる。

$task = Task::find(1);
$task->delete();

destroy

削除対象のPrimaryKeyを指定し、レコードを削除する。

Task::destroy(1);

関連があるテーブル群をまとめて操作する

DBのテーブルは他のテーブルと関連していることが多い。 例えば、ユーザー(users)は複数のタスク(tasks)を持っている、など。 Eloquentはテーブルの関係性を踏まえて処理することができるリレーションという機能を持っっている。 以下はユーザー(users)、タスク(tasks)、カテゴリー(categories)の例で記述していく。

1対1 hasOne, belongsTo

例)書籍(books)テーブルと書籍詳細(book_details)テーブル

第1引数に関連付けるモデル名、第2引数に内部キー、第3キーに外部キーを指定することができる。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    public function detail()
    {
        return $this->hasOne(BookDetail::class);
    }
}

逆はbelongsToで指定することができる。

1対多 hasMany

例)ユーザー(users)テーブルとタスク(tasks)テーブル

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    public function tasks()
    {
        return $this->hasMany(Task::class);
    }
}

hasOneと同様に、第1引数に関連付けるモデル名、第2引数に内部キー、第3キーに外部キーを指定することができる。

逆は、belongsToで指定できる。

多対多 belongsToMany

例)タスク(tasks)テーブルテーブルとカテゴリ(categories)テーブル

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
    public function categories()
    {
        return $this->belongsToMany(Category::class);
    }
}

hasOneと同様に、第1引数に関連付けるモデル名、第2引数に内部キー、第3キーに外部キーを指定することができる。

逆もbelongsToManyで指定できる。

多対多では、結果コレクションに中間テーブル情報が付与されてくる。 これを取得したくないときは、モデルに$hiddenを定義することで除外できる。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
    // 中間テーブル情報を取得しない
    protected $hidden = ['pivot'];

    public function categories()
    {
        return $this->belongsToMany(Category::class);
    }
}

中間テーブルのカラムを取得したい場合はモデルに取得したいカラムを指定しておき、取得時にpivotプロパティにアクセスする。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
    // 中間テーブル情報を取得しない
    protected $hidden = ['pivot'];

    public function categories()
    {
        return $this
            ->belongsToMany(Category::class, 'task_category_ref')
            ->withPivot('task_id', 'category_id')
    }
}

Has Many Through

テーブルをまたぐリレーションを行うことができる。 以下の例で考える。

        users
    ⇅          ⇅
  roles       tasks

rolesからusersを経由してtasksを取得したい時

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    public function tasks()
    {
        return $this->hasManyThrough(Task::class, User::class);
    }
}

第1引数にリレーションを行うモデル、第2引数には経由するモデルを指定する。

PHP勉強会に参加してきました!

昨日、PHP勉強会 #141に参加してきたので参加レポート的なものを書こうと思います。 PHP勉強会に参加するのは2回目です。

会場

f:id:ryonnsui1201:20190829103748j:plain

いつもどおり、会場はGMO Yoursです。

タイムテーブル

Time title
19:00 会場・飲み物の配布・名刺交換
19:30 オープニング
19:35 自己紹介タイム
20:10 メイン発表枠①
20:30 懇親会前半
21:00 メイン発表枠②
21:20 LT枠

タイムテーブルは大体こんな感じだったと思います。 始めて参加した時も感じましたが、自己紹介でお題を与えられそれに答えたり、発表の途中で懇親会を挟んだりすることで非常に和やかで楽しい雰囲気な勉強会になっていると思います。

ちなみに、今回の自己紹介のテーマは「この夏にハマったことは?」でした。

発表

いくつか自分が気になった発表をピックアップして紹介したいと思います。

PHP始めて1年、レガシーシステムにどう立ち向かっているか @mizuki_r

speakerdeck.com

一番最初の発表はメイン発表枠の@mizuki_rさん。 自社プロダクトのビジネスモデルと乖離したレガシーシステムにどう立ち向かっているかというお話でした。 特に、ビジネス側が想像しているシステム、開発側が想像しているシステム、実際に動いているシステムがバラバラで、温度感スケジュールが合わないという話は非常に共感しました(前職で同じようなことがあったので)

P++とは何だったのか @tadsan

www.pixiv.net

メイン発表枠2番目は@tadsanさん。 最近話題になったP++について、経緯と事の真相についてのお話でした。 話の主軸とは関係ないのですが、個人的には今当たり前のように使っているPHPの言語仕様がどのバージョンで追加されたのか。 バージョンアップによる言語仕様の変更がどのように行われているかを知れたのが学びになりました。

※@tadsanさんが書いたこちらのQiitaの記事もとても勉強になります。 qiita.com

ひとりで画面数1,000以上のSaaSを開発した方法 @Toshiyuki Tanakaさん

speakerdeck.com LT枠トップバッター、EC事業者向けの受注管理、物流倉庫向けの倉庫管理を統合し1つのシステムにしたサービスを提供している@Toshiyuki Tanakaさんの発表でした。 Webエンジニアが1人しかいない状況で、いかに開発していくかというお話でした。 UIを徹底的にパターン化しているというお話がとても興味深かったです。

ブラウザ上で動くPHP @niisantokyo @新倉 涼太さん

docs.google.com

LT枠3番目?か4番目の弊社 新倉の発表です。 個人的にちょっと動作確認したいときに、ブラウザ上で動く環境があれば便利だねというお話でした。

JupyterProject Jupyter | Homeに関しては僕も使ってみたいと思いました。

総括

今回で2回目の参加でしたが、非常に楽しく有意義でした。 PHP勉強会に参加されてる方は、PHPに対する愛みたいなものを非常に感じます。 僕もそのうちLTで発表できたらなと思っています。

次回は2019年9月25日(水)に行われるようです。 ぜひ皆さん参加しましょう!

phpstudy.doorkeeper.jp

Laravel+JWTで認証付きAPIをつくる

JWTとは?

JWT = JSON Web Token

特徴としては、

  1. 電子署名がついているため改竄をチェックできる
  2. URL-safeになってる(URLに含むことができる文字のみで構成される)
  3. 実際の中身はJSON
  4. ステートレスであること

で、すごいざっくりいうと改竄ができないURLに埋め込むことができるTokenのこと

4について詳しく書くと トークンを使った認証はサーバー側で認証状態を管理しなく、送られてきたトークンを検証することしかしない。 サーバー側で認証状態をどう扱い、管理していくかという問題を考えずにすむことができるということ。

JWTの構成要素

  1. ヘッダー(Base64文字列)
  2. JSONの中身(Base64文字列)
  3. 電子署名

Base64とは、すべてのデータをアルファベット(a~z, A~z)と数字(0~9)、一部の記号(+,/)の64文字で表すエンコード方式のこと

3つの要素が.で繋がれた形になっている。

{ヘッダー}.{クレームデータ(JSONの中身)}.{電子署名}

ヘッダー

{
  'alg': 'HS256',
  'typ': 'JWT'
}

ヘッダ情報には、署名アルゴリズムの種類やメタ情報が記載されている。

alg:電子署名に使われているアルゴリズムを指定するキー
typ:トークンタイプ。JWT以外使うかは不明

algは、HS256やRS256などがありますが、RS256を使用すると脆弱性があるようです。

RS256 でTOKENを発行する仕組みになっている場合、攻撃者が HS256 のアルゴリズムで公開鍵(利用用途的に公開されているので誰でも取得可能)を用いて TOKEN を発行するとチェックする際に HS256 を用いてチェックされ KEY は同じ公開鍵を利用するためチェックが通ってしまうことになり、改ざんが可能になるということみたいです。

※ 参考 JWTライブラリの危機的な脆弱性について調べた - Qiita

クレームデータ

JSONの中身が入っている。デコードするだけで内容を確認できる。
※デコードするだけで中身が全部見れてしまうので、見えてはいけない情報を含めるのは回避すべき

予約済みのキー名が存在している。以下はその一覧

キー名 役割
exp 期限切れ日時
ndf 期間開始日時
iat JWT発行日時
iss 発行者(サーバ側)の識別子
aud 利用する側(クライアント側)の識別子

電子署名

電子署名部分はデータ改竄されているかどうかをチェックする際に使われている。

JWT認証の流れ

  1. クライアントが認証情報(user_idやpassword)を送信する。
  2. サーバが認証情報を取得してuser_idやexpを含む、JSON秘密鍵で暗号化してJWTとして返却する。
  3. クライアントはこのJWTを使って通常のAPI リクエストを行なう。
  4. サーバは、秘密鍵を使ってJWTを検証し、ログインIDをJSONから取り出し、処理を行なう。

クライアントから送信されるJWTはサーバで秘密鍵を使って改竄が行われたかを検証することができる為、user_idexpの改竄を検知する事ができる。

LaravelでのJWTの使い方

認証機能を導入

make:authコマンドでユーザー登録、ログイン、パスワードリセットのベースを構築することができる。

php artisan make:auth

上記コマンドでマイグレーションファイルも作ってくれるのでmigrationをする

php aritsan migrate

composerでjwt-authをインストール

まずはjwt-authを入れる。

composer require tymon/jwt-auth 1.0.0-rc3

※Laravel5.5以上は、1.0以上のjwt-authが必要なのでバージョンを指定する

次にjwt-authの初期設定を行う

php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

これを行うことで、/config/配下にjwt.phpが作成されます。

php artisan vendor:publishを何故行うのか
JWTAuthが持つconfigファイルは、vendor配下に設置されている。直接このファイルを変更しても今後パッケージが更新されると変更内容が失われてしまう。
そのため、このようなconfigファイルはPublish(内容を変更してもいいディレクトリにコピー)して使用する。
php artisan vendor:publishを行うことで/config配下にjwt.phpという形で変更可能なconfigファイルが作成される。

jwt-authが使えるシークレットキーを生成する。

php artisan jwt:secret

.envに以下のように行が追加されたか確認する

JWT_SECRET=XXXXXXX

jwt-authの設定

以下のようにconfig\app.phpのproviders配列とaliases配列にjwt-authを登録する。

config/app.php

'providers' => [

...

  Tymon\JWTAuth\Providers\LaravelServiceProvider::class,
],

'aliases' => [

...

  'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class,
  'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class,
],

Guardを修正

guardは認証を管理するドライバークラスです。 apiでjwt-authを使うのでdriver:jwtに変えます。

'guards' => [
...
        'api' => [
            'driver' => 'jwt',
            'provider' => 'users',
        ],
    ],

Userモデルを編集

Userモデルを以下のように編集する。

<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject;

class User extends Authenticatable implements JWTSubject
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * Get the identifier that will be stored in the subject claim of the JWT.
     *
     * @return mixed
     */
    public function getJWTIdentifier() {
        return $this->getKey();
    }

    /**
     * Return a key value array, containing any custom claims to be added to the JWT.
     *
     * @return array
     */
    public function getJWTCustomClaims()
    {
        return [];
    }
}

Laravelの認証機能を使うためにAuthenticatableを継承し、jwt-authのインタフェースをJWTSubjectを通じて実装する。 getJWTIdentifierはjwtのトークンを取得する処理。
getJWTCustomClaimsはクレームデータ(jwtの中身)に情報を追加したい時に使用する。

認証用コントローラの作成

認証用のコントローラAuthControllerを作成する。

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use JWTAuth;
use Tymon\JWTAuth\Exceptions\JWTException;

class AuthController extends Controller
{
    public function login(request $request) {
        $credentials = $request->only('email', 'password');
        try {
            if (! $token = JWTAuth::attempt($credentials)) {
                return response()->json(['error' => 'Unauthorized'], 401);
            }
        } catch (JWTException $e) {
            return response()->json(['error' => 'could_not_create_token'], 500);
        }

        return response()->json(compact('token'));
    }

    public function me()
    {
        $user = JWTAuth::parseToken()->authenticate();
        return response()->json(compact('user'));
    }
}

loginはリクエストからemailとpasswordをとってJWTAuth::attemptメソッドで認証し、 失敗した場合は401、成功した場合はトークンを発行して返す処理。 meはJWTを付与してリクエストすると、ユーザー情報を返す処理。

最後に、ルーティングを定義する。

$router = app('Illuminate\Routing\Router');

$router->group(['middleware' => ['guest:api']], function () use ($router) {
    $router->post('/login', 'AuthController@login')->name('auth-login');
});
$router->group(['middleware' => ['auth:api']], function () use ($router) {
    $router->get('/me', 'AuthController@me')->name('auth-me');
});

以上で、Laravel+JWTで認証付きAPIを実装することができる。

参考文献

qiita.com