Laravelを使って班分け（グループ分け）してみた


# はじめに
既に良記事があるのですが、
総人数n人の場合に、自分の想い通りに班分け（グループ分け）するためにはどうすればいいのか
記録のためにこの記事を書きます。
これは自分の所属するコミュニティー内で出題された特別課題です。
Laravelでやりましたが、素のPHPでもできるかと思います。

## 要件
・n人の班分けをする
・メンバー新規登録可（名前入力で追加）
・基準は4人1組, 余りが出たら4人の組に入れて５人組にする(人数が少ない場合は適度にちょうど良いバランスにする→６人、７人、１１人がイレギュラー)
・例として20人の場合は5人×4グループではなく、4人×5グループとする
・各個人削除可、全員いっぺんに削除も可能にする（ボタン設置）
仕様を変えたい場合微調整して下さい。

### 記事について
・DB接続など環境構築はやってある前提で省きます
・Eloquentなどについても説明省きます

##画面
こんな感じ
<img width="813" alt="スクリーンショット 2019-10-22 14.23.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517865/1eae1a81-1269-f80a-a838-d79a59e96294.png">
<img width="813" alt="スクリーンショット 2019-10-22 14.24.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517865/998f359a-d9f5-6c5d-d9da-c4ab05643cf2.png">
<img width="813" alt="スクリーンショット 2019-10-22 14.25.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517865/2b0f3aa7-1b17-0c6c-fe76-36a9b04a6fa2.png">




# コードと解説

## ルーティング
```web.php
Route::get('/sub', 'GroupController@group')->name('sub.group');
Route::post('/sub/delete/{id}', 'GroupController@delete')->name('group.delete');
Route::post('/sub/all/delete', 'GroupController@all_delete')->name('all.delete');
Route::get('/sub/all/delete', 'GroupController@all_delete');
Route::post('/sub/add', 'GroupController@add')->name('group.add');
Route::post('/sub/devide', 'GroupController@devide')->name('group.devide');
```

ここは基本的な部分かと思いますので説明省略。

##　コントローラー
```GroupController.php
<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\SubUser;

class GroupController extends Controller
{
    public function group()
    {
        $sub_users = SubUser::get();
        $count = SubUser::count();
        return view('sub.group', compact('sub_users', 'count'));
    }
    public function add(Request $request)
    {
        $validate_rule = [
            'name' => 'required',
        ];
        $message = [
            'name.required' => '名前:必須項目です',
        ];
        $this->validate($request, $validate_rule, $message);
        $sub_user = new SubUser;
        $sub_user->name = $request->name;
        $sub_user->save();
        return redirect()->route('sub.group')->with('flash_message', '追加しました');
    }
    public function delete($id)
    {
        $sub_user = SubUser::find($id);
        $sub_user->delete();
        return redirect()->route('sub.group')->with('flash_message', '消去しました');
    }
    public function all_delete()
    {
        SubUser::query()->delete();
        return redirect()->route('sub.group')->with('flash_message', '全員消去しました');
    }
    public function devide()
    {
        $count = SubUser::count();
        $sub_users = SubUser::pluck('name')->toArray();
        shuffle($sub_users);
        $division = floor($count / 4);
        $remainder = $count % 4;
        $offset = 0;
        $groups = array();
        if ($count <= 5) {
            $groups[] = $sub_users;
        } elseif ($count == 6) {
            $groups = array_chunk($sub_users, 3);
        } elseif ($count == 7 || $count == 11) {
            $groups = array_chunk($sub_users, 4);
        } else {
            for ($i = 0; $i < $division; $i++) {
                if (empty($remainder)) {
                    $length = 4;
                } else {
                    $length = 5;
                    //余りを0になるまで減らす
                    $remainder--;
                }
                $groups[] = array_slice($sub_users, $offset, $length);
                $offset += $length;
            }
        }
        return view('sub.devide', compact('groups'));
    }
}
```
## 解説
少し区切って解説していきます。

```GroupController.php
public function group()
    {
        $sub_users = SubUser::get();
        $count = SubUser::count();
        return view('sub.group', compact('sub_users', 'count'));
    }
```
DBのsub_usersテーブルからデータを全て取得。
（usersは既に登録済みだったので。membersとかでもいいかもしれません）
総人数も知りたいので、$countで取得し、それをviewに渡します。

