【Laravel + Breeze + Socialite】GitHubソーシャルログインを実装(3)

GitHub

前回の記事の続きです。

この記事のゴール

前回は「Laravel\Socialite\Two\InvalidStateException」の原因究明と対策の提示をしました。

今回は、ログイン後のページでGitHubのユーザー情報を取得して表示するところまでの実装を進めていきます。

トークンを使ってユーザー情報を取得する

公式サイトに説明がありますが、

Laravel - The PHP Framework For Web Artisans
Laravel is a PHP web application framework with expressive, elegant syntax. We’ve already laid the foundation — freeing ...
Socialite::driver('github')->userFromToken($token);

でGitHubのユーザー情報を取得することができます。

ここでいうトークンは、ログイン時にGitHubで発行されたトークンで、

usersテーブルに登録されている「github_token」です。

ログイン後に、

auth()->user()->github_token

で取得することができます。

$githubToken = auth()->user()->github_token;
$githubUser = Socialite::driver('github')->userFromToken($githubToken);
var_dump($githubUser);

を実行して出力される内容は次の通りです。

object(Laravel\Socialite\Two\User)[1456]
  public 'id' => int 19181121
  public 'nickname' => string 'macocci7' (length=8)
  public 'name' => string 'macocci7' (length=8)
  public 'email' => string 'macocci7@yahoo.co.jp' (length=20)
  public 'avatar' => string 'https://avatars.githubusercontent.com/u/19181121?v=4' (length=52)
  public 'user' => 
    array (size=32)
      'login' => string 'macocci7' (length=8)
      'id' => int 19181121
      'node_id' => string 'MDQ6VXNlcjE5MTgxMTIx' (length=20)
      'avatar_url' => string 'https://avatars.githubusercontent.com/u/19181121?v=4' (length=52)
      'gravatar_id' => string '' (length=0)
      'url' => string 'https://api.github.com/users/macocci7' (length=37)
      'html_url' => string 'https://github.com/macocci7' (length=27)
      'followers_url' => string 'https://api.github.com/users/macocci7/followers' (length=47)
      'following_url' => string 'https://api.github.com/users/macocci7/following{/other_user}' (length=60)
      'gists_url' => string 'https://api.github.com/users/macocci7/gists{/gist_id}' (length=53)
      'starred_url' => string 'https://api.github.com/users/macocci7/starred{/owner}{/repo}' (length=60)
      'subscriptions_url' => string 'https://api.github.com/users/macocci7/subscriptions' (length=51)
      'organizations_url' => string 'https://api.github.com/users/macocci7/orgs' (length=42)
      'repos_url' => string 'https://api.github.com/users/macocci7/repos' (length=43)
      'events_url' => string 'https://api.github.com/users/macocci7/events{/privacy}' (length=54)
      'received_events_url' => string 'https://api.github.com/users/macocci7/received_events' (length=53)
      'type' => string 'User' (length=4)
      'site_admin' => boolean false
      'name' => string 'macocci7' (length=8)
      'company' => null
      'blog' => string '' (length=0)
      'location' => string 'Japan' (length=5)
      'email' => string 'macocci7@yahoo.co.jp' (length=20)
      'hireable' => null
      'bio' => string 'full stack web engineer.' (length=24)
      'twitter_username' => null
      'public_repos' => int 14
      'public_gists' => int 4
      'followers' => int 36
      'following' => int 88
      'created_at' => string '2016-05-04T04:59:39Z' (length=20)
      'updated_at' => string '2024-01-24T12:27:14Z' (length=20)
  public 'attributes' => 
    array (size=6)
      'id' => int 19181121
      'nodeId' => string 'MDQ6VXNlcjE5MTgxMTIx' (length=20)
      'nickname' => string 'macocci7' (length=8)
      'name' => string 'macocci7' (length=8)
      'email' => string 'macocci7@yahoo.co.jp' (length=20)
      'avatar' => string 'https://avatars.githubusercontent.com/u/19181121?v=4' (length=52)
  public 'token' => string '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' (length=40)
  public 'refreshToken' => null
  public 'expiresIn' => null
  public 'approvedScopes' => null

