Vue.jsとの共同開発にて、出退勤の時間を打刻するアプリを作成中です。
DBに時間を格納する方法にはCarbonを使えばいいと聞き、不明点が多くLaravelのみで動作検証したかったため試してみました。
DBに時間を格納し、扱うのが初めてでデータ型をどうしようと思ったのですが、dateTime型で成功しました。
以下を参考にしました。
Carbon:Introduction
PHPで日付時刻処理を書くならCarbonを使うべき
環境
- Laravel 5.7.19
- MySQL 8.0.12
Carbonを使った日時データの格納
LaravelにはCarbonが用意されてます。ない場合は以下の方法でComposer使ってインストール。
composer require nesbot/carbon
DB設計について
本筋から外れてしまいますがログインについてはメールアドレスを使ってではなく、ログインIDでのログインとします。
Usersテーブル
<?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
Timestampsテーブル
次に打刻用データベースです。
出勤の打刻と同時にカラムを作成、退勤時間はNullに設定しました。もちろん、Usersテーブルのidと外部キー制約でまとめます。
<?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
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
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
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
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をご覧ください。


