このページの本文へ

PHPでカレンダーを作りたい!だったらGoogle Calendar APIを使ってみたら?

2017年03月07日 08時00分更新

文●Wern Ancheta

  • この記事をはてなブックマークに追加
本文印刷
予定の管理はGoogleカレンダーが便利。でも独自のインターフェイスを持ったカレンダーサービスが作りたい。そんなときはGoogle Calendar APIを使ってみるといいかもしれません。

この記事では、PHPでGoogle Calendar APIを扱う方法を紹介します。カレンダーアプリを構築し、ユーザーが新しいカレンダーを作成したり、イベントを追加したり、Googleカレンダーと同期したりできるようにします。この記事に沿って作業する場合は、Homesteadをセットアップするとアプリの実行環境を簡単に用意できるのでおすすめです。

Google Console Projectのセットアップ

最初にGoogle Developers Consoleで新たなプロジェクトを作成します。

new project

プロジェクトを作成したら、ダッシュボードにある「enable and manage APIs(APIを有効にして管理しましょう)」をクリックします。

dashboard

Google APIのページで、Calendar APIとGoogle+ APIを選択し有効にします。

google apis

有効にしたら、Google APIのページに戻り「Credentials(認証情報)」のリンクをクリックします。続いて、「add credentials(認証情報を追加)」ボタンをクリックし、「OAuth 2.0 client ID(OAuth 2.0 クライアント ID)」を選択します。

add credentials

同意画面の設定が表示されるので「configure consent screen(同意画面を設定)」をクリックして設定します。

任意のメールアドレス、サービス名をそれぞれ選択、入力し「save(保存)」をクリックします。

oauth consent screen

「Web Application(ウェブ アプリケーション)」を作成します。

create client ID

クライアントIDとクライアントシークレットを取得できます。

アプリの構築

Composer経由でLaravelを使用します。

依存オブジェクトのインストール

composer create-project --prefer-dist laravel/laravel kalendaryo

プロジェクトのディレクトリとしてkalendaryoという新たなフォルダーが作成されます。

そのほかの依存オブジェクトをインストールします。

composer require nesbot/carbon google/apiclient

サインイン処理ではGoogleクライアントを使用してGoogle+ APIとやりとりします。また、Google Calendar APIを使用してGoogleカレンダーを操作します。

アプリの設定

プロジェクトディレクトリのルートにある.envファイルを開き、必要な項目を以下のように追加します。

APP_ENV=local
APP_DEBUG=true
APP_KEY=base64:iZ9uWJVHemk5wa8disC8JZ8YRVWeGNyDiUygtmHGXp0=
APP_URL=http://localhost

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=kalendaryo
DB_USERNAME=root
DB_PASSWORD=secret

SESSION_DRIVER=file

APP_TITLE=Kalendaryo
APP_TIMEZONE="Asia/Manila"

GOOGLE_CLIENT_ID="YOUR GOOGLE CLIENT ID"
GOOGLE_CLIENT_SECRET="YOUR GOOGLE CLIENT SECRET"
GOOGLE_REDIRECT_URL="http://kalendaryo.dev/login"
GOOGLE_SCOPES="email,profile,https://www.googleapis.com/auth/calendar"
GOOGLE_APPROVAL_PROMPT="force"
GOOGLE_ACCESS_TYPE="offline"

追加する必要のある設定値はDB_DATABASEDB_USERNAMEDB_PASSWORDAPP_TIMEZONEGOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRETGOOGLE_REDIRECT_URLです。

APP_TIMEZONEについては、PHPタイムゾーンのページから任意の値を使用できます。

データベースの項目については、新たなMySQLデータベースを作成し、データベース名をDB_DATABASEの値に使用します。DB_USERNAMEDB_PASSWORDはデータベースにログインするための認証情報です。Homestead Improvedを使用する場合は、あらかじめ作成済みのデータベースhomesteadと認証情報homestead / secretを利用できます。

グーグルの設定はGOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRETGOOGLE_REDIRECT_URLの値をGoogleコンソールで前もって取得しておいた認証情報に置き換えます。GOOGLE_SCOPESはアプリで必要となる権限です。同意画面に表示されたものをここに入れます。ユーザーは、アプリが特定のデータにアクセスすることに同意する必要があります。

説明したような情報がどこにあるか分からなければ、oauthplaygroundで調べられます。Google+ APIとGoogle Calendar APIを選択しドロップダウンに表示されるURLがその権限です。emailprofileは単にhttps://www.googleapis.com/auth/userinfo.emailhttps://www.googleapis.com/auth/userinfo.profileを省略したものです。あとで配列にコンバートするので、それぞれの権限はカンマで分けておきます。

Googleクライアントのサービスコンテナを作成する

次に、Googleクライアントのサービスコンテナの説明をします。app/Googl.phpファイルを作成し以下を追加します。

<?php
namespace App;

class Googl 
{    
    public function client()
    {
        $client = new \Google_Client();
        $client->setClientId(env('GOOGLE_CLIENT_ID'));
        $client->setClientSecret(env('GOOGLE_CLIENT_SECRET'));
        $client->setRedirectUri(env('GOOGLE_REDIRECT_URL'));
        $client->setScopes(explode(',', env('GOOGLE_SCOPES')));
        $client->setApprovalPrompt(env('GOOGLE_APPROVAL_PROMPT'));
        $client->setAccessType(env('GOOGLE_ACCESS_TYPE'));
        return $client;
    }
}

