【PHP】ファイルの変更箇所を一括修正する

フォルダの中のファイルをすべて一括で修正したい!

ということで今回は、前回記事で取得したファイルの内容それぞれに対して修正箇所を指定して一括で書き換えるプログラムを作成したのでここに残しておく。

フォルダのファイルパスを配列で取得

これが前々回の記事の内容。
フォルダからすべてのファイルのパスをまとめて取得する方法はそちらの記事にまとめておいた。

すべてのファイルパスからソースを配列で受け取る

こっちが前回の記事。
ファイルすべての内容を配列として取得できる。(ただしその分メモリを食うので余計なファイルパスは外しておくのがベター)

ソースを部分的にかつ一括編集するには?

今回のやりたかった作業は特定の箇所をそうとっかえすることだった。

つまり「文字列 $source の中の一致する文字列 $str を文字列 $replace に置き換える」プログラムだ。

これをすべてのファイル(すなわち$source1, $source2, ..)に対して複数箇所($str1, $str2, ..)書き換えをかけるものを作りたい。

これをまとめると

  • 文字列 $source の中の一致する文字列 $str を文字列 $replace に置き換える
  • 任意個の $source に
  • 任意個の $str を $replace へ

それでは始めていく。

文字列の中の文字を置き換える関数

今回は strtr() 関数を使っていく。(当初はstr_replace()を使っていたがstrtr()が使いこなせればよりスマートな記述になる。)

https://www.php.net/manual/ja/function.strtr

strtr(文字列, array('変換したい文字列’ => '変換後の文字列’));

これで文字列の中の変換が完了する強力な関数だ。

$contents = 
  '<?php
  echo "Hello, World!!";
  echo "Hello, World!!";
   ?>';
$array = [
  'echo' =>'print',
  'Hello' => 'Hi',
  'World' => 'Japan'
];

$buff = fileContentsReplace($contents, $array);

var_dump($buff);

結果

string(57) 
  "<?php
  print "Hi, Japan!!";
  print "Hi, Japan!!";
   ?>"

きれいに変換することができた。

関数を作成する

function fileContentsReplace($file_contents = [], $arrayReplace = [''=>''])
{
  $buffs = [];
  //複数のファイルに対してすべてに置換を適用
  foreach ($file_contents as $file_content) {
     array_push($buffs, strtr($file_content, $arrayReplace));
    $j++;
  }
  return $buffs;
}

これが核となるコード。

strtr()に$file_contents の配列の中身をforeachで回してひとつずつ変換してarray_push()関数で $buffs 配列に収めている。

これでぶっちゃけ処理自体は完了するのだが、ファイルソースが数万字とかあったらほんとにできたのか不安になるし、わざわざテキストエディタで開いてチェックするなんてプログラムを作った意味がない。

というわけでチェックするためのあれこれを実装させていく。

君、どこを直した?

ファイルそれぞれについて、修正した(する前の)文字列を確認したかったのでこんなコードを入れた。

print '<br>ファイル: '.$j;
    $i = 0;
    foreach ($arrayReplace as $uncheck => $checked) {
      if (strpos($file_content, $uncheck) !== false) {
        print '<br>訂正箇所: '. $i.' '. htmlspecialchars($uncheck). '<br>';
      }
      $i++;
    }

まとめるとこっち

function fileContentsReplace($file_contents = [], $arrayReplace = [''=>''])
{
  $buffs = [];
  //複数のファイルに対してすべてに置換を適用
  $j = 0;
  foreach ($file_contents as $file_content) {
    print '<br>ファイル: '.$j;
    $i = 0;
    foreach ($arrayReplace as $uncheck => $checked) {
      if (strpos($file_content, $uncheck) !== false) {
        print '<br>訂正箇所: '. $i.' '. htmlspecialchars($uncheck). '<br>';
      }
      $i++;
    }
    // array_push($buffs, str_replace($before, $after, $file_content));
     array_push($buffs, strtr($file_content, $arrayReplace));
    $j++;
  }
  return $buffs;
}

ファイルそれぞれに対して、strpos() 関数を使ってどの変換前文字(以後$uncheck)があるのかを返してくれる。

strpos()はbool値を返すので「!== false」は冗長と思われるかもしれない。
が、strpos()が「(int)0」を返した場合、bool値に変換するとfalseになってしまうためこのように必ず「!== false」をつけた方がベターらしい。

https://www.php.net/manual/ja/function.strpos.php

結果、このようにprint表示してくれる。

ファイル: 0
訂正箇所: 0 echo
訂正箇所: 1 Hello
訂正箇所: 2 World

その箇所が修正されたんだな〜とわかるようになった。

