【Laravel】Carbonを使って日時をDBに格納する打刻システム

Vue.jsとの共同開発にて、出退勤の時間を打刻するアプリを作成中です。
DBに時間を格納する方法にはCarbonを使えばいいと聞き、不明点が多くLaravelのみで動作検証したかったため試してみました。<br>

DBに時間を格納し、扱うのが初めてでデータ型をどうしようと思ったのですが、dateTime型で成功しました。

以下を参考にしました。
[Carbon:Introduction](https://carbon.nesbot.com/docs/)
[PHPで日付時刻処理を書くならCarbonを使うべき](https://qiita.com/yudsuzuk/items/ff894bd0b76d4657741d)

## 環境
- Laravel 5.7.19
- MySQL 8.0.12


# Carbonを使った日時データの格納

LaravelにはCarbonが用意されてます。ない場合は以下の方法でComposer使ってインストール。<br>

```shell
composer require nesbot/carbon
```


### DB設計について

![スクリーンショット 2019-01-17 22.01.15.png](https://qiita-image-store.s3.amazonaws.com/0/222848/4219fe44-3700-f6aa-5d2b-72b2118f8bf4.png)

本筋から外れてしまいますがログインについてはメールアドレスを使ってではなく、ログインIDでのログインとします。<br>

### Usersテーブル

```php:database/migration/create_users_table.php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('loginid')->unique()->comment('ログインID');
            $table->string('password');
            $table->tinyInteger('role')->unsigned()->default(10)->comment('権限0:system  5:admin  10:user');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
}
```

上記に伴い、`app/Http/Controllers/Auth/LoginController.php`を変更します。

参照：[app/Http/Controllers/Auth/LoginController.php](https://github.com/rinonkia/AttendanceSystem/commit/4003e042a625c45dba72ceaa4be47093730a432f#diff-7c891627c8bea7abd474ebe4712cee5f)

### Timestampsテーブル

次に打刻用データベースです。<br>

出勤の打刻と同時にカラムを作成、退勤時間はNullに設定しました。もちろん、Usersテーブルのidと外部キー制約でまとめます。<br>

```php:database/migration/create_timestamps_table.php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateTimestampsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('timestamps', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('user_id')->unsigned()->index();
            $table->dateTime('punchIn');
            $table->dateTime('punchOut')->nullable();
            $table->timestamps();

            $table->foreign('user_id')->references('id')->on('users');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('timestamps');
    }
}

```

## 1対多リレーション

UserモデルとTimestampモデルを用意し、一対多の構造用意。

```php:app/User
<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable;

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

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

    /**
     * Timestamp関連付け
     * 1対多
     */
    public function timestamp()
    {
        return $this->hasMany(Timestamp::class);
    }
}
```




```php:app/Timestamp.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Timestamp extends Model
{
    protected $fillable = ['user_id', 'punchIn', 'punchOut'];

    /**
     * ユーザー関連付け
     * 1対多
     */
    public function user()
    {
        $this->belongsTo(User::class);
    }
}
```
## ルート

ルーティングは以下のようにコーディングしました。
AuthServiceProviderを使った認証でページのアクセスをadminとuserで制限しています。

参照：[app/Providers/AuthServiceProvider.php](https://github.com/rinonkia/AttendanceSystem/blob/master/app/Providers/AuthServiceProvider.php)


```php:routes/web.php

Route::get('/', function () {
    return view('home');
})->middleware('auth');


Route::get('/login', 'Auth\LoginController@showLoginForm')->name('login');
Route::post('/login', 'Auth\LoginController@login');
Route::post('/logout', 'Auth\LoginController@logout')->name('logout');


Route::group(['middleware' => ['auth', 'can:admin']], function() {
    Route::get('admin/user/index', 'UserController@index')->name('admin/user/index');
    Route::get('admin/user/show/{id}', 'UserController@show')->name('admin/user/show');
});

Route::group(['middleware' => 'auth'], function() {
    Route::post('/punchin', 'TimestampsController@punchIn')->name('timestamp/punchin');
    Route::post('/punchout', 'TimestampsController@punchOut')->name('timestamp/punchout');
});
```

### コントローラー

以下のような挙動を考えて設計しました。

 - 出勤ボタンを連続して押せない（同日中）
 - 出勤打刻がない状態で退勤打刻を押せない

(2019/02/07 TimestampsController一部修正)


```php:app/Http.TimestampsController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Auth;
use Carbon\Carbon;
use App\User;
use App\Timestamp;

class TimestampsController extends Controller
{
    public function punchIn()
    {
        $user = Auth::user();

        /**
         * 打刻は1日一回までにしたい 
         * DB
         */
        $oldTimestamp = Timestamp::where('user_id', $user->id)->latest()->first();
        if ($oldTimestamp) {
            $oldTimestampPunchIn = new Carbon($oldTimestamp->punchIn);
            $oldTimestampDay = $oldTimestampPunchIn->startOfDay();
        }
        
        $newTimestampDay = Carbon::today();

        /**
         * 日付を比較する。同日付の出勤打刻で、かつ直前のTimestampの退勤打刻がされていない場合エラーを吐き出す。
         */
        if (($oldTimestampDay == $newTimestampDay) && (empty($oldTimestamp->punchOut))){
            return redirect()->back()->with('error', 'すでに出勤打刻がされています');
        }

        $timestamp = Timestamp::create([
            'user_id' => $user->id,
            'punchIn' => Carbon::now(),
        ]);

        return redirect()->back()->with('my_status', '出勤打刻が完了しました');
    }

    public function punchOut()
    {
        $user = Auth::user();
        $timestamp = Timestamp::where('user_id', $user->id)->latest()->first();

        if( !empty($timestamp->punchOut)) {
            return redirect()->back()->with('error', '既に退勤の打刻がされているか、出勤打刻されていません');
        }
        $timestamp->update([
            'punchOut' => Carbon::now()
        ]);

        return redirect()->back()->with('my_status', '退勤打刻が完了しました');
    }
}
```

View周りはGitHubをご覧ください。

https://github.com/rinonkia/AttendanceSystem