上のコードで、client ID(クライアントID) 、client secret(クライアントシークレット)、redirect URL(リダイレクトURL)、scopes(権限)、approval prompt(承認プロンプト) 、access type(アクセス種別)をセットしています。これらの値はすべて、前に作成しておいた.envファイルから読み込みます。すべてセットしたら、新たなインスタンスを返します。あとでclientメソッドを呼び出しGoogleクライアントを初期化します。

ルーティング

app/Http/routes.phpファイルを開き、アプリ内の各ページへのルーティングを追加します。

Route::group(
    ['middleware' => ['admin']],
    function(){

        Route::get('/dashboard', 'AdminController@index');

        Route::get('/calendar/create', 'AdminController@createCalendar');
        Route::post('/calendar/create', 'AdminController@doCreateCalendar');

        Route::get('/event/create', 'AdminController@createEvent');
        Route::post('/event/create', 'AdminController@doCreateEvent');

        Route::get('/calendar/sync', 'AdminController@syncCalendar');
        Route::post('/calendar/sync', 'AdminController@doSyncCalendar');

        Route::get('/events', 'AdminController@listEvents');

        Route::get('/logout', 'AdminController@logout');
});

Route::get('/', 'HomeController@index');
Route::get('/login', 'HomeController@login');

上のコードでadminと呼ばれる独自のルーティングミドルウェアを使用していますが、あとで作成します。ルーティンググループ内でミドルウェアを使うと、グループ内のすべてのルートに対しミドルウェアが有効になります。

Route::group(
    ['middleware' => ['admin']],
    function(){
        ...
    }
);

コールバック関数内には、そのグルーブに属する各ルートがあります。これらは一目で内容が分かります。POSTリクエストに応じるルートは書き込み処理のために使用します。一方、GETルートは読み取り専用です。

Route::get('/dashboard', 'AdminController@index');

Route::get('/calendar/create', 'AdminController@createCalendar');
Route::post('/calendar/create', 'AdminController@doCreateCalendar');

Route::get('/event/create', 'AdminController@createEvent');
Route::post('/event/create', 'AdminController@doCreateEvent');

Route::get('/calendar/sync', 'AdminController@syncCalendar');
Route::post('/calendar/sync', 'AdminController@doSyncCalendar');

Route::get('/events', 'AdminController@listEvents');

Route::get('/logout', 'AdminController@logout');

Adminルーティングミドルウェア

先に述べたように、ルーティングミドルウェアは特定のルートへのアクセスに対してコードを実行するために使用されます。adminミドルウェアの場合は、ユーザーが現在ログインしているかどうかを確認するために使用されます。

app/Http/Middleware/AdminMiddleware.phpadminミドルウェアを作成し、以下を追加します。

<?php
namespace App\Http\Middleware;

use Closure;

class AdminMiddleware
{
    public function handle($request, Closure $next)
    {
        if ($request->session()->has('user')) {
            return $next($request);
        }

        return redirect('/')
            ->with(
                'message',
                ['type' => 'danger', 'text' => 'You need to login']
            );
    }
}

ほとんどのミドルウェアは共通点を1つ持っています。リクエストを処理するためのhandleメソッドがあるということです。これはルーティングに応答するどのメソッドよりも先に実行されます。メソッド内でuserが現在のセッションにセット済みかチェックします。セット済みであれば、リクエストの処理に進みます。

if ($request->session()->has('user')) {
    return $next($request);
}

セット済みでなければ、ユーザーをホームページにリダイレクトし、セッションに対しログインが必要だというメッセージを渡します。

return redirect('/')
            ->with(
                'message',
                ['type' => 'danger', 'text' => 'You need to login']
            );

app/Http/Kernel.phpファイル内の$routeMiddleware配列の下に以下を追加してミドルウェアを使用可能にします。

'admin' => \App\Http\Middleware\AdminMiddleware::class

データベース

アプリはuser、calendar、eventという3つのテーブルを使用します。リポジトリからマイグレーションデータをコピーし、プロジェクトの同じパスに貼り付ければ、時間を短縮できます。プロジェクトのルートでphp artisan migrateを実行するとデータベースにテーブルが作成されます。

そのあと、User.phpCalendar.phpEvent.phpのコンテンツをappディレクトリにコピーします。この3つのファイルはあとでデータベースとやりとりする際のモデルになります。

ホームページ

app/Http/Controllers/HomeController.phpファイルを作成し、以下を追加します。

<?php
namespace App\Http\Controllers;

use App\Googl;
use App\User;
use App\Calendar;
use Illuminate\Http\Request;

class HomeController extends Controller
{
   public function index()
   {
        return view('login');
   }

   public function login(Googl $googl, User $user, Request $request)
   {
        $client = $googl->client();
        if ($request->has('code')) {

            $client->authenticate($request->get('code'));
            $token = $client->getAccessToken();

            $plus = new \Google_Service_Plus($client);

            $google_user = $plus->people->get('me');

            $id = $google_user['id'];

            $email = $google_user['emails'][0]['value'];
            $first_name = $google_user['name']['givenName'];
            $last_name = $google_user['name']['familyName'];

            $has_user = $user->where('email', '=', $email)->first();

            if (!$has_user) {
                //not yet registered
                $user->email = $email;
                $user->first_name = $first_name;
                $user->last_name = $last_name;
                $user->token = json_encode($token);
                $user->save();
                $user_id = $user->id;

                //create primary calendar
                $calendar = new Calendar;
                $calendar->user_id = $user_id;
                $calendar->title = 'Primary Calendar';
                $calendar->calendar_id = 'primary';
                $calendar->sync_token = '';
                $calendar->save();
            } else {
                $user_id = $has_user->id;
            }

            session([
                'user' => [
                    'id' => $user_id,
                    'email' => $email,
                    'first_name' => $first_name,
                    'last_name' => $last_name,
                    'token' => $token
                ]
            ]);

            return redirect('/dashboard')
                ->with('message', ['type' => 'success', 'text' => 'You are now logged in.']);

        } else {
            $auth_url = $client->createAuthUrl();
            return redirect($auth_url);
        }
   }
}

