今回はToDoリストを作ります。
リストページの他にゴミ箱ページを使って、リスト→ゴミ箱へ、ゴミ箱→リストへを移動させるようにします。
サーバーとの通信はAjaxを使い、非同期で行う感じです。
ソースコード(Github)
2つの方法用意してます。Laravelのインストールからやる場合は、「一から作成」、完成版を見たい方は「完成版」をクローンしてみてください。
一から作成
git clone <https://github.com/sho55/todo-app-laravel9.git>
完成版
git clone -b created_demo https://github.com/sho55/todo-app-laravel9.git
開発環境
Laravel 9.*
MySQL8.0
PHP8.1
jQuery(Ajax) 3.6
dockerファイルとdocker-compose.yml作成
ファイル構成は以下の通り。
docker-config
├── mysql
│ ├── data //データの保管用
│ └── my.cnf
├── nginx
│ ├── Dockerfile
│ └── default.conf
└── php
├── Dockerfile
└── php.ini
順に解説していきます。
PHP
docker-config/php/php.ini
[Date]
date.timezone = "Asia/Tokyo"
[xdebug]
xdebug.remote_enable = On
xdebug.remote_port = 9001
xdebug.remote_autostart = On
xdebug.remote_host = 192.168.99.1
xdebug.profiler_output_dir = "/tmp"
xdebug.max_nesting_level= 1000
xdebug.idekey = "PHPSTORM"
docker-config/php/Dockerfile
FROM php:8.1-fpm
WORKDIR /var/www
ADD . /var/www
RUN chown -R www-data:www-data /var/www
COPY php.ini /usr/local/etc/php/
# install composer
RUN cd /usr/bin && curl -s <http://getcomposer.org/installer> | php && ln -s /usr/bin/composer.phar /usr/bin/composer
# install packages
RUN apt-get update && \\
apt-get -y install --no-install-recommends npm libzip-dev libicu-dev libonig-dev libmcrypt-dev git unzip vim mariadb-client curl gnupg openssl && \\
apt-get clean && \\
rm -rf /var/lib/apt/lists/* && \\
docker-php-ext-install intl pdo_mysql zip bcmath mbstring mysqli
# install stable node and latest npm
RUN curl -sL <https://deb.nodesource.com/setup_10.x> | bash
RUN apt-get install -y nodejs
# RUN apt-get install -y npm
RUN npm install -g n
RUN n stable
RUN npm update -g npm
docker-config/php/php.ini
[Date]
date.timezone = "Asia/Tokyo"
[xdebug]
xdebug.remote_enable = On
xdebug.remote_port = 9001
xdebug.remote_autostart = On
xdebug.remote_host = 192.168.99.1
xdebug.profiler_output_dir = "/tmp"
xdebug.max_nesting_level= 1000
xdebug.idekey = "PHPSTORM"
[mysqlnd]
mysqlnd.collect_memory_statistics = on
Nginx
docker-config/nginx/default.conf
server {
listen 80;
root /var/www/public;
index index.php index.html;
allow all;
access_log /var/log/nginx/ssl-access.log;
error_log /var/log/nginx/ssl-error.log;
location / {
root /var/www/public;
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \\.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\\.php)(/.+)$;
fastcgi_pass web:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
# CORS start
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods "POST, GET, OPTIONS";
add_header Access-Control-Allow-Headers "Origin, Authorization, Accept";
add_header Access-Control-Allow-Credentials true;
# CORS end
}
}
docker-config/nginx/Dockerfile
FROM alpine:3.6
RUN apk update && \\
apk add --no-cache nginx
RUN mkdir -p /run/nginx
# フォアグラウンドでnginx実行
CMD nginx -g "daemon off;"
MySQL
docker-config/mysql/my.cnf
# MySQLサーバーへの設定
[mysqld]
# 文字コード/照合順序の設定
character-set-server = utf8mb4
collation-server = utf8mb4_bin
# タイムゾーンの設定
default-time-zone = 'Asia/Tokyo'
log_timestamps = SYSTEM
# デフォルト認証プラグインの設定
default-authentication-plugin = mysql_native_password
# エラーログの設定
# log-error = /var/log/mysql/mysql-error.log
# スロークエリログの設定
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 5.0
log_queries_not_using_indexes = 0
# 実行ログの設定
general_log = 1
general_log_file = /var/log/mysql/mysql-query.log
# mysqlオプションの設定
[mysql]
# 文字コードの設定
default-character-set = utf8mb4
# mysqlクライアントツールの設定
[client]
# 文字コードの設定
default-character-set = utf8mb4
docker-compose.yml
version: '3'
services:
web:
build: ./docker-config/php
volumes:
- ./src/:/var/www/
working_dir: /var/www/
depends_on:
- mysql
nginx:
image: nginx
build: ./docker-config/nginx
ports:
- "81:80"
volumes:
- ./src/:/var/www
- ./docker-config/nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- web
mysql:
image: mysql:8.0
ports:
- 3316:3306
environment:
MYSQL_DATABASE: mainsys1
MYSQL_ROOT_USER: root
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: my_user
MYSQL_PASSWORD: my_user
TZ: 'Asia/Tokyo'
volumes:
- ./docker-config/mysql/data:/var/lib/mysql
- ./docker-config/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
depends_on:
- mysql-volume
mysql-volume:
image: busybox
volumes:
- ./docker-config/mysql:/var/lib/mysql
phpmyadmin:
image: phpmyadmin/phpmyadmin
environment:
- PMA_ARBITRARY=1
- PMA_HOST=mysql
- PMA_USER=root
- PMA_PASSWORD=root
links:
- mysql
ports:
- 8081:80
volumes:
- /sessions
Dockerの立ち上げ
Dockerを立ち上げていきます。 「-d」はデーモンを使うということで、裏側で動かすために必要です。
# docker-compose up -d
//コンテナに入る
# docker exec -it php9-laravel8-mysql8_web_1 bash
Laravelインストール
今回はLaravel9(2022年5月時点で最新)を導入します。
今インストールしたらversion9の指定をしてなくてもいいんですが、一応。
composer create-project laravel/laravel:^9.0 src
cd src
環境変数を修正します。 回はDBのところだけでOKです。
.env
APP_NAME=ToDoApp
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=mainsys1
DB_USERNAME=root
DB_PASSWORD=root
BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
マイグレーションファイル作成
DBのテーブル、カラムを用意します。
php artisan make:migration create_posts_table --create=posts
ファイル構成
create_posts_table
create_posts_table
public function up() { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->string('text'); $table->smallInteger('complete_flag'); $table->softDeletes($column = 'deleted_at', $precision = 0); $table->timestamps(); }); }
マイグレーションしていきます。
php artisan migrate
修正ファイル
config/app.php
'timezone' => 'UTC',
↓
'timezone' => 'Asia/Tokyo',
コントローラーとモデル作成
コントローラーとモデルを作っていきます。
今回はPostという名前にしております。
php artisan make:controller PostController --model=Post
モデルの修正
論理削除を行うので、SoftDeletes
をトレイトしております。
Post.php
<?php
namespace App\\Models;
use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
use Illuminate\\Database\\Eloquent\\Model;
use Illuminate\\Database\\Eloquent\\SoftDeletes;
class Post extends Model
{
use HasFactory;
use SoftDeletes;
protected $table = 'posts';
protected $fillable = [
'text',
'complete_flag'
];
}
パッケージをインストール
今回用意するのは以下の通りです。
・Laravel UI : Bootstrapを追加するため
・Bootstrap : デザインを簡単に作るフレームワーク
・jQuery : サーバーと非同期通信をするAjaxの元
・FontAwesome : ゴミ箱マークなどのアイコンを使う
順に入れていきます。
Laravel UI
//Laravel UIパッケージ
$ composer require laravel/ui
BootStrap
$ php artisan ui bootstrap --auth
jQuery
$ npm install jquery --save-dev
fontawesome
$ npm install @fortawesome/fontawesome-free
npmのリストを確認する
//確認
/var/www# npm ls
www@ /var/www
+-- @fortawesome/fontawesome-free@6.1.1
+-- @popperjs/core@2.11.5
+-- autoprefixer@10.4.5
+-- axios@0.25.0
+-- bootstrap@5.1.3
+-- jquery@3.6.0 //追加されている
+-- laravel-mix@6.0.43
+-- lodash@4.17.21
+-- postcss@8.4.13
+-- resolve-url-loader@5.0.0
+-- sass-loader@11.1.1
`-- sass@1.51.0
jQueryをLaravelで使えるようにする
このままでは使えないので、以下を追加しましょう。
resources/js/bootstrap.js
~
window.$ = window.jQuery = require('jquery')
Font awesomeも使えるようにする
以下を追加します。
resources/sass/app.scss
// Font Awesome
@import '~@fortawesome/fontawesome-free/scss/fontawesome';
@import '~@fortawesome/fontawesome-free/scss/regular';
@import '~@fortawesome/fontawesome-free/scss/solid';
@import '~@fortawesome/fontawesome-free/scss/brands';
npmをコンパイル
以下を叩くことで、jQueryやFont awesomeが正常に使えるようになります。
npm run dev
※開発をする時は、いちいちコマンドを叩くのが面倒なので、以下を使って自動でコンパイル処理をしていくのが効率的です。
npm run watch
npmでのWarnig対応
僕がコンパイルした時に以下の状態になりました。
1 WARNING in child compilations (Use 'stats.children: true' resp. '--stats-children' for more details)
というものが出まして、しらべたところ以下の対処をするみたいです。
/webpack.mix.js
mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css')
~
// 以下を追加
mix.webpackConfig({
stats: {
children: true,
},
});
追加してもう一度コンパイルをしてみます。
そうすると別のワーニングが出てきました。
WARNING in ./resources/sass/app.scss (./node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[5].use[1]!./node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[5].use[2]!./node_modules/resolve-url-loader/index.js??ruleSet[1].rules[5].use[3]!./node_modules/sass-loader/dist/cjs.js??ruleSet[1].rules[5].use[4]!./resources/sass/app.scss) Module Warning (from ./node_modules/postcss-loader/dist/cjs.js): Warning (2423:3) autoprefixer: Replace color-adjust to print-color-adjust. The color-adjust shorthand is currently deprecated.
どうやら、autoprefixerというものが必要らしい。
なので以下を叩きます。
npm install autoprefixer@10.4.5 --save-exact
その後、再度コンパイルします。
$ npm run dev
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬──────────┐
│ File │ Size │
├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────┤
│ /js/app.js │ 2.23 MiB │
│ css/app.css │ 202 KiB │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴──────────┘
Child mini-css-extract-plugin
~~
webpack compiled successfully
これでOKな様子ですが、皆さんはどうでしょうか?
Viewの作成
画面を作っていきましょう。
今回はリストとゴミ箱の2つのみです。
resources/views/posts/index.blade.php
@extends('layouts.app')
@section('content')
<style>
input[type="button"],
.trash-area,
.body-area{
cursor: pointer;
}
.trash-area:hover{
opacity: 0.5;
}
</style>
<div class="container">
<div class="row justify-content-center">
<div class="col-12 col-md-6">
<label for="task-input">New Task</label>
<div class="form-group d-flex mb-4">
{{-- @csrf --}}
<input type="text" name="task" id="task-input" class=" form-control mr-3">
<input type="button" value="追加" onClick="createTask()" class="btn btn-outline-primary mx-3">
</div>
<table class="w-100 table table-hover">
<thead>
<tr>
<th><i class="fas fa-check-square"></i></th>
<th>タスク</th>
<th>日付</th>
<th class="float-end">ゴミ箱へ</th>
</tr>
</thead>
<tbody class="tr_lists">
@foreach ($posts as $post)
<tr id="tr_{{$post->id}}" class="@if($post->complete_flag == 1) bg-success @endif">
<td><input type="checkbox" name="task-done" id="checkbox_{{$post->id}}" onChange="checkChange( {{$post->id}} )" @checked($post->complete_flag == 1) ></td>
<td class="w-50"><label for="checkbox_{{$post->id}}"><span class="body-area">{{$post->text}}</span></label></td>
<td><span class="date-area">{{$post->create_time}}</span></td>
<td><span class="trash-area float-end" onClick="goToTrash({{$post->id}})"><i class="fas fa-trash fa-2xl"></i></span></td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
<script>
function createTask() {
const task = $("#task-input").val();
console.log(task);
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
$.ajax({
type: 'post',
data: {
'task' : task
},
datatype: 'json',
url: '/create'
})
.done(function(data){
console.log(data.post);
$("#task-input").val('');
// $('tbody.tr_lists').empty();
for (let i = 0; i < data.post.length; i++) {
const element = data.post[i];
console.log(element);
var el = '';
el+= '<tr id="tr_'+element.id+'" class="">';
el+= '<td><input type="checkbox" name="task-done" id="checkbox_'+element.id+'" onChange="checkChange('+element.id+')"></td>';
el+= '<td class="w-50"><label for="checkbox_'+element.id+'"><span class="body-area">'+element.text+'</span></label></td>';
el+= '<td><span class="date-area">'+element.create_time+'</span></td>';
el+= '<td><span class="trash-area float-end" onClick="goToTrash('+element.id+')"><i class="fas fa-trash fa-2xl"></i></span></td>';
el+= '</tr>';
$('tbody.tr_lists').prepend(el);
}
})
.fail(function(data){
console.log(data);
alert("error!");
});
}
function goToTrash(id) {
console.log(id);
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
$.ajax({
type: 'post',
data: {
'id' : id
},
datatype: 'json',
url: '/softdelete'
})
.done(function(data){
//json = JSON.parse(data);
console.log(data);
$('#tr_'+id).remove();
})
.fail(function(data){
console.log(data);
alert("error!");
});
}
function checkChange(id) {
var is_checked = $('#checkbox_'+id).prop("checked");
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
$.ajax({
type: 'post',
data: {
'id' : id,
'is_checked' : is_checked
},
datatype: 'json',
url: '/check/change'
})
.done(function(data){
//json = JSON.parse(data);
console.log(data);
if(data == 1){
$('#tr_'+id).addClass('bg-success');
}else{
$('#tr_'+id).removeClass('bg-success');
}
})
.fail(function(data){
console.log(data);
alert("error!");
});
}
</script>
@endsection
createTask()
では通信で戻ってきたデータをDOM操作で一番上に表示するという仕様にしてます。
次にゴミ箱の画面が次の通り。
resources/views/posts/trash.blade.php
@extends('layouts.app') @section('content') <style> .trash-area, .body-area { cursor: pointer; } .trash-area:hover { opacity: 0.5; } </style> <div class="container"> <div class="row justify-content-center"> <div class="col-12 col-md-6"> <label for="task-input"><p>ゴミ箱<i class="fas fa-trash"></i></p></label> @if( count($posts) > 0) <table class="w-100 table table-hover"> <thead> <tr> <th>タスク</th> <th>日付</th> <th class="float-end">元に戻す</th> </tr> </thead> <tbody> @foreach ($posts as $post) <tr id="tr_{{$post->id}}"> <td class="w-50"><span class="body-area">{{$post->text}}</span></td> <td><span class="date-area">{{$post->create_time}}</span></td> <td><span class="trash-area float-end" onClick="restore({{$post->id}})"><i class="fas fa-undo fa-2xl"></i></span> </td> </tr> @endforeach </tbody> </table> <form action="/delete" method="post" onSubmit="return emptyTrash()"> @csrf <button class="btn btn-outline-danger" type="submit">ゴミ箱を空にする</button> </form> @else <p>データはありません。</p> @endif </div> </div> </div> <script> // $(function(){ function restore(id) { console.log(id); $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') } }); $.ajax({ type: 'post', data: { 'id' : id }, datatype: 'json', url: '/restore' }) .done(function(data){ console.log(data); $('#tr_'+id).remove(); }) .fail(function(data){ console.log(data); alert("error!"); }); } function emptyTrash() { if(window.confirm('本当に実行しますか?')) { return true; } else { return false; } } // }); </script> @endsection
emptyTrash()
はダイアログをつけて、削除の確認を行ってます。
全体にconsole.logでデータのテストをしておりますので、適宜削除してOKです。
コントローラーの編集
リストとゴミ箱で分けても構いませんが、今回は1つに表示から削除まで入れております。
以下の通りです。
app/Http/Controllers/PostController.php
<?php
namespace App\\Http\\Controllers;
use App\\Models\\Post;
use Illuminate\\Http\\Request;
use Illuminate\\Support\\Str;
use Illuminate\\Support\\Facades\\Log;
use Illuminate\\Support\\Facades\\DB;
class PostController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \\Illuminate\\Http\\Response
*/
public function index()
{
$posts = $this->_getDisplayLists();
return view('posts.index',['posts'=> $posts]);
}
/**
* creating a new resource.
*
* @return \\Illuminate\\Http\\Response
*/
public function create(Request $request)
{
$result = Post::create([
'text' => $request->task
]);
$post = DB::table('posts')
->selectRaw('id,text,complete_flag,DATE_FORMAT(created_at, "%Y-%m-%d %H:%i") AS create_time')
->where('id', $result->id)->get();
return response()->json( ['post' => $post]);
}
/**
* change the specified resource.
*
* @return \\Illuminate\\Http\\Response
*/
public function checkedChange(Request $request)
{
$is_checked = $request->is_checked === 'true' ? 1 : 0;
// Log::debug($is_checked);
$posts = DB::table('posts')
->where('id',$request->id)
->update(['complete_flag' => $is_checked]);
return $is_checked;
}
/**
* Display a listing of the trash resource.
*
* @return \\Illuminate\\Http\\Response
*/
public function trash()
{
$posts = $this->_getTrashLists();
return view('posts.trash', ['posts' => $posts]);
}
/**
* SoftDelete the specified resource from storage.
*
* @param \\App\\Models\\Post $post
* @return \\Illuminate\\Http\\Response
*/
public function goToTrash(Request $request)
{
$result = Post::destroy($request->id);
return $result;
}
/**
* Back to store the specified resource from storage.
*
* @param \\App\\Models\\Post $post
* @return \\Illuminate\\Http\\Response
*/
public function restore(Request $request)
{
$result = Post::where('id',$request->id)->withTrashed()->restore();
return redirect()->route('post.trash');
}
/**
* Remove the specified resource from storage.
*
* @return \\Illuminate\\Http\\Response
*/
public function delete()
{
$result = DB::table('posts')->whereNotNull('deleted_at')->delete();
$posts = $this->_getTrashLists();
return redirect()->route('post.trash', ['post' => $posts]);
}
// SQL
private function _getDisplayLists()
{
$result = DB::table('posts')
->selectRaw('id,text,complete_flag,DATE_FORMAT(created_at, "%Y-%m-%d %H:%i") AS create_time')
->whereNull('deleted_at')
->orderByRaw('created_at DESC')
->get();
return $result;
}
private function _getTrashLists()
{
$result = DB::table('posts')
->selectRaw('id,text,complete_flag,DATE_FORMAT(created_at, "%Y-%m-%d %H:%i") AS create_time')
->whereNotNull('deleted_at')
->orderByRaw('created_at DESC')
->get();
return $result;
}
}
下2つのprivate関数はよく使うものをまとめております。
論理削除がポイントなので、リストを表示する時は->whereNull('deleted_at')
を使っており、
ゴミ箱は->whereNotNull('deleted_at')
で表示してます。
Log:debug
はテスト用に出力しているものなので、消しちゃって構いません。
ルーティングを作成
ルーティングのところに、Laravel UIを入れた時に入っている、LoginやRegisterなどが入ってますが、全て消してOKです。
今回使うのは以下通りです。
use App\\Http\\Controllers\\PostController;
Route::get('/', [PostController::class, 'index'])->name('post.index');
Route::get('/trash', [PostController::class, 'trash'])->name('post.trash');
Route::post('/create', [PostController::class, 'create']);
Route::post('/check/change', [PostController::class, 'checkedChange']);
Route::post('/softdelete', [PostController::class, 'goToTrash']);
Route::post('/restore', [PostController::class, 'restore']);
Route::post('/delete', [PostController::class, 'delete']);
get通信になっているのがリストページとゴミ箱ページでして、post通信になっているのが非同期処理で行う部分です。
use ~ PostController;と宣言するのが必須です。