GitHubプロフィールを表示してみる

ログイン後のダッシュボードにGitHubプロフィールを表示してみます。

「resources/views/components/」フォルダ内に「github」フォルダを作成し、

その中に新しいファイル「profile.blade.php」を作成します。

<!-- GitHub Profile //-->
<div class="mt-4 mb-4 border border-gray-800 rounded-lg bg-gray-100">
    <p class="ml-4 mt-2 font-semibold text-lg text-gray-500">GitHub Profile:</p>
    <div class="flex ml-2 py-4">
        <a href="{{ $githubUser->user['html_url'] }}" target="_blank">
            <img
                src="{{ $githubUser->avatar }}"
                width="120"
                height="120"
                alt="Avatar: {{ $githubUser->name }}"
                title="{{ $githubUser->name }}"
                class="ml-4 rounded-full"
            />
        </a>
        <ul class="ml-4 text-gray-500">
            <li>Name: <a href="{{ $githubUser->user['html_url'] }}" target="_blank">{{ $githubUser->name }}</a></li>
            <li>Email: {{ $githubUser->email }}</li>
            <li>
                <span>followers: {{ $githubUser->user['followers'] }}</span>
                <span class="ml-4">following: {{ $githubUser->user['following'] }}</span>
            </li>
            <li>bio: {{ $githubUser->user['bio'] }}</li>
            <li>location: {{ $githubUser->user['location'] }}</li>
        </ul>
    </div>
</div>
<!-- // GitHub Profile -->

ダッシュボードのビューファイルに読み込ませます。

「resources/views/dashboard.blade.php」を編集します。

<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('Dashboard') }}
        </h2>
    </x-slot>

    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                <div class="p-6 text-gray-900">

                    <x-github.profile
                        :github-user="$github_user"
                    />

                </div>
            </div>
        </div>
    </div>
</x-app-layout>

「routes/web.php」を編集・保存します。

「/dashboard」の箇所に追記します。

Route::get('/dashboard', function () {
    $githubToken = auth()->user()->github_token;
    $githubUser = Socialite::driver('github')->userFromToken($githubToken);
    return view('dashboard', [
        'user' => auth()->user(),
        'github_user' => $githubUser,
        ]);
})->middleware(['auth', 'verified'])->name('dashboard');

WEBブラウザでGitHubソーシャルログインし

http://localhost:8000/dashboard

ダッシュボードを開きます。

このように表示されました。

GitHubAPIで他の情報も取得してみる

ここから先はSocialiteは全く関係ありませんが、

折角なので、公開リポジトリ、フォロワーリスト、フォローリストくらいは追加で表示してみたいと思います。

GitHub APIを叩いて情報を取得していきます。

GitHub REST API に関するドキュメント - GitHub Docs
GitHub の REST API でインテグレーションを作成し、データを取り出して、ワークフローを自動化する。

公開リポジトリの一覧は、

https://api.github.com/users/[ログインID]/repos

フォロワーリストは、

https://api.github.com/users/[ログインID]/followers

フォローリストは、

https://api.github.com/users/[ログインID]/following

で取得することができます。

それぞれ、URLパラメータ

?per_page=[1ページ当たりの件数]&page=[ページ番号]

を追加することでページングできます。

例えば、

https://api.github.com/users/macocci7/repos?per_page=1&page=12

で出力される内容は

[
  {
    "id": 698479567,
    "node_id": "R_kgDOKaHzzw",
    "name": "PHP-PhotoGps",
    "full_name": "macocci7/PHP-PhotoGps",
    "private": false,
    "owner": {
      "login": "macocci7",
・・・(中略)・・・
    },
    "html_url": "https://github.com/macocci7/PHP-PhotoGps",
    "description": "Gets GPS data from a photo.",
    "fork": false,
    "url": "https://api.github.com/repos/macocci7/PHP-PhotoGps",
・・・(中略)・・・
    "created_at": "2023-09-30T03:45:44Z",
    "updated_at": "2023-11-05T21:40:46Z",
・・・(中略)・・・
    "stargazers_count": 0,
    "watchers_count": 0,
    "language": "PHP",
・・・(中略)・・・
    "forks": 0,
    "open_issues": 1,
    "watchers": 0,
    "default_branch": "main"
  }
]