上のコードを細かく説明すると、最初にログインビューを返すindexメソッドがあります。

public function index()
{
    return view('login');
}

resources/views/login.blade.phpファイル内でログインビューを作成します。

@extends('layouts.default')

@section('content')
<form method="GET" action="/login">
    <button>Login with Google</button>
</form>
@stop

ログインビューはグーグルにログインするためのログインボタンがついているフォームです。このビューはデフォルトテンプレート(resources/views/layouts/default.blade.php)を継承していて、以下が含まれています。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{ env('APP_TITLE') }}</title>
    <link rel="stylesheet" href="{{ url('assets/lib/picnic/picnic.min.css') }}">
    <link rel="stylesheet" href="{{ url('assets/lib/picnic/plugins.min.css') }}">

    <link rel="stylesheet" href="{{ url('assets/css/style.css') }}">
</head>
<body>
    <div class="container">
        <header>
            <h1>{{ env('APP_TITLE') }}</h1>
        </header>
        @include('partials.alert')
        @yield('content')
    </div>
</body>
</html>

上のテンプレートで、picnic CSSは表示を整えるために使用されています。スタイルシートはpublic/assetsディレクトリに保存します。手動でインストールするか、あるいはBowerPHPのような便利なツールも利用できます。

@yield('content')部分がログインフォームを表示する場所です。

また、partials.alertもインクルードしています。resources/views/partials/alert.blade.phpファイルに定義してあり、ユーザーに対してメッセージやフォームのバリデーションエラーを表示するために使用します。

@if(session('message'))
    <div class="alert alert-{{ session('message.type') }}">
        {{ session('message.text') }}
    </div>
@endif