```GroupController.php
public function add(Request $request)
    {
        $validate_rule = [
            'name' => 'required',
        ];
        $message = [
            'name.required' => '名前:必須項目です',
        ];
        $this->validate($request, $validate_rule, $message);
        $sub_user = new SubUser;
        $sub_user->name = $request->name;
        $sub_user->save();
        return redirect()->route('sub.group')->with('flash_message', '追加しました');
    }
```
これはメンバー新規登録の部分です。
入力がないのに登録ボタンを押された場合、何らかのアクションが起こった方がいいので
バリデーションをかけました。
フォームから入力された情報をDBに新規保存し、flash messageで追加した旨を表示します。

```php
    public function delete($id)
    {
        $sub_user = SubUser::find($id);
        $sub_user->delete();
        return redirect()->route('sub.group')->with('flash_message', '消去しました');
    }
    public function all_delete()
    {
        SubUser::query()->delete();
        return redirect()->route('sub.group')->with('flash_message', '全員消去しました');
    }
```
deleteが個別削除。そのメンバーのidから、その人だけを削除しています。
all_deleteは全員削除です。

### 実装するにあたり一番重要なところ
```php
    public function devide()
    {
        $count = SubUser::count();
        $sub_users = SubUser::pluck('name')->toArray();
        shuffle($sub_users);
        $division = floor($count / 4);
        $remainder = $count % 4;
        $offset = 0;
        $groups = array();
        if ($count <= 5) {
            $groups[] = $sub_users;
        } elseif ($count == 6) {
            $groups = array_chunk($sub_users, 3);
        } elseif ($count == 7 || $count == 11) {
            $groups = array_chunk($sub_users, 4);
        } else {
            for ($i = 0; $i < $division; $i++) {
                if (empty($remainder)) {
                    $length = 4;
                } else {
                    $length = 5;
                    //余りを0になるまで減らす
                    $remainder--;
                }
                $groups[] = array_slice($sub_users, $offset, $length);
                $offset += $length;
            }
        }
        return view('sub.devide', compact('groups'));
    }
}
```

$countはメンバーの総人数です。

```php
SubUser::pluck('name')->toArray();
```

メンバー全員を名前の配列にします。
<img width="119" alt="スクリーンショット 2019-10-22 14.48.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517865/3af30608-5f85-1eef-9a5a-419e63769a86.png">

```php
shuffle($sub_users);
```
全員をシャッフル。

```php
$division = floor($count / 4);
```