このような感じです。かなり長いので省略しています。

フォロワーリストの例は、

https://api.github.com/users/macocci7/followers?per_page=2&page=7
[
  {
    "login": "cryptobear0108",
    "id": 79739656,
    "node_id": "MDQ6VXNlcjc5NzM5NjU2",
    "avatar_url": "https://avatars.githubusercontent.com/u/79739656?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/cryptobear0108",
    "html_url": "https://github.com/cryptobear0108",
    "followers_url": "https://api.github.com/users/cryptobear0108/followers",
    "following_url": "https://api.github.com/users/cryptobear0108/following{/other_user}",
    "gists_url": "https://api.github.com/users/cryptobear0108/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/cryptobear0108/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/cryptobear0108/subscriptions",
    "organizations_url": "https://api.github.com/users/cryptobear0108/orgs",
    "repos_url": "https://api.github.com/users/cryptobear0108/repos",
    "events_url": "https://api.github.com/users/cryptobear0108/events{/privacy}",
    "received_events_url": "https://api.github.com/users/cryptobear0108/received_events",
    "type": "User",
    "site_admin": false
  },
  {
    "login": "najlae01",
    "id": 88176530,
    "node_id": "MDQ6VXNlcjg4MTc2NTMw",
    "avatar_url": "https://avatars.githubusercontent.com/u/88176530?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/najlae01",
    "html_url": "https://github.com/najlae01",
    "followers_url": "https://api.github.com/users/najlae01/followers",
    "following_url": "https://api.github.com/users/najlae01/following{/other_user}",
    "gists_url": "https://api.github.com/users/najlae01/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/najlae01/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/najlae01/subscriptions",
    "organizations_url": "https://api.github.com/users/najlae01/orgs",
    "repos_url": "https://api.github.com/users/najlae01/repos",
    "events_url": "https://api.github.com/users/najlae01/events{/privacy}",
    "received_events_url": "https://api.github.com/users/najlae01/received_events",
    "type": "User",
    "site_admin": false
  }
]

このような感じです。

フォローリストの例は、

https://api.github.com/users/macocci7/following?per_page=2&page=4
[
  {
    "login": "soxofaan",
    "id": 44946,
    "node_id": "MDQ6VXNlcjQ0OTQ2",
    "avatar_url": "https://avatars.githubusercontent.com/u/44946?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/soxofaan",
    "html_url": "https://github.com/soxofaan",
    "followers_url": "https://api.github.com/users/soxofaan/followers",
    "following_url": "https://api.github.com/users/soxofaan/following{/other_user}",
    "gists_url": "https://api.github.com/users/soxofaan/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/soxofaan/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/soxofaan/subscriptions",
    "organizations_url": "https://api.github.com/users/soxofaan/orgs",
    "repos_url": "https://api.github.com/users/soxofaan/repos",
    "events_url": "https://api.github.com/users/soxofaan/events{/privacy}",
    "received_events_url": "https://api.github.com/users/soxofaan/received_events",
    "type": "User",
    "site_admin": false
  },
  {
    "login": "facebook",
    "id": 69631,
    "node_id": "MDEyOk9yZ2FuaXphdGlvbjY5NjMx",
    "avatar_url": "https://avatars.githubusercontent.com/u/69631?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/facebook",
    "html_url": "https://github.com/facebook",
    "followers_url": "https://api.github.com/users/facebook/followers",
    "following_url": "https://api.github.com/users/facebook/following{/other_user}",
    "gists_url": "https://api.github.com/users/facebook/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/facebook/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/facebook/subscriptions",
    "organizations_url": "https://api.github.com/users/facebook/orgs",
    "repos_url": "https://api.github.com/users/facebook/repos",
    "events_url": "https://api.github.com/users/facebook/events{/privacy}",
    "received_events_url": "https://api.github.com/users/facebook/received_events",
    "type": "Organization",
    "site_admin": false
  }
]