@if($errors->any())
    <div class="alert alert-danger">
        <ul>
            @foreach($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

HomeControllerに戻ります。次はloginメソッドです。Googleクライアントのサービスコンテナを使用できるようにタイプヒンティングを使用します。Userモデルについても同様です。

public function login (Googl $googl, User $user)
{
...
}

loginメソッド内で、Googleクライアントの新たなインスタンスを生成し、コードがクエリパラメーターとしてURLに渡されているか確認します。

過去のプロジェクトでOAuthを実装したことがない人のために動作を説明すると、ユーザーはサービスのログインページへと続くリンクをクリックしなければなりません。今回の場合はグーグルです。つまりユーザーはグーグルのログインページにリダイレクトされます。ログイン後、ユーザーは認証を求められます。ユーザーが同意すれば、ユニークなコードをURLに付加したWebサイトにリダイレクトします。このコードはアクセストークンを取得するために使用します。アクセストークンを使用することで、アプリはAPIにリクエストを出せます。

ここで確認しているコードはアクセストークンと交換するために使用するコードです。コードが存在しなければ、ユーザーをグーグルのログインページにリダイレクトします。

$client = $googl->client();
if ($request->has('code')) {
    ...
} else {
    $auth_url = $client->createAuthUrl();
    return redirect($auth_url);
}

コードが存在すれば、そのコードを使用してユーザーを認証します。

$client->authenticate($request->get('code'));

ユーザーの認証によって、アクセストークンの取得とGoogle+サービスに対するリクエストの初期化が可能になります。

$token = $client->getAccessToken();
$plus = new \Google_Service_Plus($client);

このあと、現在ログイン中のユーザーを取得し、ユーザー情報を取り出します。

$google_user = $plus->people->get('me');
$id = $google_user['id'];

$email = $google_user['emails'][0]['value']; //email
$first_name = $google_user['name']['givenName']; //first name
$last_name = $google_user['name']['familyName']; //last name

ユーザーがデータベース上にすでに存在するか確認します。

$has_user = $user->where('email', '=', $email)->first();

ユーザーが存在しなければ、ユーザーを新規作成しエントリをデータベースに保存します。また、ユーザーのデフォルトカレンダーを作成します。以下は初期設定のGoogleカレンダーです。コピーを作成して、Googleカレンダーサービスとデータの同期を開始すればイベント(予約)がコンテナを持てるようにします。

if (!$has_user) {
    //not yet registered
    $user->email = $email;
    $user->first_name = $first_name;
    $user->last_name = $last_name;
    $user->token = $token;
    $user->save(); 
    $user_id = $user->id;

    //create primary calendar
    $calendar = new Calendar;
    $calendar->user_id = $user_id;
    $calendar->title = 'Primary Calendar';
    $calendar->calendar_id = 'primary';
    $calendar->sync_token = '';
    $calendar->save();
}

ユーザーがすでに存在していれば、値を$user_idに割り当て、ユーザーをセッションにセットします。

$user_id = $has_user->id;

ユーザーの詳細をセッションに保存します。

session([
    'user' => [
        'id' => $user_id,
        'email' => $email,
        'first_name' => $first_name,
        'last_name' => $last_name,
        'token' => $token
    ]
]);

最後にユーザーを管理ダッシュボードページにリダイレクトします。

return redirect('/dashboard')
                ->with('message', ['type' => 'success', 'text' => 'You are now logged in.']);

管理ページ

続いて、app/Http/Controllers/AdminController.phpファイルに管理ページのコントローラーを作成します。

<?php
namespace App\Http\Controllers;

use App\Googl;
use App\User;
use App\Calendar;
use App\Event;
use Carbon\Carbon;
use Illuminate\Http\Request;

class AdminController extends Controller
{

   private $client;

   public function __construct(Googl $googl)
   {
        $this->client = $googl->client();
        $this->client->setAccessToken(session('user.token'));
   }


   public function index(Request $request)
   {
        return view('admin.dashboard');
   }


   public function createCalendar(Request $request)
   {
        return view('admin.create_calendar');
   }


   public function doCreateCalendar(Request $request, Calendar $calendar)
   {
        $this->validate($request, [
            'title' => 'required|min:4'
        ]);

        $title = $request->input('title');
        $timezone = env('APP_TIMEZONE');

        $cal = new \Google_Service_Calendar($this->client);

        $google_calendar = new \Google_Service_Calendar_Calendar($this->client);
        $google_calendar->setSummary($title);
        $google_calendar->setTimeZone($timezone);

        $created_calendar = $cal->calendars->insert($google_calendar);

        $calendar_id = $created_calendar->getId();

        $calendar->user_id = session('user.id');
        $calendar->title = $title;
        $calendar->calendar_id = $calendar_id;
        $calendar->save();

        return redirect('/calendar/create')
            ->with('message', [
                'type' => 'success', 'text' => 'Calendar was created!'
            ]);
   }


   public function createEvent(Calendar $calendar, Request $request)
   {
        $user_id = session('user.id');
        $calendars = $calendar
            ->where('user_id', '=', $user_id)->get();
        $page_data = [
            'calendars' => $calendars
        ];
        return view('admin.create_event', $page_data);
   }


   public function doCreateEvent(Event $evt, Request $request)
   {
        $this->validate($request, [
            'title' => 'required',
            'calendar_id' => 'required',
            'datetime_start' => 'required|date',
            'datetime_end' => 'required|date'
        ]);

        $title = $request->input('title');
        $calendar_id = $request->input('calendar_id');
        $start = $request->input('datetime_start');
        $end = $request->input('datetime_end');

        $start_datetime = Carbon::createFromFormat('Y/m/d H:i', $start);
        $end_datetime = Carbon::createFromFormat('Y/m/d H:i', $end);

        $cal = new \Google_Service_Calendar($this->client);
        $event = new \Google_Service_Calendar_Event();
        $event->setSummary($title);

        $start = new \Google_Service_Calendar_EventDateTime();
        $start->setDateTime($start_datetime->toAtomString());
        $event->setStart($start);
        $end = new \Google_Service_Calendar_EventDateTime();
        $end->setDateTime($end_datetime->toAtomString());
        $event->setEnd($end);

        //attendee
        if ($request->has('attendee_name')) {
            $attendees = [];
            $attendee_names = $request->input('attendee_name');
            $attendee_emails = $request->input('attendee_email');

            foreach ($attendee_names as $index => $attendee_name) {
                $attendee_email = $attendee_emails[$index];
                if (!empty($attendee_name) && !empty($attendee_email)) {
                    $attendee = new \Google_Service_Calendar_EventAttendee();
                    $attendee->setEmail($attendee_email);
                    $attendee->setDisplayName($attendee_name);
                    $attendees[] = $attendee;
                }
            }

            $event->attendees = $attendees;
        }

        $created_event = $cal->events->insert($calendar_id, $event);

        $evt->title = $title;
        $evt->calendar_id = $calendar_id;
        $evt->event_id = $created_event->id;
        $evt->datetime_start = $start_datetime->toDateTimeString();
        $evt->datetime_end = $end_datetime->toDateTimeString();
        $evt->save();

        return redirect('/event/create')
                    ->with('message', [
                        'type' => 'success',
                        'text' => 'Event was created!'
                    ]);
   }


   public function syncCalendar(Calendar $calendar)
   {
        $user_id = session('user.id');
        $calendars = $calendar->where('user_id', '=', $user_id)
            ->get();

        $page_data = [
            'calendars' => $calendars
        ];
        return view('admin.sync_calendar', $page_data);
   }


   public function doSyncCalendar(Request $request)
   {
        $this->validate($request, [
            'calendar_id' => 'required'
        ]);

        $user_id = session('user.id');
        $calendar_id = $request->input('calendar_id');

        $base_timezone = env('APP_TIMEZONE');

        $calendar = Calendar::find($calendar_id);
        $sync_token = $calendar->sync_token;
        $g_calendar_id = $calendar->calendar_id;

        $g_cal = new \Google_Service_Calendar($this->client);

        $g_calendar = $g_cal->calendars->get($g_calendar_id);
        $calendar_timezone = $g_calendar->getTimeZone();

        $events = Event::where('id', '=', $calendar_id)
            ->lists('event_id')
            ->toArray();

        $params = [
            'showDeleted' => true,
            'timeMin' => Carbon::now()
                ->setTimezone($calendar_timezone)
                ->toAtomString()
        ];

        if (!empty($sync_token)) {
            $params = [
                'syncToken' => $sync_token
            ];
        }

        $googlecalendar_events = $g_cal->events->listEvents($g_calendar_id, $params);


        while (true) {

            foreach ($googlecalendar_events->getItems() as $g_event) {

                $g_event_id = $g_event->id;
                $g_event_title = $g_event->getSummary();
                $g_status = $g_event->status;

                if ($g_status != 'cancelled') {

                    $g_datetime_start = Carbon::parse($g_event->getStart()->getDateTime())
                        ->tz($calendar_timezone)
                        ->setTimezone($base_timezone)
                        ->format('Y-m-d H:i:s');

                    $g_datetime_end = Carbon::parse($g_event->getEnd()->getDateTime())
                        ->tz($calendar_timezone)
                        ->setTimezone($base_timezone)
                        ->format('Y-m-d H:i:s');

                    //check if event id is already in the events table
                    if (in_array($g_event_id, $events)) {
                        //update event
                        $event = Event::where('event_id', '=', $g_event_id)->first();
                        $event->title = $g_event_title;
                        $event->calendar_id = $g_calendar_id;
                        $event->event_id = $g_event_id;
                        $event->datetime_start = $g_datetime_start;
                        $event->datetime_end = $g_datetime_end;
                        $event->save();
                    } else {
                        //add event
                        $event = new Event;
                        $event->title = $g_event_title;
                        $event->calendar_id = $g_calendar_id;
                        $event->event_id = $g_event_id;
                        $event->datetime_start = $g_datetime_start;
                        $event->datetime_end = $g_datetime_end;
                        $event->save();
                    }

                } else {
                    //delete event
                    if (in_array($g_event_id, $events)) {
                        Event::where('event_id', '=', $g_event_id)->delete();
                    }
                }

            }

            $page_token = $googlecalendar_events->getNextPageToken();
            if ($page_token) {
                $params['pageToken'] = $page_token;
                $googlecalendar_events = $g_cal->events->listEvents('primary', $params);
            } else {
                $next_synctoken = str_replace('=ok', '', $googlecalendar_events->getNextSyncToken());

                //update next sync token
                $calendar = Calendar::find($calendar_id);
                $calendar->sync_token = $next_synctoken;
                $calendar->save();

                break;
            }

        }

        return redirect('/calendar/sync')
            ->with('message',
                [
                    'type' => 'success',
                    'text' => 'Calendar was synced.'
                ]);

   }


   public function listEvents()
   {
        $user_id = session('user.id');
        $calendar_ids = Calendar::where('user_id', '=', $user_id)
            ->lists('calendar_id')
            ->toArray();

        $events = Event::whereIn('calendar_id', $calendar_ids)->get();

        $page_data = [
            'events' => $events
        ];

        return view('admin.events', $page_data);
   }


   public function logout(Request $request)
   {
        $request->session()->flush();
        return redirect('/')
            ->with('message', ['type' => 'success', 'text' => 'You are now logged out']);
   }

}

上のコードを細かく説明します。

コントローラー内で$client変数はコンストラクタで初期化したGoogleクライアントへの参照を保存します。この方法により、Googleクライアントが必要になるたびに初期化しなくて良くなります。

private $client;

public function __construct(Googl $googl)
{
    $this->client = $googl->client();
    $this->client->setAccessToken(session('user.token'));
}

indexメソッドは管理ダッシュボード(resources/views/admin/dashboard.blade.php)ビューを返します。

public function index(){
    return view('admin.dashboard');
}

管理ダッシュボードは各ページへのリンクを含み、ユーザーはリンク先でさまざまな操作ができます。

@extends('layouts.admin')

@section('content')
<h3>What do you like to do?</h3>
<ul>
    <li><a href="/calendar/create">Create Calendar</a></li>
    <li><a href="/event/create">Create Event</a></li>
    <li><a href="/calendar/sync">Sync Calendar</a></li>
    <li><a href="/events">Events</a></li>
    <li><a href="/logout">Logout</a></li>
</ul>
@stop

このビューは以下の管理レイアウト(resources/views/layouts/admin.blade.php)を継承しています。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{ env('APP_TITLE') }}</title>
    <link rel="stylesheet" href="{{ url('assets/lib/picnic/picnic.min.css') }}">
    <link rel="stylesheet" href="{{ url('assets/lib/picnic/plugins.min.css') }}">

    @yield('jquery_datetimepicker_style')

    <link rel="stylesheet" href="{{ url('assets/css/style.css') }}">