総人数を４で割り、その整数を$divisionに入れます。(基準にしたい人数で割る）
４人が何組いるかを表しています。

```php
$remainder = $count % 4;
```

4で割った余りが幾つになるか調べます。（基準にしたい人数で割る）
余りの数だけ、5人組が必要ということになります。

```php
$offset = 0;
```
array_slice(配列(sub_users), どこから切り出すか(offset), 要素の長さ(length)）;
$offsetというのは[array_slice](https://www.php.net/manual/ja/function.array-slice.php)を使うときに、配列のどこから切り出したいか、設定するために作っています。（後で出てきます）
班分けするにあたり、一番最初は先頭から切り出したいので０にします。


```php
$groups = array();
```
[配列を初期化](https://uxmilk.jp/14771)
viewに渡すデータを配列にしたい、後々配列を追加したいので、この処理をしています。

```php        
        if ($count <= 5) {
            $groups[] = $sub_users;
        } elseif ($count == 6) {
            $groups = array_chunk($sub_users, 3);
        } elseif ($count == 7 || $count == 11) {
            $groups = array_chunk($sub_users, 4);
```
総人数5人以下の時はシャッフルした配列をそのまま出力します。

６人（3,3） と 7人(4,3) と 11人(4,4,3) の時、[array_chunk](https://www.php.net/manual/ja/function.array-chunk.php)を使います。
array_chunk(配列(sub_users), サイズ（数字));
サイズで配列を分割、余りにはそのまま配列に残ります。
ちょうど良く割れる場合以外は、最後の部分の要素数はサイズより小さくなります。

```php
        } else {
            for ($i = 0; $i < $division; $i++) {
                if (empty($remainder)) {
                    $length = 4;
                } else {
                    $length = 5;
                    //余りを0になるまで減らす
                    $remainder--;
                }
                $groups[] = array_slice($sub_users, $offset, $length);
                $offset += $length;
            }
        }
```
その他の場合は全て[array_slice](https://www.php.net/manual/ja/function.array-slice.php)を使います。

array_slice(配列(sub_users), どこから切り出すか(offset), 要素の長さ(length)）;
という使い方をします。

```php
for ($i = 0; $i < $division; $i++) {
```

divisonは4人が何組必要か、でした。
3組必要（12〜15人）であれば、forの中身が３回繰り返されるということになります。

```php
                if (empty($remainder)) {
                    $length = 4;
                } else {
                    $length = 5;
                    //余りを0になるまで減らす
                    $remainder--;
                }
```

remainderは余りです。
もし余りがなければ(empty(remainder))、全組4人でいいので、lengthは4になります。
もし余りが３あるとすると、１人×3あぶれているということです。
その場合は最初の3組に一人ずつ足したい、最初の3組だけ5人組にしたいということで、
ということは、余りの分だけ５人組を作る必要がある、ということで

```php
$remainder--;
```
これを付け加えています。
(総人数11人の場合は、2余り3となり、余りの方が商より大きいので5人が3組作れません。)

```php
$groups[] = array_slice($sub_users, $offset, $length);
```
既にある配列に作った配列を追加します。
[配列に配列を追加](https://qiita.com/kazu56/items/6947a0e4830eb556d575)

```php
$offset += $length;
```

offsetは配列のどこから切り出すか、なので、最初の4-5人を切り出したあと、次はどこから切り出せばいいかというと、5-6人目から（ちょうど、長さ分プラスしたところから）となります。
この処理をしないと毎回配列の最初から4人か５人切り取ってくることになります。


```php
return view('sub.devide', compact('groups'));
```
そして最後にviewにgroupを渡して終わりです。


#入力/削除画面、メンバー表示画面のBlade
```html:group.blade.php
@extends('layouts.app')

@section('sub.group')
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">Dashboard</div>
<br>
@if ($errors->any())
<div class ="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
@if (session('flash_message'))
<div class="flash_message">
{{ session('flash_message') }}
</div>
@endif
@if ($sub_users->isEmpty())
メンバーがいません<br>
@else
人数：{{$count}}名<br>
<table border="1">
<tr>
<th>名前</th>
<th>削除</th>
</tr>
@foreach ($sub_users as $sub_user)
<tr>
<td>{{$sub_user->name}}</td>
<td>
<form action="{{ route('group.delete', ['id' => $sub_user->id])}}" method="POST">
{{ csrf_field() }}
<input type="hidden" name="id" value="{{$sub_user->id}}">
<button type="submit">
削除
</button>
</form>
</td>
@endforeach
</table><br>
<form action="{{ route('all.delete')}}" method="POST">
{{ csrf_field() }}
@foreach ($sub_users as $sub_user)
<input type="hidden" name="id" value="{{$sub_user->id}}">
@endforeach
<button type="submit" onclick="return confirm('本当に全員消去して宜しいですか？')">
全員消去
</button>
</form>
@endif
<br>
<form action="{{ route('group.add')}}" method="POST">
{{ csrf_field() }}
<label>名前：<input id="name" type="text" name="name"></label>
<button type="submit">
登録
</button>
</form><br>
<form action="{{ route('group.devide')}}" method="POST">
{{ csrf_field() }}
<button type="submit">
班分け
</button>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
```
### ポイント
```html:group.blade.php
<button type="submit" onclick="return confirm('本当に全員消去して宜しいですか？')">
全員消去
</button>
```
全員消去前には確認するモーダルを立ち上げるようにしました。

# 班分け（グループ分け）結果表示のBlade
```html:devide.blade.php
@extends('layouts.app3')

@section('sub.devide')
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">Dashboard</div>
<br>
@if ($errors->any())
<div class ="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
@if (session('flash_message'))
<div class="flash_message">
{{ session('flash_message') }}
</div>
@endif
@foreach ($groups as $key => $group)
<?php $key++; ?>
{{$key}}班
<table border="1">
<tr>
<th>名前</th>
</tr>
@foreach ($group as $name)
<tr>
<td>{{$name}}</td>
</tr>
@endforeach
</table>
<br>
@endforeach
</div>
</div>
</div>
</div>
</div>
@endsection
```
### ポイント
```php:devide.blade.php
@extends('layouts.app3')
```
これは下記のJavaScriptを挿入するために書き換えました。

```html:app3.blade.php
<head>
<script>
alert('結果発表!!');
</script>
</head>
```
上記scriptをheadタグ内に入れることで、ページを読み込む前に結果発表というアラートを出すことができます。

```php:devide.blade.php
<?php $key++; ?>
```
上記コードを入れることで、キーを利用して、班の名前を1班、2班、3班と名付けることができます。

これで完成です。

#参考
[PHPで配列をn等分する（そして余りをどうするか）](http://bashalog.c-brains.jp/14/01/15-104933.php)
[array PHPマニュアル](https://www.php.net/manual/ja/ref.array.php)
[array_slice 参考記事](https://php.plus-server.net/function.array-slice.html)

#おわりに
どうしてこういうコードになるのか理解できると面白いなぁと思います。
自分の理解度チェックにもなりました。