【PHP】クロージャで連番を作成する

システム作成において連番やグループ番号を作成していくということがあります。その際に、クロージャを使用することで、色々と効率を上げることができます。

# クロージャ作成のおさらい
PHPでクロージャを用いるのはけっこう簡単で、だいたい、こんな式になります。注意点は**クロージャを使用する際には必ずセミコロンを忘れない**ことです（構文エラーになります）。

```php:php
function hoge(){
    return function() use(&$x){
        return $x;
    }; //このセミコロンを忘れないこと
}
echo $hoge(); //変数のまま呼び出す
```

## カウント処理用の関数を作成
基本文法を踏まえたところで、実際に作っていきます。ですが、今回は汎用性をもたせるために敢えてオブジェクト指向で作成します。オブジェクト指向で作成することで、システム内のあらゆる場面において連番作成を行うことができるからです。

この特長を活かして、クラス化した上で使用します。このオブジェクト指向でクロージャを用いる方法が、なかなか情報が少なく困っていましたが、以下の記事を参考にしました。

https://qiita.com/tricogimmick/items/3e344486658e3cfbd407

例の場合は任意の変数の後に連番を付与した場合です。

```php:php
	class Counter
	{
		private $cnt = 0;
		public function count(){
			$cnt = $this -> cnt;
			return function($name)use(&$cnt){
				$cnt++;
                return $name.$cnt;
			};
		}
	}

	$Counter = new Counter(); //インスタンス作成
	$c = $Counter -> count(); //インスタンスからエンクロージャを作成
    $names = ["さいたま","川口","川越","所沢","越谷","春日部","草加","上尾"];
	foreach($names as $name ){
		$vname = $c($name); //エンクロージャを呼び出す
	    echo $vname,"<br>";	//さいたま1、川口2、川越3…と表示される
	}
```

注意点は、インスタンスによって生成されたメソッドにシーケンスを作成したいデータを代入しないことです。また、インスタンスとメソッドによるクロージャ作成はループ文の外側で定義してください（ループ内で定義すると、逐一値が初期化されてしまうので、クロージャの意味がありません）。

クロージャのメリットは、**変数の値を継承してくれることで、継続的に同じ変数を処理できること**です。そして、実例では単一の変数に対して用いることが多いのですが、次のように**クロージャは配列にも応用できます**（しかも、**キーごとに格納された値も保持**してくれている）。

# 配列で連番を作成する
システム作成において、DBテーブルの処理が必要な案件にはよく連番やグループ番号を作成していくということがあります。そして、その度、各テーブルに問い合わせて最大値を取得するというような面倒なことをしていたのですが、クロージャを用いることで、その手間を省略し、パフォーマンスも大幅に向上させることができます。

そして、だいたい番号作成には以下の種類がパターン化しています。

- 初めて登場するグループ番号　→　初期値は1
- インポートされたデータで2回め以降に登場するグループ番号　→　システム内でインクリメントされた値
- 既にDBで登録されているグループ　→　DBテーブル内の最大値＋1

そして、これらの処理を一つの関数で処理して、作業を効率化しようというのがこの記事の狙いです。

# バルクインサートでグループ番号を一括生成する
システムを構築する際には、データを一括登録したいという要望を受けることがよくあります。そのシステム構築にはcsvデータやExcelデータなどを読み込み、DBテーブルにインポートしていくという流れで実装していきます。

そして、DBテーブルに登録していく場合、自分はよく**バルクインサート**を用います。バルクインサートとはinsert文一つで複数のデータ行を登録できるというもので

```sql
insert into trn_records values(1,1,"hoge"),(2,1,"fuga"),(1,2,"hoge")…………;
```

こんな感じで記述していきます。なぜ、このバルクインサートを活用するかといえば、もし登録データにエラーなどが生じた場合に処理を全部中断してくれるからです。これがプログラム内でinsert文を行数分繰り返したりすると、エラーが生じた場合に、その時点で中断されます。なので、次からどのデータを入れていけばいいのかわからない、また一括登録なので、インポートファイルに適宜行削除などの再調整が必要という面倒な事態に陥ることがあります。