</head>
<body>
    <div class="container">
        <header>
            <h1>{{ env('APP_TITLE') }}</h1>
        </header>
        @include('partials.alert')
        @yield('content')
    </div>

    @yield('attendee_template')

    @yield('jquery_script')

    @yield('handlebars_script')

    @yield('jquery_datetimepicker_script')

    @yield('create_event_script')

</body>
</html>

上のコードで、メインコンテンツのほかにいくつかのテンプレートをyieldしています。これらのテンプレートは別のビューから渡されます。ビューがyieldしたセクションを定義していなければ、コードはなにも返しません。

カレンダーの作成

AdminControllerに戻ります。以下はカレンダーを新規作成するためのビューを返すメソッドです。

public function createCalendar(Request $request)
{
    return view('admin.create_calendar');
}

作成したカレンダービューは、カレンダーのタイトルを入力するフォームを含みます。また、CSRF(クロスサイトリクエストフォージェリ)の防止に使用される、隠れたフィールドがあります。

@extends('layouts.admin')

@section('content')
<form method="POST">
    <input type="hidden" name="_token" value="{{{ csrf_token() }}}" />
    <p>
        <label for="title">Title</label>
        <input type="text" name="title" id="title" value="{{ old('title') }}">
    </p>
    <button>Create Calendar</button>
</form>
@stop

フォームを送信したらdoCreateCalendarメソッドが呼び出されます。メソッド内で、タイトルフィールドに値が与えられているか、またその値が4文字以上あるかを検証します。

$this->validate($request, [
    'title' => 'required|min:4'
]);

Googleカレンダークライアントを初期化します。

$cal = new \Google_Service_Calendar($this->client);

新たなカレンダーを作成します。ユーザーが入力したタイトルをセットし、アプリのデフォルトタイムゾーンをセットします。

$google_calendar = new \Google_Service_Calendar_Calendar($this->client);
$google_calendar->setSummary($title);
$google_calendar->setTimeZone($timezone);

