Friction River Software

  • お問い合わせ

CakePHP5入門【WebAPI編④】管理用ページ①

B美

今回の手順としては

1.管理用ページのコントローラーとビューを作成
2.BASIC認証を導入
3.Excelファイルのインポート処理を実装

…って感じね

A子

了解
まずは「MATE端末」上でbakeするよ

cd html/numapp[Enter]
bin/cake bake controller admin[Enter]

出来上がった「src/Controller/AdminController.php」の中身についても「index」メソッドを残して全削除ね


C菜

「templates」の中に「Admin」ディレクトリを作って、その中に「index.php」をコピー、ちょっとだけ修正しました~
(「templates/Top/index.php」を流用です~)


B美

ひとまずブラウザで確認してみましょうか

http://192.168.1.205/numapp/admin

まだ認証機構を作ってないから、普通にアクセスできるわね

A子

んじゃ次は「BASIC認証」なんだけど、えっと…どうやるんだったっけ?(汗)

C菜

こうですよ~

bin/cake bake middleware HttpBasicAuth[Enter]

C菜

このあと「src/Middleware/HttpBasicAuthMiddleware.php」を修正するです~

あ、「process」メソッドの中身については、前回のチャットアプリのやつを流用しましたよ~
(メソッド単位で認証可否を切り分ける必要がないので、少し簡単になりました~)

A子

AdminController側にも何か記述が必要だった気がするんだけど…

こういうときは以前のソースをカンニングだね(苦笑)
(「authapp」プロジェクトの「src/Controller/UsersController.php」を参照するよ)

・・・

うん、わかった
「src/Controller/AdminController.php」の中に「initialize」メソッドを追加するのと、「namespace」の下に「use」の一文を追加だね

C菜

テストしてみましたけど、「認証成功」と「認証失敗」の両方ともきちんと動きましたよ~

A子

「過去のソースコードがプログラマにとっての資産である」…ってのがよく分かるね

絶対に憶えてられないよ(苦笑)

B美

A子もなかなか分かってきたじゃない

さて、それじゃ最後の一つ「Excelファイルのインポート」を実装するよ

C菜

画像ファイルをアップロードしたときのように、フォームからPOST送信するんでしょうか~?

B美

その通り!

管理用ページ(index.php)の中に直接フォームを記述して良いわよ
(だって、これ以外の機能を実装しないからね)

A子

んじゃ、画像投稿掲示板(「bbsapp」プロジェクト)のソースをカンニングだね

・・・

こんな感じでどうかな?
(インポート処理については「import」メソッドを実装する予定)



C菜

良いと思います~

B美

さて、問題は「import」メソッドなんだけど…

とりあえずは、POST送信されたものを受け取る記述を書いてみなさい
(以前のソースの流用で良いから…)

A子

えーっと、こんなところかな?

public function import()
{
    if ($this->request->is('post')) {
        $upload = $this->request->getData('upload');

        //オリジナルのファイル名を取得
        $original_filename = $upload->getClientFilename();
        $extension = mb_strtolower(pathinfo($original_filename, PATHINFO_EXTENSION)); //拡張子

        if ($extension == 'xlsx') {
            //拡張子チェックOK

        }
    }
}

B美

OK、良いでしょう

では、Excelファイルを取り扱うためのクラスを教えるわ
それが『Spreadsheet』クラスよ

C菜

use文として記述するんですか~?

B美

ええ、ただその前にcomposerでインストールしなきゃだけどね

cd html/numapp[Enter]
composer require phpoffice/phpspreadsheet[Enter]

もちろん、上記を「MATE端末」上で実行してね

んで、use文は以下の通り

use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Shared\Date;

これを「AdminController.php」の先頭に記述します




A子

いや、『Spreadsheet』クラスじゃないじゃん(苦笑)

IOFactory』クラスと『Date』クラスだよね

B美

『Spreadsheet』クラスも存在するんだけど、それを使うのはExcelファイルを新規作成するときね

読み込みだけなら『IOFactory』クラスになるの
(ちなみに、『Date』クラスはシリアル値であるExcelの日付型データを変換するために必要)

