[PHP]データの扱い:Iterator

#プログラミング #PHP #Iterator #Generator

データをどう持たせる・扱うとコードが綺麗になる・バグが生み出されにくくなるかを考えます。

1.データを配列で持たせる

その場限りの利用ならシンプルに下記で問題はなさそうです。

<?php
    class User{
        public function setUser($db){
            $user             = array();
            $user['name']     = $db['name'];
            $user['age']      = $db['age'];
            $user['address']  = $db['address'];
            $user['mail']     = $db['mail'];
            $user['homepage'] = $db['homepage'];

            return $user;
        }
    }

連想配列でなく、stdClassを利用する方法もあります。
※利用するメリットは、好き嫌いの話だけになりそう。stdClassの方が個人的には見栄えがよくて好きです。

<?php
    class User{
        public function setUser($db){
            $user           = new stdClass;
            $user->name     = $db['name'];
            $user->age      = $db['age'];
            $user->address  = $db['address'];
            $user->mail     = $db['mail'];
            $user->homepage = $db['homepage'];

            return (array)$user;
        }
    }

stdClass型の特徴(配列と比較して)

・初期化の際、タイプ文字数が多くなる傾向(複雑なケースは多くなりますが、通常は配列とほぼ変わりません)
・利用できる関数が少ない→データ入れた後に処理する場合は向かない
・参照型として扱われる
 https://qiita.com/mpyw/items/bd38da57837d35214aae

$arr2 = ['foo' => ['bar' => ['baz' => 'A']]];
$obj1 = new stdClass;
$obj1->foo = new stdClass;
$obj1->foo->bar = new stdClass;
$obj1->foo->bar->baz = 'A';

https://teratail.com/questions/3541
https://qiita.com/onomame/items/be2261c6eb566edab030

2.データ構造をメンバークラスに分離する

利用する箇所が複数あるデータの場合は、データ構造をメンバークラスに分離した方がよさそうです。(今後そのデータ構造を流用する箇所が出そうである場合も)

メリット:
・別のところで同じデータ構造を利用する場合に間違いにくい
・初期化もメンバークラスで行えるので実処理の部分で行う必要がない
・他の人が見たときに、データ構造を把握しやすい

メンバークラスのプロパティを「public」にしたケース:
プロパティは何が何でもprivateにしないと駄目というのは盲目的すぎるのでこれで十分というケースはありそうです。(getter/setterを大量に作るのも、コード量が増えてよくないですし)

<?php
    //メンバークラス
    class UserEntity{
        public $user = "";
        public $age  = 0;
        public $address = "";
        public $mail = "";
        public $homepage = "";
    }
    
    class User{
        public function setUser($db){
            $userEntity = new UserEntity();
            $userEntity->name     = $db['name'];
            $userEntity->age      = $db['age'];
            $userEntity->address  = $db['age'];
            $userEntity->mail     = $db['mail'];
            $userEntity->homepage = $db['homepage'];
            //インスタンスを返す
            return $userEntity;
        }
    }

メンバークラスのプロパティを「private」にしたケース:
プロパティが「public」だとメンバークラスの外からもアクセス可能になり、意図しないところでデータをセットされる可能があります。そのため、メンバークラスのプロパティをprivateにして、できることを制限します。

<?php
    //メンバークラス
    class UserEntity{
        private $user = "";
        private $age  = 0;
        private $address = "";
        private $mail = "";
        private $homepage = "";

        public function __construct($db){
            $this->user = $db['user'];
            $this->age  = $db['age'];
            $this->address = $db['address'];
            $this->mail = $db['mail'];
            $this->homepage = $db['homepage'];
        }

        public function getUser(){
            return $this->user;
        }
        public function getAge(){
            return $this->age;
        }
        public function getAddress(){
            return $this->address;
        }
        public function getMail(){
            return $this->mail;
        }
        public function getHomepage(){
            return $this->homepage;
        }
    }
    
    class User{
        public function setUser($db){
            $userEntity = new UserEntity($db);
            //インスタンスを返す
            return $userEntity;
        }
    }

補足:getter / setterをプロパティごとに作らない方法:

getter / setterをプロパティごとに作らない方法もありますが
その場合、どんなプロパティを持つか明示されず保守性が下がるので
なるべくやらない方がよさそうです。
https://qiita.com/mikakane/items/00c798964f7c2c122e7d
※gong023さんのコメントが参考になりました。

class UserEntity{
    private $var = [];

    public function __get($key){
        return $this->get($key);
    }

    public function __set($key,$value){
        $this->set($key,$value);
    }

    public function get($key,$default=null){
        if(array_key_exists($key,$this->var)){
            return $this->var[$key];
        }
        return $default;
    }

    public function set($key,$value){
        $this->var[$key] = $value;
    }
}

class User{
    public function setUser($db){
        $userEntity = new UserEntity();
        $userEntity->name     = $db['name'];
        $userEntity->age      = $db['age'];
        $userEntity->address  = $db['age'];
        $userEntity->mail     = $db['mail'];
        $userEntity->homepage = $db['homepage'];
        //インスタンスを返す
        return $userEntity;
    }
}

3.データに対しての共通処理化:Iterator

データ構造が複雑、なおかつ色々なところでそのデータ構造が利用されている場合に、機能変更の度に色々なところを書き換えるのは手間なので、処理を共通化させるためにIteratorを利用します。

<?php
class User{
    private $name;
    private $age;
    private $address;
    private $mail;
    private $homepage;
    
    public function __construct($name, $age, $address, $mail, $homepage){
        $this->name     = $name;
        $this->age      = $age;
        $this->address  = $address;
        $this->mail     = $mail;
        $this->homepage = $homepage;
    }
    public function getName(){
        return $this->name;
    }
    public function getAge(){
        return $this->age;
    }
    public function getAddress(){
        return $this->address;
    }
    public function getMail(){
        return $this->mail;
    }
    public function getHomepage(){
        return $this->homepage;
    }
}

//イテレータ:オブジェクトに対する反復処理を行う
class Users implements IteratorAggregate{
    private $users;
    public function __construct(){
        $this->users = new ArrayObject();
    }
    public function add(User $user){
        $this->users[] = $user;
    }
    public function getIterator(){
        return $this->users->getIterator();
    }
}

//データに対してのフィルタ処理
class AdressIterator extends FilterIterator{
    public function __construct($iterator){
        parent::__construct($iterator);
    }
    
    /*
     * 住所が「東京都」のみ抽出
     */
    public function accept(){
        $user = $this->current();
        return ($user->getAddress() === '東京都');
    }
}

$users = new Users();
$users->add(new User("山田太郎", 20, "東京都", "a@yahoo.co.jp", "https://note.mu/a"));
$users->add(new User("田中一郎", 30, "埼玉県", "b@yahoo.co.jp", "https://note.mu/b"));
$users->add(new User("佐藤幸一", 25, "東京都", "c@yahoo.co.jp", "https://note.mu/c"));
$users->add(new User("益田喜平", 28, "茨城県", "d@yahoo.co.jp", "https://note.mu/d"));
$users->add(new User("岬洋平", 32, "東京都", "e@yahoo.co.jp", "https://note.mu/e"));
$iterator = new AdressIterator($users->getIterator());

foreach($iterator as $user){
    echo $user->getName() . " | " . 
         $user->getAge()  . " | " . 
         $user->getAddress() . " | " . 
         $user->getMail() . " | " .
         $user->getHomepage() . "<br>";
}

Iteratorインタフェースで簡単に色々な機能を利用することもできます。

AppendIterator
appendメソッドで、配列を追加したらまとめて処理することができます。
https://qiita.com/suin/items/697e07d32a8408ea2fc8

$headerBlock = new ArrayIterator([
    ['ID', 'Name', 'Email'],
]);

//複数
$contentBlock = new ArrayIterator([
    ['1', 'Alice', 'alice@example.com'],
    ['2', 'Bob', 'bob@example.com'],
]);

$rows = new AppendIterator;
$rows->append($headerBlock);
$rows->append($contentBlock);

foreach ($rows as $row) {
    var_dump($row);
}

CachingIterator
イテレータのキャッシュを行うことができます。

$collection = new CachingIterator(
                  new ArrayIterator(
                      array('Cat', 'Dog', 'Elephant', 'Tiger', 'Shark')));