$created_calendar = $cal->calendars->insert($google_calendar);

カレンダーのIDを取得します。

$calendar_id = $created_calendar->getId();

新たに作成したカレンダーをデータベースに保存します。

$calendar->user_id = session('user.id');
$calendar->title = $title;
$calendar->calendar_id = $calendar_id;
$calendar->save();

ユーザーをカレンダー新規作成ページにリダイレクトします。

return redirect('/calendar/create')
            ->with('message', [
                'type' => 'success', 'text' => 'Calendar was created!'
            ]);

イベントの作成

次は、イベントを新規作成するビューを返すメソッドです。ここでは、ユーザーによって作成済みの複数のカレンダーを渡します。ユーザーがイベントをどのカレンダーに追加するか選ぶ必要があるからです。

public function createEvent(Calendar $calendar, Request $request)
{
    $user_id = session('user.id');
    $calendars = $calendar
        ->where('user_id', '=', $user_id)->get();
    $page_data = [
        'calendars' => $calendars
    ];
    return view('admin.create_event', $page_data);
}

admin.create_eventビュー(resources/views/admin/create_event.blade.php)はjQueryhandlebarsjQuery datetimepickerといったスタイルやスクリプトを含みます。

DOMイベントをリッスンするためにjQueryを使用します。また、新たなHTMLを作成するためにhandlebarsを、日付けフィールドを日時入力支援機能(ピッカー)に変換するためにjQuery datetimepickerを使用します。フォーム内には、イベントのタイトルを入力するためのフィールド、イベントを追加するカレンダーを選択するためのフィールド、開始日時と終了日時、参加者の名前とメールアドレスを入力するためのフィールドがあります。フォームの下には、参加者の列に使用するhandlebarsテンプレートがあります。この列はattendees(参加者)コンテナの下に追加されます(<div id="attendees">...</div>)。

@extends('layouts.admin')

@section('jquery_datetimepicker_style')
<link rel="stylesheet" href="{{ url('assets/lib/jquery-datetimepicker/jquery.datetimepicker.min.css') }}">
@stop

@section('content')
<form method="POST">
    <input type="hidden" name="_token" value="{{{ csrf_token() }}}" />
    <p>
        <label for="title">Title</label>
        <input type="text" name="title" id="title" value="{{ old('title') }}">
    </p>
    <p>
        <label for="calendar_id">Calendar</label>
        <select name="calendar_id" id="calendar_id">
            @foreach($calendars as $cal)
            <option value="{{ $cal->calendar_id }}">{{ $cal->title }}</option>
            @endforeach
        </select>
    </p>
    <p>
        <label for="datetime_start">Datetime Start</label>
        <input type="text" name="datetime_start" id="datetime_start" class="datetimepicker" value="{{ old('datetime_start') }}">
    </p>
    <p>
        <label for="datetime_end">Datetime End</label>
        <input type="text" name="datetime_end" id="datetime_end" class="datetimepicker" value="{{ old('datetime_end') }}">
    </p>
    <div id="attendees">
        Attendees
        <div class="attendee-row">
            <input type="text" name="attendee_name[]" class="half-input name" placeholder="Name">
            <input type="text" name="attendee_email[]" class="half-input email" placeholder="Email">
        </div>
    </div>
    <button>Create Event</button>
</form>
@stop


@section('attendee_template')
<script id="attendee-template" type="text/x-handlebars-template">
    <div class="attendee-row">
        <input type="text" name="attendee_name[]" class="half-input name" placeholder="Name">
        <input type="text" name="attendee_email[]" class="half-input email" placeholder="Email">
    </div>
</script>
@stop

@section('jquery_script')
<script src="{{ url('assets/lib/jquery.min.js') }}"></script>
@stop

@section('handlebars_script')
<script src="{{ url('assets/lib/handlebars.min.js') }}"></script>
@stop

@section('jquery_datetimepicker_script')
<script src="{{ url('assets/lib/jquery-datetimepicker/jquery.datetimepicker.min.js') }}"></script>
@stop

@section('create_event_script')
<script src="{{ url('assets/js/create_event.js') }}"></script>
@stop

上のテンプレートではcreate_event.jsファイルもインクルードしています。このファイルの役割は日付けフィールドを日時入力支援機能に変換し、参加者のメールアドレスのテキストフィールドでblurイベントをリッスンすることです。blurイベントが発生し、ユーザーが参加者名とメールアドレスを列に入力していれば、新たな列を生成しユーザーが新たな参加者を入力できるようにします。

var attendee_template = Handlebars.compile($('#attendee-template').html());

$('.datetimepicker').datetimepicker();

$('#attendees').on('blur', '.email', function(){

    var attendee_row = $('.attendee-row:last');
    var name = attendee_row.find('.name').val();
    var email = attendee_row.find('.email').val();

    if(name && email){
        $('#attendees').append(attendee_template());
    }
});

次は、イベント作成フォームの送信時に実行されるメソッドです。

public function doCreateEvent(Event $evt, Request $request)
{
    ...
}

メソッド内でフォームを検証しフィールドからデータを取得します。

$this->validate($request, [
    'title' => 'required',
    'calendar_id' => 'required',
    'datetime_start' => 'required|date',
    'datetime_end' => 'required|date'
]);

$title = $request->input('title');
$calendar_id = $request->input('calendar_id');
$start = $request->input('datetime_start');
$end = $request->input('datetime_end');

開始および終了日時を日付けオブジェクトにコンバートすることで、容易にフォーマットできるようにします。

$start_datetime = Carbon::createFromFormat('Y/m/d H:i', $start);
$end_datetime = Carbon::createFromFormat('Y/m/d H:i', $end);