C菜

それをどうやって使うのでしょうか~?

B美

その前に、POST送信されたものから「オリジナルのファイル名を取得」まではできてるから、念のためにファイルが送信されていないときのエラー処理を書きましょう
(下の赤字が追加した箇所ね)

$original_filename = $upload->getClientFilename();
if ($original_filename == '') {
    $this->Flash->error('ファイルが選択されていません。');
    return $this->redirect(['action' => 'index']);
}

$extension = mb_strtolower(pathinfo($original_filename, PATHINFO_EXTENSION)); //拡張子

で、Excelファイルの読み込みについては、こうするの

$reader = IOFactory::createReader('Xlsx');
$reader->setReadDataOnly(true);
$tmpPath = $upload->getStream()->getMetadata('uri');
$spreadsheet = $reader->load($tmpPath);

そうしたら、まずはアクティブなシートを指定してから、それを配列に変換しましょう
(ほかの方法もあるけど、配列化するのが一番簡単だと思う)

$sheet = $spreadsheet->getActiveSheet();
$data = $sheet->toArray(null, true, true, true);

ここで注意してほしいのが、toArrayメソッドの第4引数(上記の赤字部分)ね

true → 列番号をキーとした連想配列
false → 普通の配列(ゼロスタート)

だから、上記の例だと['A']や['B']みたいな形になるわ
(falseならば[0]や[1]ってこと)

A子

さっき出てきた『Date』クラスって、いつ使うのよ?

B美

Excelの日付型の値って、実は単なる整数値(シリアル値)でね
これを普通の日付表示に変更するのに『Date』クラスを使うのよ

例えば、「$lottery_date」という変数にシリアル値が入っているとしましょう
まず、数値であるかどうかのチェックを入れて、あとは『Date』クラスの「excelToDateTimeObject」メソッドを呼び出せばOK

if (is_numeric($lottery_date)) {
    $lottery_date = Date::excelToDateTimeObject($lottery_date);
    $lottery_date_str = $lottery_date->format('Y年m月d日');
}

あ、このとき注意しなければいけないのが、「$lottery_date」に入っている値が整数値でなければならない…ってこと
(文字列を引数に渡すとエラーになるわよ)

C菜

え~っと、まとめてみますね~

public function import()
{
    if ($this->request->is('post')) {
        $cnt = 0; //処理件数
        $upload = $this->request->getData('upload');

        //オリジナルのファイル名を取得
        $original_filename = $upload->getClientFilename();
        if ($original_filename == '') {
            $this->Flash->error('ファイルが選択されていません。');
            return $this->redirect(['action' => 'index']);
        }
        $extension = mb_strtolower(pathinfo($original_filename, PATHINFO_EXTENSION)); //拡張子

        if ($extension == 'xlsx') {
            //Excelファイルの読み込み
            $reader = IOFactory::createReader('Xlsx');
            $reader->setReadDataOnly(true);
            $tmpPath = $upload->getStream()->getMetadata('uri');
            $spreadsheet = $reader->load($tmpPath);

            //配列に変換して処理
            $sheet = $spreadsheet->getActiveSheet();
            $data = $sheet->toArray(null, true, true, true);
            foreach ($data as $row) {
                $lottery_time = intval($row['A']); //実施回
                $lottery_date = intval($row['B']); //抽選日(intvalで数値化しないとexcelToDateTimeObjectでエラーになる)


                if (is_numeric($lottery_date)) {
                    $lottery_date = Date::excelToDateTimeObject($lottery_date); //Excelのシリアル値を日付型に変換
                    $lottery_date_dt = $lottery_date->format('Y/m/d'); //抽選日(日付型)
                    $lottery_date_str = $lottery_date->format('Y年m月d日'); //抽選日(文字列型)
                    $lottery_date_year = intval($lottery_date->format('Y')); //抽選日の年
                    $lottery_date_month = intval($lottery_date->format('m')); //抽選日の月
                    $lottery_date_day = intval($lottery_date->format('d')); //抽選日の日
                    $lottery_week_int = 0; //抽選日の曜日(整数型)
                    $lottery_week_str1 = ''; //抽選日の曜日1(文字列型)
                    $lottery_week_str2 = ''; //抽選日の曜日2(文字列型)
                    $lottery_rokuyo_int = 0; //抽選日の六曜(整数型)
                    $lottery_rokuyo_str = ''; //抽選日の六曜(文字列型)

                }

                $num3_str = $row['C']; //ナンバーズ3の当選番号(文字列型)
                $num3_int = intval($num3_str); //ナンバーズ3の当選番号(整数型)
                $num3_place1 = intval($num3_int % 10); //ナンバーズ3の当選番号の一の位
                $num3_place10 = intval($num3_int % 100 / 10); //ナンバーズ3の当選番号の十の位
                $num3_place100 = intval($num3_int / 100); //ナンバーズ3の当選番号の百の位

                $num4_str = $row['D']; //ナンバーズ4の当選番号(文字列型)
                $num4_int = intval($num4_str); //ナンバーズ4の当選番号(整数型)
                $num4_place1 = intval($num4_int % 10); //ナンバーズ4の当選番号の一の位
                $num4_place10 = intval($num4_int % 100 / 10); //ナンバーズ4の当選番号の十の位
                $num4_place100 = intval($num4_int % 1000 / 100); //ナンバーズ4の当選番号の百の位
                $num4_place1000 = intval($num4_int / 1000); //ナンバーズ4の当選番号の千の位

                $cnt++;
            }

        }

        $this->Flash->success($cnt.'件の処理を行いました。');
        return $this->redirect(['action' => 'index']);

    }
}