ですが、このバルクインサートにおいて大きな問題点があります。それは**グループごとのシーケンスを取得するのが困難になる**からで、データの中には上述の３パターンが混在し、それらを逐一振り分けてデータを自動生成していく必要があります。なお、今回はPHPを用いていますが、原理さえわかれば、他の言語でも可能じゃないかと思います（`array_key_exists`と同様の挙動をする関数があれば、より簡単です）。

## プログラム作成
それで作ってみたプログラムの根幹が以下の部分です。原理は単純で`array_key_exists`関数を用いることによって、この３種類のパターンを簡単に処理できるようになります。array_key_exists関数は検索値のキーが存在するかを確認するための関数で、bool判定を返します。

## 新規か加算か？
新規に入ってくるグループか、インクリメントが必要なグループかを判別したい場合、対象となるグループ名でキーを作成し、それをarray_key_exist関数で判別すれば、簡単に振り分けが可能です。

```php:php
    //既に登録されたグループ名かを判定
	if(array_key_exists($row_value,$ar_count)){
		$ar_count[$row_value]++; //登録済ならグループ番号をインクリメントする
	}else{
		$ar_count[$row_value] = 1; //未登録なら、グループ名をキーにして初期値を代入する。
	}
```

これを応用して、今回のインポートファイルに登録済でないデータのうち、もう一つif文を用いて、DBテーブル内に格納されているグループの最大値＋1を取得するようにすれば、3パターンのシーケンス取得に対応できます。

```php:php
    //既に登録されたグループ名かを判定
	if(array_key_exists($row_value,$ar_count)){
		$ar_count[$row_value]++;
	}else{
        /*中略。ここでDBテーブルの最大値を取得する*/
        if(is_countable($rows)){
            $ar_count[$row_value] = $rows['max'] + 1; //DBテーブル上の最大値＋1
        }else{
		     $ar_count[$row_value] = 1;  //DB未登録なら、グループ名をキーにして初期値を代入する。
        }
	}
```

一括インポートによって、以下のようなデータを得ることができます。

```php:php
    //一括インポートによって得られたデータ
	$ar_values = [
		["さいたま","川口","さいたま","越谷","さいたま"],
		["さいたま","川越","所沢","越谷","川口"],
		["さいたま","川口","川越","春日部","上尾"],
		["熊谷","所沢","上尾","越谷","春日部"],
		["上尾","川口","さいたま","草加","草加"],
	];
	$Calc = new Calc(); //インスタンス作成
	$Cntval = $Calc -> count(); //メンバからクロージャを作成。ここに変数を代入しないこと！
	foreach($ar_values as $row_value){
		$datas = $Cntval($row_value); //このメソッドがクロージャ、ここに引数を代入する。
		echo var_export($datas),"<hr>";
	}
	
    //シーケンス計算のためのクラス
	class Calc
	{
		private $ar_count = []; //シーケンスの格納用
		
        //シーケンス計算のためのメソッド
		public function count(){
			$ar_count = $this -> ar_count; //初期値はクロージャの外側で
            //クロージャ制御
			return function($ar_values) use(&$ar_count){ 
                $values = []; //シーケンスを生成したデータの格納用
				foreach($ar_values as $row_value){
                    //既に登録されたグループ名かを判定
					if(array_key_exists($row_value,$ar_count)){
						$ar_count[$row_value]++;
					}else{
                        /*ここでDBテーブルの最大値を取得する*/
                        //作業は省略
                        /*最大値は$rows['max']に格納、存在しない場合はnullが返される*/
                        if(is_countable($rows)){
                            $ar_count[$row_value] = $rows['max'] + 1; //DBテーブル上の最大値＋1
                        }else{
		                    $ar_count[$row_value] = 1; //新規グループの初期値
                        }
					}
					$values[] = [$row_value,$ar_count[$row_value]];
				}
				return $values;
			};
		}
	}
```