新たなGoogleカレンダーのイベントを作成し、ユーザーが入力したタイトルをサマリーにセットします。

$event = new \Google_Service_Calendar_Event();
$event->setSummary($title);

開始および終了日時をセットします。Google Calendar APIは、日付けと時間がATOM時間表記にフォーマットされていることを前提としているので注意してください。そのため、Carbonが提供するtoAtomString()メソッドを使用して$start_datetime$end_datetimeをフォーマットします。

$start = new \Google_Service_Calendar_EventDateTime();
$start->setDateTime($start_datetime->toAtomString());
$event->setStart($start);

$end = new \Google_Service_Calendar_EventDateTime();
$end->setDateTime($end_datetime->toAtomString());
$event->setEnd($end);

ユーザーが参加者を追加したか確認します。

if ($request->has('attendee_name')) {
    ...
}

追加していれば、参加者に変数を割り当てます。

$attendees = [];
$attendee_names = $request->input('attendee_name');
$attendee_emails = $request->input('attendee_email');

参加者それぞれについてループ処理し、ユーザーが名前とメールアドレスの両方を入力していれば、Googleカレンダーに新たなイベント参加者を作成し、名前とメールアドレスをセットします。そして、作成した参加者を$attendees配列に追加します。

foreach ($attendee_names as $index => $attendee_name) {
    $attendee_email = $attendee_emails[$index];
    if (!empty($attendee_name) && !empty($attendee_email)) {
        $attendee = new \Google_Service_Calendar_EventAttendee();
        $attendee->setEmail($attendee_email);
        $attendee->setDisplayName($attendee_name);
        $attendees[] = $attendee;
    }
}

各参加者のループ処理が終了したら、$attendees配列をイベントの参加者に設定します。

$event->attendees = $attendees;

イベントをGoogleカレンダーに保存します。

$created_event = $cal->events->insert($calendar_id, $event);

イベントをデータベースにも保存します。

$evt->title = $title;
$evt->calendar_id = $calendar_id;
$evt->event_id = $created_event->id;
$evt->datetime_start = $start_datetime->toDateTimeString();
$evt->datetime_end = $end_datetime->toDateTimeString();
$evt->save();

イベント作成ページにリダイレクトします。

return redirect('/event/create')
            ->with('message', [
                'type' => 'success',
                'text' => 'Event was created!'
            ]);

カレンダーの同期

次はカレンダーを同期させるためのビュー(resources/views/admin/sync_calendar.blade.php)を返す関数です。ユーザーが作成したカレンダーをビューのデータとして渡します。

public function syncCalendar(Calendar $calendar)
{
    $user_id = session('user.id');
    $calendars = $calendar->where('user_id', '=', $user_id)
        ->get();

    $page_data = [
        'calendars' => $calendars
    ];
    return view('admin.sync_calendar', $page_data);
}

以下はadmin.sync_calendarビューのコードで、ユーザーは同期するカレンダーを選択できます。送信後にコードが処理する内容は、データベースにまだ追加されていないイベントの追加、キャンセルされたイベントの削除、最後の同期から変更された内容の更新です。

@extends('layouts.admin')

@section('content')
<form method="POST">
    <input type="hidden" name="_token" value="{{{ csrf_token() }}}" />
    <p>
        <label for="calendar_id">Calendar</label>
        <select name="calendar_id" id="calendar_id">
            @foreach($calendars as $cal)
            <option value="{{ $cal->id }}">{{ $cal->title }}</option>
            @endforeach
        </select>
    </p>
    <button>Sync Calendar</button>
</form>
@stop

ユーザーがカレンダー同期フォームを送信したらdoSyncCalendarメソッドが実行されます。

public function doSyncCalendar(Request $request)
{
    ...
}

関数内で、データベースにあるカレンダーを見つけます。

$calendar = Calendar::find($calendar_id);
$sync_token = $calendar->sync_token;
$g_calendar_id = $calendar->calendar_id;

Googleカレンダーサービスの新たなインスタンスを作成し、IDに基づきカレンダーとカレンダーのタイムゾーンを取得します。イベント日時が使用しているタイムゾーンにセットします。この方法を使えば、アプリが使用するタイムゾーンへ容易にコンバートできます。

$g_cal = new \Google_Service_Calendar($this->client);

$g_calendar = $g_cal->calendars->get($g_calendar_id);
$calendar_timezone = $g_calendar->getTimeZone();

ユーザーが選択したカレンダーに属するイベントのIDを取得します。イベントが追加済みか確認するために使用します。ここから、イベントをどう処理するか判断していきます。イベントがデータベース中にあり、さらにキャンセルされていない状態なら、更新が必要です。そうでなければデータベースから削除します。データベース中になければ追加する必要があります。

$events = Event::where('id', '=', $calendar_id)
            ->lists('event_id')
            ->toArray();

最初の同期のためにデフォルトフィルターを指定します。showDeletedオプションを使用すると削除されたイベントを返します。timeMinオプションを使用するとリクエストに使用される日時の下限を指定できるので、過去のイベントが返されなくなります。

$params = [
    'showDeleted' => true,
    'timeMin' => Carbon::now()
        ->setTimezone($calendar_timezone)
        ->toAtomString()
];

Googleカレンダーと同期するために、同期トークンを使用します。カレンダーに変更がある(例:イベントの追加)たびに同期トークンも変わります。データベースにトークンを保存しておく必要があります。Googleのサーバーにあるトークンが、持っているトークンと同じなら変更はありません。トークンが異なるときにのみ、更新します。更新後に、データベース中のトークンも更新する必要があります。