このような感じです。

スタックの選択

スタックの選択は何でも良いのですが、

筆者の趣味としてVue.jsを使います。

今回は実験的な内容なのでCDN版を使います。

ページネーションはVuetify3のv-paginationを使います。

※最初はvuejs-paginate-nextで実装を進めていたのですが、動作が不安定な上に、イベント発火していない筈がAPIをフルスピードで数万回連打してしまい、気付いた時にはAPIのアクセスレート制限超過でブロックされる事態になっていました。。状況が改善されなかった為、Vuetify3に変更しました。

ダッシュボードビューファイル編集

「resources/views/dashboard.blade.php」を編集・保存します。

<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('Dashboard') }}
        </h2>
    </x-slot>

    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                <div class="p-6 text-gray-900">
                    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
                    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vuetify@3.5.4/dist/vuetify.min.css"></link>
                    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css">
                    <script src="https://cdn.jsdelivr.net/npm/vuetify@3.5.4/dist/vuetify.min.js"></script>
                    <script src="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/scripts/verify.min.js"></script>

                    <x-github.profile
                        :github-user="$github_user"
                    />

                    <x-github.attributes.container
                        :github-user="$github_user"
                    />

                </div>
            </div>
        </div>
    </div>
</x-app-layout>

変更点は、

  • Vue.jsのCDN版読込追加
  • VuetifyのCDN版読込追加
  • MDIのCDN版読込追加
  • GitHub属性表示用テンプレート読込追加

です。

GitHub属性表示用テンプレート作成

「resources/views/components/github/」フォルダ内に「attributes」フォルダを作成し、その中に「container.blade.php」を作成します。

<!-- github-attributes // -->
<div id="github-attributes">
    <x-github.attributes.tabs />
    <x-github.attributes.stats
        :githubUser="$githubUser"
    />
    <x-github.attributes.repos
        :githubUser="$githubUser"
    />
    <x-github.attributes.followers
        :githubUser="$githubUser"
    />
    <x-github.attributes.follows
        :githubUser="$githubUser"
    />
</div>
<!-- // github-attributes -->
<x-github.attributes.script
    :github-user="$githubUser"
/>
<x-github.attributes.style />

同じフォルダ内に「tabs.blade.php」を作成します。

<!-- GitHub Tabs // -->
<div class="github-tabs">
    <ul class="flex justify-between py-0 my-2 border-b-2">
        <li
            v-for="(tab,tabKey) in tabs"
            :class="{'tab': true, 'tab-active': tabKey == currentTab}"
            @click="activateTab(tabKey)"
        >
            @{{ tab.name }}
        </li>
    </ul>
</div>
<!-- // GitHub Tabs -->

同じフォルダ内に「stats.blade.php」を作成します。

<!-- GitHub Stats //-->
<div :class="{hide: tabs[currentTab].name !== 'GitHub Stats'}">
    <img align="center" src="https://github-readme-stats.vercel.app/api?username={{ $githubUser->name }}&show_icons=true&theme=shadow_green&rank_icon=percentile&include_all_commits=true&theme=transparent&hide_border=true" alt="{{ $githubUser->name }}'s github stats" />
    <img align="center" src="https://github-readme-stats.vercel.app/api/top-langs/?username={{ $githubUser->name }}&layout=compact&theme=buefy&hide_border=true" />
</div>
<!-- // GitHub Stats -->

同じフォルダ内に「repos.blade.php」を作成します。