君、ほんとに直した?

当然、成功したのか失敗したのかがわからないと夜も眠れないのでそれをチェックするためのコードを追加する。

一気に完成版を載せるので順番に解説を残す。

function fileContentsReplace($file_contents = [], $arrayReplace = [''=>''])
{
  $buffs = [];
  $j = 0;
  //チェッカーのためのフラグ
  $flg = [];
  foreach ($file_contents as $file_content) {
    print '<br>ファイル: '.$j;
    $i = 0;
    foreach ($arrayReplace as $uncheck => $checked) {
      if (strpos($file_content, $uncheck) !== false) {
        print '<br>訂正箇所: '. $i.' '. htmlspecialchars($uncheck). '<br>';
        //このフラグが'is'ならチェッカーが発動する
        //j番目ファイルのi番目の訂正箇所
        $flg[$j][$i] = 'is';
      }
      $i++;
    }
     array_push($buffs, strtr($file_content, $arrayReplace));
    $j++;
  }
  //ここからチェッカー
  print '<br>ファイル編集チェッカー<br>';
  $j = 0;
  foreach ($buffs as $buff) {
    print '<br>ファイル'. $j;
    //残り物のフラグ
    $flg1 = true;
    //未完了のフラグ
    $flg2 = true;
    $i = 0;
    foreach ($arrayReplace as $uncheck => $checked) {
      if ($flg[$j][$i] === 'is') {
        //まだ残ってたら
        if (strpos($buff, $uncheck)) {
          $flg1 = false;
          print '<br>未訂正箇所:'. $i.' '. htmlspecialchars($uncheck). '<br>';
        }
        //まだ変換できてなかったら
        if (strpos($buff, $checked) === false) {
          $flg2 = false;
          print '<br>訂正エラー:'. $i.' '. htmlspecialchars($checked). '<br>';
        }
      }
      ++$i;
    }
    //j番目ファイルのi番目の訂正箇所はどうか
    $flg1 === true && $flg2 === true ? print '<br>OK' : print '未完了';
    $j++;
  }
  return $buffs;
}

flgは3種類あるので注意する。

最初の$flg[$j][$i]は、チェッカーが無駄に作動しすぎないために用意した。
これでファイルに訂正箇所があったところだけでチェッカーが働いてくれる。

$flg が’is’の場合、strpos($buff, $uncheck)でソース内に未訂正箇所が残っていないかを確かめている。

残ってたらエラー表示する。

strpos($buff, $uncheck)は実際、正しくは(0でfalse回避のため)strpos($buff, $uncheck) !== false にする必要があるが、今回$uncheck が0番目にないことはわかっていたので許している。

表示するときにhtmlコードがブラウザで解釈されないようにhtmlspecialchars()で無効化している。

結果はこうなる。

ファイル: 0
訂正箇所: 0 echo
訂正箇所: 1 Hello
訂正箇所: 2 World
ファイル編集チェッカー
ファイル0
OK

ファイルのどこを修正したのかを一覧で表示させた後、編集が完了したのかを表示させている。

作動スイッチはどこ?!それはflgにおまかせ

flgがありがたい状況の実例を残しておく。

訂正箇所の$array[]に無関係な要素を追加してみる。

$contents = [
  '<?php
  echo "Hello, World!!";
  echo "Hello, World!!";
   ?>'
];
$array = [
  'echo' =>'print',
  'Hello' => 'Hi',
  'foo' => 'bar',
  'World' => 'Japan'
];

実行すると?

ファイル: 0
訂正箇所: 0 echo
訂正箇所: 1 Hello
訂正箇所: 3 World
ファイル編集チェッカー
ファイル0
OK

「訂正箇所」のインデックス2が飛ばされて、「ファイル編集チェッカー」の$checked エラーも表示されていないのがわかる。

(そのままだと’foo’ => 'bar’,が未編集だと誤解されてエラーメッセージが表示される)

そういった、ちょっとした条件スイッチを柔軟に対応させたい時ってけっこうある気がする。

flgの使い方を少し知っているだけでも臨機応変なコードの組み方はいくらでもできると思うのでアルゴリズムの勉強は大事なんだなと実感できた瞬間だった。

まとめ

今回はファイルのソースコードを修正するプログラムをまとめてみた。(実際本編、あとの記事はおまけ)

こうやって書いてみると核となるパーツなんてほんのちょっとで、例外処理とかログとかそういったところも丁寧に組み込めるのが腕の見せどころなのか?とか感じた。

編集のに続けて、ファイルとファイルの内容をくっつける作業もやる必要があったので、次回記事ではファイルとファイルの一括結合に挑戦!