データはこのように作成されます。配列は`[グループ名,グループのシーケンス]`となっており、各グループごとに番号がインクリメント（加算）されているのがわかると思います。たとえば、さいたまは6件登録するので最後のシーケンスは**6**、川口は**4**……となっています。

![score.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272191/d7c2ad28-a97d-d227-21d6-17a2c59ee118.jpeg)

後はこれをinsert文に代入していくだけです（DBテーブルのバルクインサート処理制御プログラムは割愛）。

次からはインポートデータに存在しない値の場合は、DBテーブルに存在する値との比較になるので、さいたまの初期値は7、川口の初期値は5が取得されます。また、狭山、入間、新座といった値が登場した場合の初期値は1となります（現時点ではインポートファイルにもDBテーブル上にも存在しないため）。もう一度さいたまが取得された場合は、カウント用の変数、$ar_count['さいたま']は7となっているので値を継承して8となり、二度目に狭山が登場した場合は2となり、このようにして常時、インポートされたファイルのタイミングでシーケンス取得を継続できます。

※ここではグループ名がユニークであるという前提で、そのまま配列のキーとしています

# クロージャの外側にループを置いた場合
今度はより実用的に、クロージャにループを置かず、処理の分岐だけした場合です。また、任意コードの連番を作成したい場合はsprintf関数を使用します。また、カナのインデックスも不安なのでbin2hex関数で任意のインデックス（同一の名称ならばユニークになります）で制御しておきます。

また、今回はデータベースに保存しない前提で処理を記述してみます。

```php:php
<?php
    //一括インポートによって得られたデータ
	$ar_values = [
		["さいたま","川口","さいたま","越谷","さいたま"],
		["さいたま","川越","所沢","越谷","川口"],
		["さいたま","川口","川越","春日部","上尾"],
		["熊谷","所沢","上尾","越谷","春日部"],
		["上尾","川口","さいたま","草加","草加"],
	];
	$Calc = new Calc(); //インスタンス作成
	$Cntval = $Calc -> count(); //メンバからクロージャを作成。ここに変数を代入しないこと！
	foreach($ar_values as $row_value){
		
		foreach($row_value as $value){
			$datas = $Cntval($value); //このメソッドがクロージャ、ここに引数を代入する。
		}
	}
	
    //シーケンス計算のためのクラス
	class Calc
	{
		private $ar_count = []; //シーケンスの格納用
        //シーケンス計算のためのメソッド

		public function count(){
			$ar_count = []; //初期値はクロージャの外側で
			$seq_no = null;
            //クロージャ制御
			return function($data) use(&$ar_count,$seq_no){
				//既に登録されたグループ名かを判定
				$dcode = bin2hex($data);
				if(array_key_exists($dcode,$ar_count)){
					$ar_count[$dcode]++;
					$seq_no = $$seq_no = sprintf("%03d",$ar_count[$dcode]); //グループ番号+1　初期値
				}else{
					$seq_no = sprintf("%03d",1); //新規にカウント
				}
				$ar_count[$dcode] = $seq_no;
				echo var_export($ar_count),"<hr>";
				return $seq_no;
			};
		}

	}
```

以下のようになるので、記録テーブルや履歴テーブルを一括登録する際に利用回数を付与したい場合、ファイルのエクスポート時に自動的にグループ番号を採番していくとき、集計作業などにも応用できます。なお、bin2hexで変換したコードはhex2bin関数で簡単に戻せます。

```
array ( 'e38195e38184e3819fe381be' => '006', 'e5b79de58fa3' => '004', 'e8b68ae8b0b7' => '003', 'e5b79de8b68a' => '002', 'e68980e6b2a2' => '002', 'e698a5e697a5e983a8' => '002', 'e4b88ae5b0be' => '003', 'e7868ae8b0b7' => '001', 'e88d89e58aa0' => '002', )

```