私が書いたのは、上の赤字部分だけですけどね~(苦笑)
あと、曜日と六曜の求め方が分からないので、あとで教えてください~


B美

私が大したもんだと感じたのは、ここの記述ね

$lottery_date = intval($row['B']);

よく整数型への変換を忘れなかったわね

C菜

実は、最初「intval」を付けてなくて、エラーが出ちゃったんですけどね~(笑)

B美

(笑)
あー、ただ一点だけ残念な点があるわ

$lottery_date_dt = $lottery_date->format('Y/m/d');

の部分だけど

$lottery_date_dt = $lottery_date->format('Y-m-d');

にしたほうが良いわよ

だって、その変数の値って、MySQLのdatetime型項目に入れる値になるから…
('Y/m/d'でもうまくいくかもしれないけど、ちょっと怪しいのよね)

A子

なるほどねぇ

あ、あとさ
それぞれの変数にきちんと正しい値が入ったかどうかって、簡単に調べられないかな?

ビューファイル(templates/Admin/import.php)をわざわざ作らなきゃダメかな?

B美

まぁ、その方法もあるけど、ちょっと面倒よね

同じページにリダイレクトしているから、フラッシュメッセージを出すのが一番簡単じゃないかな
(「import.php」を作るなら「$this->set()」すれば良いんだけど…)

C菜

やってみるです~

