[2022/11/09] PHP クラス(オブジェクト)の反復処理について・その2(IteratorAggregate) (No.293)
[2022/11/08] PHP クラス(オブジェクト)の反復処理について (No.292)
[2022/11/07] PHP Carbon(ライブラリ)を使って日付処理・インストール (No.291)
[2022/11/07] PHP PhpSpreadsheet エクセルファイルの書き込み時のエラー(fopen: failed to open stream) (No.290)
[2022/11/02] PHP PhpSpreadsheet エクセルのワークシートのセルに日付型データの設定と、日付スタイル設定について2(setCellValue, getStyle) (No.289)
-
×
[PR]上記の広告は3ヶ月以上新規記事投稿のないブログに表示されています。新しい記事を書く事で広告が消えます。
-
以下の記事では、クラス(オブジェクト)の反復処理について Iterator インターフェースを使うところまでは説明を行いました。
⇒PHP クラス(オブジェクト)の反復処理について
Iterator インターフェースでは5個のメソッドを宣言しなければならないので、そのあたりを簡単にできる方法を説明します。 比較の為に、前回のソースを以下に示します。<?php // Iterator インターフェースを利用した反復処理テストクラス class cEach implements Iterator { // 内部の配列とする private $list = ["list1", "list2", "list3", "list4", "list5"]; // 配列の位置 private $pos = 0; // コンストラクタ public function __construct() { $this->pos = 0; } // 巻き戻し(初期位置) public function rewind(): void { $this->pos = 0; } // 現在値 public function current() { return $this->list[$this->pos]; } // 現在キー public function key() { return $this->pos; } // 次の位置へ public function next(): void { ++$this->pos; } // 現在位置のデータ存在チェック public function valid(): bool { return isset($this->list[$this->pos]); } } // [cEach]クラスの生成 $insEach = new cEach(); // インスタンスから取得できる反復データ foreach($insEach as $key => $value) { echo "$key => $value<br>\n"; } ?>
上記のクラスを今回は IteratorAggregate インターフェイスを使ってクラス(オブジェクト)の反復処理を書き変えます。
IteratorAggregate インターフェイスの概要は以下の様です。 getIterator メソッドの宣言は必須となります。interface IteratorAggregate extends Traversable { /* メソッド */ public getIterator(): Traversable // 外部イテレータを取得する }
それでは、書き変えたソースを以下に示します。
getIterator メソッドの戻り値に ArrayIterator クラスの生成値を戻しています。
getIterator の戻り値は Traversable インターフェース型である必要がありますが、 ArrayIterator は配列型データを Traversable インターフェース型のクラスを生成しています。<?php // IteratorAggregate インターフェースを利用した反復処理テストクラス class cEach implements IteratorAggregate { // 内部の配列とする private $list; // コンストラクタ function __construct(array $_list) { // 外部からの配列を内部に退避(配列はシャローコピーではないので) $this->list = $_list; } // 外部イテレータを取得する public function getIterator() { return new ArrayIterator($this->list); } } // [cEach]クラスの生成 $arr = ["list1", "list2", "list3", "list4", "list5"]; $insEach = new cEach($arr); // インスタンスから取得できる反復データ foreach($insEach as $key => $value) { echo "$key => $value<br>\n"; } ?>
これを実行するとブラウザに以下の様に表示されます。0 => list1 1 => list2 2 => list3 3 => list4 4 => list5
上記の ArrayIterator を使っている部分を ジェネレータ と呼ばれる yield を使う方法でもできます。
以下に変更したソースを示します。<?php // IteratorAggregate インターフェースを利用した反復処理テストクラス class cEach implements IteratorAggregate { // 内部の配列とする private $list; // コンストラクタ function __construct(array $_list) { // 外部からの配列を内部に退避(配列はシャローコピーではないので) $this->list = $_list; } // ジェネレータ(yield)を使用する public function getIterator() { foreach ($this->list as $key => $val) { yield $key => $val; } } } // [cEach]クラスの生成 $arr = ["list1", "list2", "list3", "list4", "list5"]; $insEach = new cEach($arr); // インスタンスから取得できる反復データ foreach($insEach as $key => $value) { echo "$key => $value<br>\n"; } ?>
ジェネレータ と呼ばれる yield について少し説明します。
PHP の公式ドキュメントには以下の様に記述されています。ジェネレータ関数が呼ばれると、反復処理が可能なオブジェクトを返します。 このオブジェクトを (foreach ループなどで) 反復させると、値が必要になるたびに PHP がオブジェクトの反復メソッドを呼びます。 そして、ジェネレータが値を yield した時点の状態を保存しておき、 次に値が必要になったときにはそこから再開できるようにします。 yield できる値がなくなると、ジェネレータは単純に終了します。呼び出し元のコードでは、配列の要素をすべて処理し終えた後のように、そのまま処理が続きます。
ジェネレータ関数では yield は return 命令の様な感じで動きますが、 呼び出し元で繰り返し呼ばれることで、順次出力する内容が呼び出し元に返されます。
以下の例のジェネレータ関数は内部で宣言された配列を順次 yield で出力しています。 呼び出し元では foreach 命令の「$value」に値が返ってきます。<?php // ジェネレータ関数 function generator_func() { $arr = ["list1", "list2", "list3"]; foreach($arr as $val) { yield $val; } } // ジェネレータ関数の呼出 foreach (generator_func() as $value) { echo "$value<br>\n"; } ?>
yield では {キー} => {値} の形での指定もできます。ソースを変更しました。
<?php // ジェネレータ関数 function generator_func() { $arr = ["list1", "list2", "list3"]; foreach($arr as $key => $val) { yield $key => $val; } } // ジェネレータ関数の呼出 foreach (generator_func() as $key => $value) { echo "$key => $value<br>\n"; } ?>
PR -
配列の反復処理では foreach 命令を使って配列のデータを順次処理することができます。 この foreach 命令ですが、オブジェクトの反復処理にも使用することができます。
オブジェクトの反復処理に使用する場合、書式は以下の様になります。 書き方は配列の場合と何も変わったところはありません。
ただし、アクセスできる変数はアクセス権限のあるメンバー変数のみを取得できます。foreach (オブジェクト変数 as メンバ変数名 => メンバ変数の値) { }
それでは、この反復処理を行う例を以下に示します。<?php // 反復処理テストクラス class cEach { // public プロパティ public $public_pub_para1 = "public_val1"; public $public_param2 = "public_val2"; public $public_param3 = "public_val3"; // protected プロパティ protected $protected_param1 = "protected_val1"; protected $protected_param2 = "protected_val2"; // private プロパティ private $private_param1 = "private_val1"; // 自分のクラス内の反復処理 public function list_cEach() { echo "クラスの内での反復...<br>\n"; foreach($this as $key => $value) { echo "$key => $value<br>\n"; } } } // [cEach]クラスの生成 $insEach = new cEach(); // インスタンスから取得できる反復データ echo "クラスの外からの反復...<br>\n"; foreach($insEach as $key => $value) { echo "$key => $value<br>\n"; } // クラス内部で取得できる反復データ $insEach->list_cEach(); ?>
このプログラムを実行した時に、最初に「cEach」クラスを生成し、28行目の foreach 命令が実行されます。 「cEach」クラスのインスタンス「$insEach」からアクセスできるのは「cEach」クラスの public 属性を持った変数のみです。
その後で「cEach」クラスの list_cEach 関数をコールすることでクラス内のアクセス可能な変数を全て反復処理できます。 18行目の foreach 命令で行います。
これを実行するとブラウザに以下の様に表示されます。クラスの外からの反復... public_param1 => public_val1 public_param2 => public_val2 public_param3 => public_val3 クラスの内での反復... public_param1 => public_val1 public_param2 => public_val2 public_param3 => public_val3 protected_param1 => protected_val1 protected_param2 => protected_val2 private_param1 => private_val1
foreach 命令はアクセス権限のあるメンバー変数のみを取得しますので、 クラスの外側では protected および private 属性の変数は取得できないことになります。
クラスのインスタンスに対する foreach 命令は public 変数を順次取得できるということです。
ただ、これ(public 変数を順次取得することが)が何に役立つのでしょうか?
このままでは前に進みませんので以下の例を示します。
<?php // 反復処理テストクラス class cEach { // public プロパティ public $public_list = ["list1", "list2", "list3"]; } // [cEach]クラスの生成 $insEach = new cEach(); // インスタンスから取得できる反復データ foreach($insEach as $key => $value) { echo "$key => $value<br>\n"; } ?>
これを実行すると以下の様なエラーが表示されます。13行目で取得した「$value」は配列のデータなので文字列型への変換は注意!と表示されました。Notice: Array to string conversion in C:\xampp\htdocs\_test\class15-each-2.php on line 13 public_list => Array
「$value」は配列データなので当たり前なので、以下の様にする必要があります。<?php // 反復処理テストクラス class cEach { // public プロパティ public $public_list = ["list1", "list2", "list3"]; } // [cEach]クラスの生成 $insEach = new cEach(); // インスタンスから取得できる反復データ foreach($insEach as $key => $value) { if (is_array($value)) { // 配列の場合は再度foreach foreach($value as $keyArr => $valueArr) { echo "$keyArr => $valueArr<br>\n"; } } else { echo "$key => $value<br>\n"; } } ?>
一応結果は以下の様になります。0 => list1 1 => list2 2 => list3
上記の方法では便宜的で一般性に欠けるので、クラスの方で仕掛けが必要になります。
PHP ではこの様な場合の反復処理が出来る様に Iterator インターフェースが実装されています。interface Iterator extends Traversable { /* メソッド */ public current(): mixed // 現在の要素を返す(あらゆる型を返すことが可能です) public key(): mixed // 現在の要素のキーを返す(成功した場合にスカラー型,失敗した場合にnullを返す) public next(): void // 次の要素に進む public rewind(): void // イテレータの最初の要素に巻き戻す public valid(): bool // 現在位置が有効かどうかを調べる(成功した場合にtrueを,失敗した場合にfalseを返す) }
Iterator インターフェースを利用して先ほどのクラスを変更します。 尚、5個のメソッドは必ず宣言する様です。<?php // Iterator インターフェースを利用した反復処理テストクラス class cEach implements Iterator { // 内部の配列とする private $list = ["list1", "list2", "list3", "list4", "list5"]; // 配列の位置 private $pos = 0; // コンストラクタ public function __construct() { $this->pos = 0; } // 巻き戻し(初期位置) public function rewind(): void { $this->pos = 0; } // 現在値 public function current() { return $this->list[$this->pos]; } // 現在キー public function key() { return $this->pos; } // 次の位置へ public function next(): void { ++$this->pos; } // 現在位置のデータ存在チェック public function valid(): bool { return isset($this->list[$this->pos]); } } // [cEach]クラスの生成 $insEach = new cEach(); // インスタンスから取得できる反復データ foreach($insEach as $key => $value) { echo "$key => $value<br>\n"; } ?>
これを実行すると以下の様に表示されます。 Iterator インターフェースを利用したクラスの foreach ではちゃんと処理される様です。0 => list1 1 => list2 2 => list3 3 => list4 4 => list5
これでもまだ改良の余地がありそうです。 このままではキー値が数値しか扱えませんし、クラスの生成時に外からリストを可変に設定したいとかいろいろ考えられます。
それらの件に付いてはまた別の記事で書いていこうと思います。
-
今回は日付処理のライブラリである Carbon の使い方について説明したいと思います。
Carbon は Laravel でも使用されていますので、割と認知されていると思います。
では先ずインストールについて説明していきます。 ここでのインストールは Composer というパッケージ管理ソフトを使いますので、 以前 PhpSpreadsheet のインストールで説明していますので、以下の記事を参照して下さい。
⇒PHP PhpSpreadsheet のインストールについて(XAMPPでのPhpSpreadsheetのインストールその1:composer)
⇒PHP PhpSpreadsheet のインストールについて(XAMPPでのPhpSpreadsheetのインストールその2:composerでPhpSpreadsheet)
■Composer によるインストール
XAMPP でのインストールを想定していますので「Windwosコマンドプロンプト(管理者権限)」を起動して以下のコマンドを実行します。
>composer require nesbot/carbon
このコマンドの後で以下の様な表示になります。
C:\xampp\htdocs>composer require nesbot/carbon Info from https://repo.packagist.org: #StandWithUkraine Using version ^2.62 for nesbot/carbon ./composer.json has been updated Running composer update nesbot/carbon Loading composer repositories with package information Updating dependencies Lock file operations: 5 installs, 0 updates, 0 removals - Locking nesbot/carbon (2.62.1) - Locking symfony/deprecation-contracts (v2.5.2) - Locking symfony/polyfill-php80 (v1.26.0) - Locking symfony/translation (v5.4.14) - Locking symfony/translation-contracts (v2.5.2) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 5 installs, 0 updates, 0 removals - Downloading symfony/translation (v5.4.14) 0/1 [>---------------------------] 0% 1/1 [============================] 100% - Installing symfony/translation-contracts (v2.5.2): Extracting archive - Installing symfony/polyfill-php80 (v1.26.0): Extracting archive - Installing symfony/deprecation-contracts (v2.5.2): Extracting archive - Installing symfony/translation (v5.4.14): Extracting archive - Installing nesbot/carbon (2.62.1): Extracting archive 0/5 [>---------------------------] 0% 2/5 [===========>----------------] 40% 3/5 [================>-----------] 60% 4/5 [======================>-----] 80% 5/5 [============================] 100% 3 package suggestions were added by new dependencies, use `composer suggest` to see details. Generating autoload files 8 packages you are using are looking for funding. Use the `composer fund` command to find out more! No security vulnerability advisories found
■Carbon のインストール確認
Carbon のインストールは XAMPP のドキュメントフォルダの中の「composer.json」内容を確認すれば分かります。
私のPCの中の設定値の例ですが Carbon のバージョン2.62がインストールされたことが分かります。
(phpspreadsheet は既にインストール済みのため存在しています){ "require": { "phpoffice/phpspreadsheet": "^1.25", "nesbot/carbon": "^2.62" }, "config": { "platform": { "php": "7.4.32" } } }
■Carbon による簡単な例
以下に簡単な例を示します。
このソースは XAMPP のドキュメントフォルダ直下に置きますので、 Carbon ライブラリ読込は vendor/autoload.php となります。
あとはライブラリ使用宣言をして、Carbon の現在日付の関数をコールします。<?php // ライブラリ読込 require 'vendor/autoload.php'; // ライブラリ使用宣言 use Carbon\Carbon; echo "Now: ".Carbon::now(); ?>
ブラウザの結果は以下の様に表示されます。
Now: 2022-11-07 08:44:04
-
今回はさらに小ネタですが、XAMPP でのデバッグ時によく遭遇するエラーなのですが PhpSpreadsheet で出力されたエクセルファイルを MS-EXCEL で開いたままで 同じプログラムを実行した時には以下の様な WARNING と ERROR が表示されます。Warning: fopen(test6-a3.xlsx): failed to open stream: Resource temporarily unavailable in C:\xampp\htdocs\vendor\phpoffice\phpspreadsheet\src\PhpSpreadsheet\Writer\BaseWriter.php on line 125 Fatal error: Uncaught PhpOffice\PhpSpreadsheet\Writer\Exception: Could not open file "test6-a3.xlsx" for writing. in C:\xampp\htdocs\vendor\phpoffice\phpspreadsheet\src\PhpSpreadsheet\Writer\BaseWriter.php:127 Stack trace: #0 C:\xampp\htdocs\vendor\phpoffice\phpspreadsheet\src\PhpSpreadsheet\Writer\Xlsx.php(542): PhpOffice\PhpSpreadsheet\Writer\BaseWriter->openFileHandle() #1 C:\xampp\htdocs\_phpspread\excel6-all-3.php(23): PhpOffice\PhpSpreadsheet\Writer\Xlsx->save() #2 {main} thrown in C:\xampp\htdocs\vendor\phpoffice\phpspreadsheet\src\PhpSpreadsheet\Writer\BaseWriter.php on line 127
このエラーが発生したプログラムは以下の記事のものを使用しています。
⇒PHP PhpSpreadsheet エクセルのワークシートのセルに日付型データの設定と、日付スタイル設定について2(setCellValue, getStyle)><?php // ライブラリ読込 require '../vendor/autoload.php'; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Writer\Xlsx; // Spreadsheetオブジェクト生成 $objSpreadsheet = new Spreadsheet(); // ワークシートオブジェクト $objSheet = $objSpreadsheet->getActiveSheet(); // セルにシステム時刻(Unix Time)を設定 $objSheet->setCellValue('A2', Date::PHPToExcel(time())); $objSheet->setCellValue([1, 3], Date::PHPToExcel(time())); // スタイルオブジェクト取得([A2]セル) $objStyle = $objSheet->getStyle('A2')->getNumberFormat()->setFormatCode('yyyy"年"m"月"d"日";@'); $objStyle = $objSheet->getStyle([1, 3, 1, 3])->getNumberFormat()->setFormatCode('yyyy"年"m"月"d"日";@'); // [test6-a2.xlsx]:Excel2007形式で保存する $objWriter = new Xlsx($objSpreadsheet); $objWriter->save('test6-a2.xlsx'); exit(); ?>
エラーが発生しているのは 23行目の save の部分です。 MS-EXCEL で開いたままの状態なので、ファイルにロックが掛かっていて上書きができない状況です。
MS-EXCEL を閉じてから再実行すれば問題無く書込みはされるのですが、これではプログラムとしてはまずいので エラーが発生したことを画面に出力してやります。
その前に、PhpSpreadsheet\Writer\BaseWriter.php のソースの 125行目の部分を覗いてみます。
125行目では fopen 関数でファイルオープンを行っていて、ファイルハンドルが取得できない場合には Exception を投げています。 この Exception を save 関数に実行で Catch してやればエラーが時の処理が書けることが分かります。$fileHandle = $filename ? fopen($filename, $mode) : false; if ($fileHandle === false) { throw new Exception('Could not open file "' . $filename . '" for writing.'); }
それでは save 関数の実行を Try Catch で囲んでやって Catch の中でエラー時の処理を書きます。
(エラー処理といっても、単にエラーメッセージを表示するだけですが。)
なお save 関数の直前で PHP の error_reporting 関数で WARNING のエラー表示をしない様に抑止しています。
これが無いと WARNING が表示されてうるさいので。<?php // ライブラリ読込 require '../vendor/autoload.php'; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Writer\Xlsx; // Spreadsheetオブジェクト生成 $objSpreadsheet = new Spreadsheet(); // ワークシートオブジェクト $objSheet = $objSpreadsheet->getActiveSheet(); // セルにシステム時刻(Unix Time)を設定 $objSheet->setCellValue('A2', Date::PHPToExcel(time())); $objSheet->setCellValue([1, 3], Date::PHPToExcel(time())); // スタイルオブジェクト取得([A2]セル) $objStyle = $objSheet->getStyle('A2')->getNumberFormat()->setFormatCode('yyyy"年"m"月"d"日";@'); $objStyle = $objSheet->getStyle([1, 3, 1, 3])->getNumberFormat()->setFormatCode('yyyy"年"m"月"d"日";@'); // [test6-a2.xlsx]:Excel2007形式で保存する $objWriter = new Xlsx($objSpreadsheet); try { error_reporting(E_ALL & ~E_WARNING); $objWriter->save('test6-a3.xlsx'); } catch (Exception $e) { echo "ファイル書込みエラーが発生しました。:".$e->getMessage(), "\n"; } exit(); ?>
-
今回はさらに小ネタですが、以下の記事では、セルに PHP のUNIT-TIMEデータを日付型データとして設定し、日付の書式で表示させる方法について説明しました。
この時のセルの書込み位置指定で従来の方法で "A2" としていましたが、最近のバージョンの PhpSpreadsheet では「列No, 行No」と配列での指定が可能になった様です。
⇒PHP PhpSpreadsheet エクセルのワークシートのセルに日付型データの設定と、日付スタイル設定について(PHPToExcel, setFormatCode)
そこで、XAMPP のPHPバージョンを「7.4.32」にアップしたのに伴って、PhpSpreadsheet のバージョンアップを行います。
まずはWindowsのコマンドプロンプトから以下のコマンドを実行します。C:\xampp\htdocs>composer config platform.php 7.4.32
「C:\xampp\htdocs\composer.json」ファイルの内容が以下の様になります。 phpspreadsheet のバージョンはそのままです。
{ "require": { "phpoffice/phpspreadsheet": "^1.8" }, "config": { "platform": { "php": "7.4.32" } } }
composer require phpoffice/phpspreadsheet
このコマンドで実行したのですが、いろいろエラーが表示されたので、「C:\xampp\htdocs\vendor」フォルダを別のものにして 「vendor」が存在しない状態で再度、上記のコマンドを実行しました。
(ちょっと強引な感じですが、更新方法が良くわからなかったので。)
「7-ZIP」という圧縮・解凍ツールがインストールされている場合は、取り敢えずアンインストールします。
(phpspreadsheet 取込の composer でファイル解凍時?にエラーになります。)
再度、コマンドを実行したら、新しく「vendor」以下が構築された様です。C:\xampp\htdocs>composer require phpoffice/phpspreadsheet Info from https://repo.packagist.org: #StandWithUkraine Using version ^1.25 for phpoffice/phpspreadsheet ./composer.json has been updated Running composer update phpoffice/phpspreadsheet Loading composer repositories with package information Updating dependencies Nothing to modify in lock file Installing dependencies from lock file (including require-dev) Package operations: 11 installs, 0 updates, 0 removals 0 [>---------------------------] 0 [->--------------------------] - Installing myclabs/php-enum (1.8.4): Extracting archive - Installing psr/simple-cache (1.0.1): Extracting archive - Installing psr/http-message (1.0.1): Extracting archive - Installing psr/http-factory (1.0.1): Extracting archive - Installing psr/http-client (1.0.1): Extracting archive - Installing markbaker/matrix (3.0.0): Extracting archive - Installing markbaker/complex (3.0.1): Extracting archive - Installing symfony/polyfill-mbstring (v1.26.0): Extracting archive - Installing maennchen/zipstream-php (2.2.1): Extracting archive - Installing ezyang/htmlpurifier (v4.16.0): Extracting archive - Installing phpoffice/phpspreadsheet (1.25.2): Extracting archive 0/11 [>---------------------------] 0% 9/11 [======================>-----] 81% 10/11 [=========================>--] 90% 11/11 [============================] 100% Generating autoload files 3 packages you are using are looking for funding. Use the `composer fund` command to find out more! No security vulnerability advisories found
■「setCellValue、getStyle」のセル指定に配列を使用
セルに座標を設定する場合、従来は "A2" の様に列のアルファベットと行Noで指定していましたが、 ここに [1, 2] の様に [列No, 行No] の指定が出来る様になりました。
なお、スタイルオブジェクトを取得する getStyle の場合は [左上列No, 左上行No, 右下列No, 右下行No] の範囲指定で行います。
以下に簡単な例を示します。 [A2] および [A3] セルに現在時刻を設定し、そのセルの書式スタイルを日付表示の設定で行っています。
<?php // ライブラリ読込 require '../vendor/autoload.php'; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Writer\Xlsx; // Spreadsheetオブジェクト生成 $objSpreadsheet = new Spreadsheet(); // ワークシートオブジェクト $objSheet = $objSpreadsheet->getActiveSheet(); // セルにシステム時刻(Unix Time)を設定 $objSheet->setCellValue('A2', Date::PHPToExcel(time())); $objSheet->setCellValue([1, 3], Date::PHPToExcel(time())); // スタイルオブジェクト取得([A2]セル) $objStyle = $objSheet->getStyle('A2')->getNumberFormat()->setFormatCode('yyyy"年"m"月"d"日";@'); $objStyle = $objSheet->getStyle([1, 3, 1, 3])->getNumberFormat()->setFormatCode('yyyy"年"m"月"d"日";@'); // [test6-a2.xlsx]:Excel2007形式で保存する $objWriter = new Xlsx($objSpreadsheet); $objWriter->save('test6-a2.xlsx'); exit(); ?>
出力されたエクセルファイルを見てみると以下の様になります。[A]列の幅は便宜上広くして表示しました。