同期トークンが空でなければ、デフォルトフィルターの代わりに使用します。更新された、あるいは追加されたイベントだけを返せます。

if (!empty($sync_token)) {
    $params = [
        'syncToken' => $sync_token
    ];
}

イベントを取り出します。

$googlecalendar_events = $g_cal->events->listEvents($g_calendar_id, $params);

次は、ユーザーが選択したカレンダーのすべての更新を取り出すまで停止しない無限ループです。

while (true) {
    ...
}

無限ループ内で、APIによって返されるイベントをループ処理し、関連するデータを取り出します。

foreach ($googlecalendar_events->getItems() as $g_event) {

    $g_event_id = $g_event->id;
    $g_event_title = $g_event->getSummary();
    $g_status = $g_event->status;
    ...
}

イベントの状態を確認します。キャンセルされていれば、イベントをデータベースから削除します。

if ($g_status != 'cancelled') {
    ...
} else {
    //delete event
    if (in_array($g_event_id, $events)) {
        Event::where('event_id', '=', $g_event_id)->delete();
    }
}

イベントがキャンセルされていなければ、イベントの開始および終了日時を取り出しアプリのタイムゾーンにコンバートします。

$g_datetime_start = Carbon::parse($g_event->getStart()
    ->getDateTime())
    ->tz($calendar_timezone)
    ->setTimezone($base_timezone)
    ->format('Y-m-d H:i:s');

$g_datetime_end = Carbon::parse($g_event->getEnd()->getDateTime())
    ->tz($calendar_timezone)
    ->setTimezone($base_timezone)
    ->format('Y-m-d H:i:s');

イベントIDがイベントID配列中に見つかれば、そのイベントは更新済みであることを意味するので、データベースも更新します。

if (in_array($g_event_id, $events)) {
    //update event
    $event = Event::where('event_id', '=', $g_event_id)->first();
    $event->title = $g_event_title;
    $event->calendar_id = $g_calendar_id;
    $event->event_id = $g_event_id;
    $event->datetime_start = $g_datetime_start;
    $event->datetime_end = $g_datetime_end;
    $event->save();

}

配列中になければ、そのイベントは新たなイベントなので、データベースに新たなエントリを追加します。

else {
    //add event 
    $event = new Event;
    $event->title = $g_event_title;
    $event->calendar_id = $g_calendar_id;
    $event->event_id = $g_event_id;
    $event->datetime_start = $g_datetime_start;
    $event->datetime_end = $g_datetime_end;
    $event->save();
}

イベントをループし終ったら、次のページのためのページトークンを取得します。このトークンは先の同期トークンとは別なので注意してください。ページトークンはページ繰りのためだけに使用されるものです。ここでは単に、現在の結果セットにまだページがあるかを確認しています。

$page_token = $googlecalendar_events->getNextPageToken();

次のページがあれば、ページを次のリクエスト用のpageTokenとしてセットし、イベント取り出しのリクエストに進みます。

if ($page_token) {
   $params['pageToken'] = $page_token;
   $googlecalendar_events = $g_cal->events->listEvents('primary', $params);
}

次のページがなければ、すべての結果を取得済みなので、次の同期トークンを取得し使用してデータベースを更新します。ここで、無限ループを抜け出します。

else {
    $next_synctoken = str_replace('=ok', '', $googlecalendar_events->getNextSyncToken());

    //update next sync token
    $calendar = Calendar::find($calendar_id);
    $calendar->sync_token = $next_synctoken;
    $calendar->save();

    break; //exit out of the inifinite loop
}

カレンダー同期ページにリダイレクトし、ユーザーに対してカレンダーが同期されたことを伝えます。

return redirect('/calendar/sync')
        ->with('message',
            [
                'type' => 'success',
                'text' => 'Calendar was synced.'
            ]);

イベントの一覧表示

次はユーザーが同期を完了したすべてのイベントを表示するメソッドです。

public function listEvents()
{
    $user_id = session('user.id');
    $calendar_ids = Calendar::where('user_id', '=', $user_id)
        ->lists('calendar_id')
        ->toArray();

    $events = Event::whereIn('calendar_id', $calendar_ids)->get();

    $page_data = [
        'events' => $events
    ];

    return view('admin.events', $page_data);
}

このイベントビュー(resources/views/admin/events.blade.php)は以下を含みます。

@extends('layouts.admin')

@section('content')
@if(count($events) > 0)
<table>
    <thead>
        <tr>
            <th>Title</th>
            <th>Datetime Start</th>
            <th>Datetime End</th>
        </tr>
    </thead>
    <tbody>
        @foreach($events as $event)
        <tr>
            <td>{{ $event->title }}</td>
            <td>{{ $event->datetime_start }}</td>
            <td>{{ $event->datetime_end }}</td>
        </tr>
        @endforeach
    </tbody>
</table>
@endif
@stop

@stop

ログアウト

最後に、logoutメソッドがあります。管理ダッシュボードにあるログアウトのリンクをユーザーがクリックすると実行されるメソッドです。すべてのセッションデータを削除し、ユーザーをホームページにリダイレクトします。

public function logout(Request $request)
{
    $request->session()->flush();
    return redirect('/')
        ->with('message', ['type' => 'success', 'text' => 'You are now logged out']);
}

最後に

以上です。記事では、Google Calendar APIを組み込んだPHPアプリの構築方法を見てきました。その中で、PHPを使ってGoogleカレンダーを操作する方法も説明しました。

(原文:Calendar as a Service in PHP? Easy, with Google Calendar API!

[翻訳:薮田佳佑/編集:Livit

Web Professionalトップへ

WebProfessional 新着記事