<!-- Public Repos //-->
<div :class="{hide: tabs[currentTab].name !== 'Public Repos'}">
    <h3 class="font-semibold text-lg">Public Repos ({{ $githubUser->user['public_repos']}}):</h3>
    <ul class="repos-list">
        <li
            v-for="repo in repos.data"
            class="mt-2 p-4 bg-gray-100 border border-gray-900"
        >
            <a
                :href="repo['html_url']"
                target="_blank"
            >
                <p class="font-semibold text-gray-700">@{{ repo['full_name'] }}</p>
            <p class="ml-4 text-gray-600">
                @{{ repo['description'] }}
            </p>
            <p class="text-gray-500">
                <span class="ml-4">★ @{{ repo['stargazers_count'] }}</span>
                <span class="ml-4">watch @{{ repo['watchers_count'] }}</span>
                <span class="ml-4">fork @{{ repo['forks_count'] }}</span>
                <span class="ml-4">issues @{{ repo['open_issues_count'] }}</span>
            </p>
            </a>
        </li>
    </ul>
    <!-- Vuetify Pagination // -->
    <div class="pagination flex justify-start">
        <v-pagination
            v-model="repos.currentPage"
            :length="repos.pageCount"
            :total-visible="6"
            :size="32"
            :color="'blue-darken-4 bg-blue-lighten-5'"
            :active-color="'white bg-blue-darken-3'"
            :rounded="'sm'"
            @click = "fetchRepos"
        >
        </v-pagination>
    </div>
    <!-- // Vuetify Pagination -->
</div>
<!-- // Public Repos -->

同じフォルダ内に「follwers.blade.php」を作成します。

<!-- Followers // -->
<div :class="{hide: tabs[currentTab].name !== 'Followers'}">
    <p class="font-semibold text-gray-800 text-lg">Followers ({{ $githubUser->user['followers']}})</p>
    <ul class="followers-list">
        <li
            v-for="(follower, followerIndex) in followers.data"
            class="mt-2 p-4 bg-gray-100 border border-gray-900"
        >
            <a
                :href="follower.html_url"
                target="_blank"
                class="flex"
            >
            <img
                :src="follower.avatar_url"
                width="40"
                height="40"
                :alt="follower.login"
                :title="follower.login"
                class="rounded-full"
            />
            <span class="ml-4">@{{ follower.login }}</span>
            </a>
        </li>
    </ul>
    <!-- Vuetify Pagination // -->
    <div class="pagination flex justify-start">
        <v-pagination
            v-model="followers.currentPage"
            :length="followers.pageCount"
            :total-visible="6"
            :size="32"
            :color="'blue-darken-4 bg-blue-lighten-5'"
            :active-color="'white bg-blue-darken-3'"
            :rounded="'sm'"
            :previous-aria-label="'<'"
            :next-aria-label="'>'"
            @click = "fetchFollowers"
        >
        </v-pagination>
    </div>
    <!-- // Vuetify Pagination -->
</div>
<!-- // Followers -->

同じフォルダ内に「follows.blade.php」を作成します。

<!-- Follows // -->
<div :class="{hide: tabs[currentTab].name !== 'Follows'}">
    <p class="font-semibold text-gray-800 text-lg">Follows ({{ $githubUser->user['following']}})</p>
    <ul class="follows-list">
        <li
            v-for="(follow, followsIndex) in follows.data"
            class="flex mt-2 p-4 bg-gray-100 border border-gray-900"
        >
            <a
                :href="follow.html_url"
                target="_blank"
                class="flex"
            >
            <img
                :src="follow.avatar_url"
                width="40"
                height="40"
                :alt="follow.login"
                :title="follow.login"
                class="rounded-full"
            />
            <span class="ml-4">@{{ follow.login }}</span>
            </a>
        </li>
    </ul>
    <!-- Vuetify Pagination // -->
    <div class="pagination flex justify-start">
        <v-pagination
            v-model="follows.currentPage"
            :length="follows.pageCount"
            :total-visible="6"
            :size="32"
            :color="'blue-darken-4 bg-blue-lighten-5'"
            :active-color="'white bg-blue-darken-3'"
            :rounded="'sm'"
            @click = "fetchFollows"
        >
        </v-pagination>
    </div>
    <!-- // Vuetify Pagination -->