foreach($collection as $animal) {
     echo "Current: $animal";
     if($collection->hasNext()) {
         echo " - Next:" . $collection->getInnerIterator()->current();
     }
     echo PHP_EOL;
 }

//出力:
//Current: Cat - Next:Dog
//Current: Dog - Next:Elephant
//Current: Elephant - Next:Tiger
//Current: Tiger - Next:Shark
//Current: Shark

連想配列で、先読みしたい場合に利用するケース:
https://stackoverflow.com/questions/2458099/peek-ahead-when-iterating-an-array-in-php

RegexIterator
正規表現を使ってイテレータをフィルタします。

$arrayIterator = new ArrayIterator(array('test 1', 'another test', 'test 123'));
$regexIterator = new RegexIterator($arrayIterator, '/^test/');

foreach ($regexIterator as $value) {
    echo $value . "\n";
}

RecursiveIteratorIterator
再帰的なイテレータの反復処理に利用します。

$array = array(
    array(
        array(
            array(
                'leaf-0-0-0-0',
                'leaf-0-0-0-1'
            ),
            'leaf-0-0-0'
        ),
        array(
            array(
                'leaf-0-1-0-0',
                'leaf-0-1-0-1'
            ),
            'leaf-0-1-0'
        ),
        'leaf-0-0'
    )
);

$iterator = new RecursiveIteratorIterator(
    new RecursiveArrayIterator($array),
    RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($iterator as $key => $leaf) {
    echo "$key => $leaf", PHP_EOL;
}

//出力:
//0 => leaf-0-0-0-0
//1 => leaf-0-0-0-1
//1 => leaf-0-0-0
//0 => leaf-0-1-0-0
//1 => leaf-0-1-0-1
//1 => leaf-0-1-0
//2 => leaf-0-0

サブフォルダ内のファイルをまとめて取得する
https://qiita.com/re-24/items/94eeea3e8051e212d9ed

RecursiveDirectoryIterator
ファイルシステムのディレクトリを再帰的に反復処理します。

$directory = '.';

$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory));

$it->rewind();
while($it->valid()) {

    if (!$it->isDot()) {
        echo 'SubPathName: ' . $it->getSubPathName() . "\n";
        echo 'SubPath:     ' . $it->getSubPath() . "\n";
        echo 'Key:         ' . $it->key() . "\n\n";
    }

    $it->next();
}

//出力:
//SubPathName: a/a.txt
//SubPath:     a
//Key:         ./a/a.txt

4.シンプルに反復処理を書く:ジェネレータ

>ジェネレータの最大のメリットは、シンプルに書けることです。 Iterator を実装するのに比べて、必要な決まり文句の数がかなり少なくなります。 また、ジェネレータを使ったコードのほうが、一般的に読みやすくなります。
http://php.net/manual/ja/language.generators.comparison.php

function filterAddress($ite) {
  foreach ($ite as $v) {
    if ($v[2]=="東京都") yield $v;
  }
}

$users = [
    ["山田太郎", 20, "東京都", "a@yahoo.co.jp", "https://note.mu/a"],
    ["田中一郎", 30, "埼玉県", "b@yahoo.co.jp", "https://note.mu/b"],
    ["佐藤幸一", 25, "東京都", "c@yahoo.co.jp", "https://note.mu/c"],
    ["益田喜平", 28, "茨城県", "d@yahoo.co.jp", "https://note.mu/d"],
    ["岬洋平", 32, "東京都", "e@yahoo.co.jp", "https://note.mu/e"]
];

foreach (filterAddress($users) as $user) {
    var_dump($user);
}

メリット:
・Iteratorよりシンプルに書ける
・全部の値が格納された配列を用意する必要がなく、値がループされるたびに生成されるのでメモリの節約になる

デメリット:
・ジェネレータは前方にしか進めないイテレータなので、いったん反復処理が始まれば巻き戻すことができない

コードをまとめる技術としてのイテレータとジェネレータ
https://qiita.com/Hiraku/items/0db9a8fed4743c1f00a4

Iteratorとジェネレータを組み合わせて利用しているコード例
https://github.com/koriym/Koriym.Psr4List/blob/master/src/Psr4List.php