$this->Flash->success('実施回:'.$lottery_time);
$this->Flash->success('抽選日(日付型):'.$lottery_date_dt);
$this->Flash->success('抽選日(文字列型):'.$lottery_date_str);
$this->Flash->success('抽選日の年:'.$lottery_date_year);
$this->Flash->success('抽選日の月:'.$lottery_date_month);
$this->Flash->success('抽選日の日:'.$lottery_date_day);
$this->Flash->success('抽選日の曜日(整数型):'.$lottery_week_int);
$this->Flash->success('抽選日の曜日1(文字列型):'.$lottery_week_str1);
$this->Flash->success('抽選日の曜日2(文字列型):'.$lottery_week_str2);
$this->Flash->success('抽選日の六曜(整数型):'.$lottery_rokuyo_int);
$this->Flash->success('抽選日の六曜(文字列型):'.$lottery_rokuyo_str);
$this->Flash->success('ナンバーズ3の当選番号(文字列型):'.$num3_str);
$this->Flash->success('ナンバーズ3の当選番号(整数型):'.$num3_int);
$this->Flash->success('ナンバーズ3の当選番号の一の位:'.$num3_place1);
$this->Flash->success('ナンバーズ3の当選番号の十の位:'.$num3_place10);
$this->Flash->success('ナンバーズ3の当選番号の百の位:'.$num3_place100);
$this->Flash->success('ナンバーズ4の当選番号(文字列型):'.$num4_str);
$this->Flash->success('ナンバーズ4の当選番号(整数型):'.$num4_int);
$this->Flash->success('ナンバーズ4の当選番号の一の位:'.$num4_place1);
$this->Flash->success('ナンバーズ4の当選番号の十の位:'.$num4_place10);
$this->Flash->success('ナンバーズ4の当選番号の百の位:'.$num4_place100);
$this->Flash->success('ナンバーズ4の当選番号の千の位:'.$num4_place1000;

これを「import」メソッドの最後らへんに書きました~
でも、これではExcelファイルの最後の行(第6929回目)の値だけしか出ませんけどね~(苦笑)

A子

別に良いんじゃない?
(6929件全てを表示するのは現実的じゃないし…)

んじゃ、さっそくテストしてみよう
B美の作ったExcelファイルを読み込ませって…っと





C菜

ばっちりです~

これで「曜日」や「六曜」の値についても、あとで確認できますね~

B美

それじゃ、今回はその「曜日」や「六曜」の値の取得までをやっておきましょう
(データベース登録については次回)

昔(CakePHP4以前)は、以下のようにcomposerでインストールできるライブラリがあったんだけどね

cd html/numapp[Enter]
composer require japanese-date/japanese-date[Enter]

てか、今でもあるんだけど、残念ながらCakePHP5では使えない
(あるメソッド…具体的には「formatLocalized」メソッド…の呼び出しでエラーになる)

A子

んじゃ、どうすんのさ

B美

仕方ないので、コンポーネントを自作します

C菜

コンポーネントって何ですか~?

B美

複数のコントローラーから利用できる共通ルーチンって感じかな

要は、便利なライブラリが無いのなら自分で作っちゃえ…ってこと
それじゃ、「MATE端末」上で以下のコマンドを打ち込んでね

cd html/numapp[Enter]
bin/cake bake component SixSevenWeek[Enter]

クラス名(SixSevenWeek)は何でも良いんだけど、六曜と七曜を取得するって意味で適当に付けたわ
(「src/Controller/Component」ディレクトリの中に「SixSevenWeekComponent.php」という名前で生成されるから)



B美

この中にフィールドとメソッドを記述します

まずはフィールドね

private static $rokuyo = ['大安', '赤口', '先勝', '友引', '先負', '仏滅'];
private static $week_long = ['日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日'];
private static $week_short = ['日', '月', '火', '水', '木', '金', '土'];
・・・
private static $rokuyo_data = [
    '1994-01-01' => 1,
    '1994-01-02' => 2,
    '1994-01-03' => 3,

    ・・・

    '2099-12-30' => 0,
    '2099-12-31' => 1,
    '2100-01-01' => 2,
];

あ、$rokuyo_dataについては末尾に記述してるわよ
(超・長いから…具体的には、38,717行

・・・

・・・

A子

んん?

その大量のデータ(「$rokuyo_data」という連想配列)は何なのよ

C菜

名前からして六曜データでしょうね~

おそらく「$rokuyo」という配列のインデックスに対応してるとみました~

B美

C菜、正解!
六曜を計算で求めることもできなくはないんだけど、正確な結果を取得するにはかなり複雑な計算が必要なのよ

あと、それでもズレる可能性があるという…(苦笑)

A子

すでに計算された『完全に正しい六曜データ』をあらかじめ持っておく…ってことか

力技だね(苦笑)

B美

まぁね(苦笑)
でも、これが一番確実よ

んじゃ、次はメソッドね

//六曜データの取得
public static function getSix($ymd)
{
    $index = self::$rokuyo_data[$ymd];

    return [
        'rokuyo_int' => $index, //0〜5までの数値
        'rokuyo_str' => self::$rokuyo[$index] //文字列
    ];
}

//七曜(曜日)データの取得
public static function getSeven($y, $m, $d)
{
    $timestamp = mktime(12, 0, 0, $m, $d, $y);
    $week_int = date('w', $timestamp);

    return [
        'week_int' => $week_int, //0〜6までの数値
        'week_str_long' => self::$week_long[$week_int], //長い文字列
        'week_str_short' => self::$week_short[$week_int] //短い文字列
    ];
}

C菜

::ダブルコロンは前にも出てきましたけど、「self」は初めてじゃないですか~?

なんとなく言いたいことは分かりますけどね~

A子

::ダブルコロンって、(インスタンス化していない)クラス自体に備わった機能を使うときのやり方だよね?

「$this->」がインスタンス化されたオブジェクトのフィールドにアクセスするんだから、「self::」はインスタンス化されていないクラスのフィールドやメソッドにアクセスする方法と見た

B美

お、ちょっとビックリ
正解よ

キーワードとして「static」がフィールドやメソッドの先頭に付いているでしょ?
それが(インスタンス化が要らない)クラス固有の機能…ってことになるのよ

C菜

$self::rokuyo_data」ではなく、「self::$rokuyo_data」なんですねぇ~
(うっかり間違えそうです~)

B美

それでは、最後にこのコンポーネントをコントローラーから使う方法だけど…
「src/Controller/AdminController.php」の「initialize」メソッドに以下の一文を追加してね

$this->loadComponent('SixSevenWeek');

あとは、こんな感じでメソッドを呼び出せるわ

$seven = $this->SixSevenWeek::getSeven($lottery_date_year, $lottery_date_month, $lottery_date_day);
$lottery_week_int = $seven['week_int']; //抽選日の曜日(整数型)
$lottery_week_str1 = $seven['week_str_long']; //抽選日の曜日1(文字列型)
$lottery_week_str2 = $seven['week_str_short']; //抽選日の曜日2(文字列型)

$six = $this->SixSevenWeek::getSix($lottery_date_dt);
$lottery_rokuyo_int = $six['rokuyo_int']; //抽選日の六曜(整数型)
$lottery_rokuyo_str = $six['rokuyo_str']; //抽選日の六曜(文字列型)

あ、ついでに一点だけ修正しておいたからね
「Y年m月d日」だった箇所を「Y年n月j日」に…
(01月01日が違和感あるから、1月1日になるように…)




C菜

コンポーネントのメソッドの戻り値が連想配列になってるんですね~

A子

なるほどねぇ

メソッドって、一個だけしか値を返せないと思ってたけど、こんな風に複数個の値を返せるんだね

B美

ええ
ただの「配列」だろうが(上記の例のように)「連想配列」であろうが、戻り値として使えるわよ

A子

てかさー

コンポーネントって(普通のクラスみたいに)インスタンス化して使うようにはできないの

B美

もちろん、できるわよ
static」を付けない場合は「self::$rokuyo_data[$ymd]」ではなく
$this->rokuyo_data[$ymd]」になるだけね

あと、呼出し側(AdminController)でも「$this->SixSevenWeek::getSix($lottery_date_dt)」ではなく
「$this->SixSevenWeek->getSix($lottery_date_dt)」になるわ

C菜

なるほどですね~

複数のコントローラーからアクセスされる(可能性のある)コンポーネントは、静的(static)な存在にしていたほうが良いってことでしょうか~?

B美

いや、別に…(苦笑)

static」を使う例題として使っただけよ

A子

がくっ(ズッコケ)

ま、まぁ良いでしょう
んじゃ、実行確認してみよう

C菜

ばっちりです~

ネット上にある六曜カレンダーの値とも比較してみましたけど、2026年2月27日は「金曜日」の「大安」で間違いないです~

A子

今回、後半はかなりB美に頼っちゃったけど、次回はデータベース登録だよね

だったら、まかせてよ
B美に頼らずにできるはず…C菜が!

B美

自分じゃないのかよ(笑)