</div>
<!-- // Follows -->

同じフォルダ内に「script.blade.php」を作成します。

<script>
    Vue.createApp({
        data () {
            return {
                currentTab: 0,
                tabs: {
                    0: { name: 'GitHub Stats' },
                    1: { name: 'Public Repos' },
                    2: { name: 'Followers' },
                    3: { name: 'Follows' },
                },
                repos: {
                    url: '{{ $githubUser->user["repos_url"] }}',
                    perPage: 10,
                    pageCount: Math.ceil({{ $githubUser->user['public_repos'] }} / 10 ),
                    currentPage: 1,
                    data: [],
                },
                followers: {
                    url: '{{ $githubUser->user["followers_url"] }}',
                    perPage: 10,
                    pageCount: Math.ceil({{ $githubUser->user['followers'] }} / 10 ),
                    currentPage: 1,
                    data: [],
                },
                follows: {
                    url: '{{ str_replace('{/other_user}', '', $githubUser->user["following_url"]) }}',
                    perPage: 10,
                    pageCount: Math.ceil({{ $githubUser->user['following'] }} / 10 ),
                    currentPage: 1,
                    data: [],
                },
            }
        },
        methods: {
            async fetchRepos () {
                const url = this.repos.url
                            + '?per_page=' + this.repos.perPage
                            + '&page=' + this.repos.currentPage;
                const res = await fetch(url);
                this.repos.data = await res.json();
            },
            async fetchFollowers () {
                const url = this.followers.url
                            + '?per_page=' + this.followers.perPage
                            + '&page=' + this.followers.currentPage;
                const res = await fetch(url);
                this.followers.data = await res.json();
            },
            async fetchFollows () {
                const url = this.follows.url
                            + '?per_page=' + this.follows.perPage
                            + '&page=' + this.follows.currentPage;
                const res = await fetch(url);
                this.follows.data = await res.json();
            },
            activateTab (tabKey) {
                this.currentTab = tabKey;
                if (this.tabs[tabKey].name == 'Public Repos') {
                    this.fetchRepos();
                } else if (this.tabs[tabKey].name == 'Followers') {
                    this.fetchFollowers();
                } else if (this.tabs[tabKey].name == 'Follows') {
                    this.fetchFollows();
                }
            },
        },
    })
    .use(Vuetify.createVuetify())
    .mount('#github-attributes');
</script>

同じフォルダ内に「style.blade.php」を作成します。

<style>
    .pagination {
        flex;
    }
    .pagination li {
        justify-content: center;
        margin-right: 6px;
    }
    .pagination li.active {
        background-color: #3333ff;
        color: #ffffff;
        font-weight: bold;
    }
    .pagination li:hover:not(.disabled):not(.active) {
        background-color: #ffee33;
    }
    .hide {
        display: none;
    }
    .tab {
        color: #999999;
    }
    .tab-active {
        border-bottom: 2px solid #999999;
        color: #333333;
        font-weight: bold;
    }
    .repos-list, .followers-list, .follows-list {
        display: flex;
        flex-wrap: wrap;
    }
    .repos-list li {
        width: 320px;
        margin: 6px;
    }
    .followers-list li, .follows-list li {
        width: 240px;
        margin: 6px;
    }
</style>

ブラウザ確認

WEBブラウザでダッシュボードページを再読み込みします。

GitHubプロフィールの下に、4つのタブと、

デフォルトで開いている「GitHub Stats」が表示されました。

「Public Repos」タブをクリックしてみます。

リポジトリが表示されました。

「Followers」タブをクリックしてみます。

フォロワーリストが表示されました。

ページネーションも機能しています。

「Follows」タブをクリックしてみます。

フォローリストが表示されました。

ページネーションも機能しています。

今回は以上です。お疲れさまでした。

コメント

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