iniファイル内のデータを読み込み、配列にセットする

iniファイルからデータを読み込んで配列にセットするPHPコードは以下のようになります。
$config = parse_ini_file("config.ini");
上記のコードでは、parse_ini_file() 関数を使用して、指定された config.ini ファイルからデータを読み込みます。読み込んだデータは、連想配列として $config 変数に格納されます。

config.ini ファイルの例を以下に示します。
[database]
host = localhost
username = root
password = secret
dbname = mydatabase

[server]
ip = 192.168.0.1
port = 8080
上記の config.ini ファイルから、以下のような配列が生成されます。
Array
(
    [database] => Array
        (
            [host] => localhost
            [username] => root
            [password] => secret
            [dbname] => mydatabase
        )

    [server] => Array
        (
            [ip] => 192.168.0.1
            [port] => 8080
        )
)
上記のように、セクション名をキーとして、各キーに対応するセクション内のキーと値のペアが、2次元配列として格納されます。

配列をiniファイルへ書き込む

配列をiniファイルへ書き込むPHPコードは以下のようになります。

$data = array(
    'database' => array(
        'host' => 'localhost',
        'username' => 'root',
        'password' => 'secret',
        'dbname' => 'mydatabase'
    ),
    'server' => array(
        'ip' => '192.168.0.1',
        'port' => '8080'
    )
);

$config = '';

foreach ($data as $key => $value) {
    $config .= "[$key]\n";
    foreach ($value as $k => $v) {
        $config .= "$k = $v\n";
    }
    $config .= "\n";
}

file_put_contents('config.ini', $config);
上記のコードでは、配列 $data をループ処理して、iniファイルの文字列を生成します。生成された文字列は file_put_contents() 関数を使用して、config.ini ファイルに書き込まれます。

生成される config.ini ファイルの内容は以下のようになります。
makefile
Copy code
[database]
host = localhost
username = root
password = secret
dbname = mydatabase

[server]
ip = 192.168.0.1
port = 8080
上記のように、セクション名をキーとして、各キーに対応するセクション内のキーと値のペアが、iniファイルに書き込まれます。

SQLサニタイズ【2023年版】

SQLインジェクション対策の歴史を辿ると、以前はaddslashes関数が主に使われていました。この関数はクエリ内の特殊文字をエスケープすることで、ある程度のSQLインジェクション攻撃を防ぐことが可能でした。 実際、私自身が開発したシステムに対してハッキングを試みた経験がありますが、全ての試みが失敗に終わったことからも、その効果は一定程度認められます。

しかし、全ての攻撃を防ぐには至らず、未知の可能性に対する不安も残ります。 特に、特定の文字列を異なるエンコーディング間で変換すると、予期せぬバックスラッシュが出現する場合があります。 例えば、"表"という文字をUTF-8からShift-JISに変換すると、この現象が発生します。このバックスラッシュは新たなセキュリティ上のリスクを生む可能性があり、大きな懸念材料となっています。

旧来の管理システムをよりセキュアなプリペアドステートメント方式へ書き換えるという対策もありますが、その修正コストは決して安価ではありません。 そのため、私はaddslashes関数を使わない新たなSQLサニタイズ方式を考案しました。しかし、SQLサニタイズ自体が時代遅れのアプローチであるとの認識もあり、この方式を使用するべきはやむを得ない理由がある場合だけとなります。

2023年現在、最先端のSQLインジェクション対策としてはプリペアドステートメント方式が主流となっています。 この方式は、SQLのパラメータを一度に指定することで、インジェクション攻撃を防ぐことができます。そのため、既存のシステムの安全性を向上させるためには、可能な限りこのプリペアドステートメント方式を採用すべきであると強く推奨します。

sqlSanitize自作関数


	function sqlSanitize($text) {
		$text = trim($text);
		
		// 文字列がUTF-8でない場合、UTF-8に変換する
		if(!mb_check_encoding($text, 'UTF-8')){
			$text = str_replace(['¥¥', '/', '¥'', '"', '`',' OR '], '', $text);
			$text = mb_convert_encoding($text, 'UTF-8');
		}
			
		// SQLインジェクションのための特殊文字をエスケープする
		$search = array("¥¥", "¥x00", "¥n", "¥r", "'", '"', "¥x1a", "`");
		$replace = array("¥¥¥¥", "¥¥0", "¥¥n", "¥¥r", "¥¥'", "¥¥¥"", "¥¥Z", "");
		
		$text = str_replace($search, $replace, $text);
			
		return $text;
	}
	
検証


require_onceを省略する | spl_autoload_register

spl_autoload_registerを使用することで、MVC(Model-View-Controller)モデルにおいてもrequire_onceを省略することが可能です。以下に、簡単な例を示します。

まず、オートローダーを設定するためのPHPファイル(たとえばautoload.php)を作成します。

php

// autoload.php
spl_autoload_register(function ($class) {
    // プレフィックスまたは名前空間を置き換える(もし必要な場合)
    $prefix = 'App\\';
    $base_dir = __DIR__ . '/';

    // クラスにプレフィックスがある場合
    $len = strlen($prefix);
    if (strncmp($prefix, $class, $len) !== 0) {
        return;
    }

    // 名前空間やクラス名の残りの部分を取得
    $relative_class = substr($class, $len);

    // クラスファイルのパスを取得(ここで適切なディレクトリ構造に変更する)
    $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';

    // クラスファイルが存在する場合は読み込む
    if (file_exists($file)) {
        require $file;
    }
});
このautoload.phpをプロジェクトのエントリポイント(通常はindex.php)で読み込むことで、必要なクラスが自動的に読み込まれるようになります。

php

// index.php
require 'path/to/autoload.php';

// 以下は自動で読み込まれる
$controller = new App\Controller\MyController();
$model = new App\Model\MyModel();
$view = new App\View\MyView();
上記のautoload.phpでは、クラスの名前空間がApp\で始まり、それに続く部分(Controller\MyController、Model\MyModelなど)がそのクラスが存在するディレクトリ構造と一致していることを前提としています。

この方法を適用すると、require_onceを各ファイルで書く手間が省けます。ただし、クラスとディレクトリ構造が一致している必要があります。それが確保できるならば、このオートロードの方法は非常に便利です。


セッション(session)のデータをDBで管理するオリジナルクラス | PDOSessionHandler

PDO (PHP Data Objects) とネイティブの PHP セッションハンドリング機能を使ってセッション情報をデータベースに保存する方法を以下に示します。

データベーステーブルの作成

まず、セッションデータを保存するためのデータベーステーブルを作成します。
CREATE TABLE sessions (
    id VARCHAR(128) NOT NULL PRIMARY KEY,
    data TEXT,
    timestamp TIMESTAMP NOT NULL
);

カスタムセッションハンドラの作成

次に、カスタムセッションハンドラを作成します。この例では、PDO を使用しています。

class PDOSessionHandler
{
    private $pdo;

    public function __construct($pdo)
    {
        $this->pdo = $pdo;
    }

    public function open($savePath, $sessionName)
    {
        return true;
    }

    public function close()
    {
        return true;
    }

    public function read($id)
    {
        $stmt = $this->pdo->prepare("SELECT data FROM sessions WHERE id = :id");
        $stmt->bindParam(':id', $id);
        $stmt->execute();

        if ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            return $row['data'];
        }

        return '';
    }

    public function write($id, $data)
    {
        $timestamp = time();
        $stmt = $this->pdo->prepare("REPLACE INTO sessions (id, data, timestamp) VALUES (:id, :data, :timestamp)");
        $stmt->bindParam(':id', $id);
        $stmt->bindParam(':data', $data);
        $stmt->bindParam(':timestamp', $timestamp, PDO::PARAM_INT);
        return $stmt->execute();
    }

    public function destroy($id)
    {
        $stmt = $this->pdo->prepare("DELETE FROM sessions WHERE id = :id");
        $stmt->bindParam(':id', $id);
        return $stmt->execute();
    }

    public function gc($maxLifetime)
    {
        $old = time() - $maxLifetime;
        $stmt = $this->pdo->prepare("DELETE FROM sessions WHERE timestamp < :old");
        $stmt->bindParam(':old', $old, PDO::PARAM_INT);
        return $stmt->execute();
    }
}

使用方法

session_start()を実行する前に上記のクラスPDOSessionHandlerで設定処理を施します。

$pdo = new PDO("mysql:host=localhost;dbname=mydatabase", "username", "password");

$handler = new PDOSessionHandler($pdo);

session_set_save_handler(
    [$handler, 'open'],
    [$handler, 'close'],
    [$handler, 'read'],
    [$handler, 'write'],
    [$handler, 'destroy'],
    [$handler, 'gc']
);

// セッションを開始する
session_start();
これで、PDO を使用してセッションデータをデータベースに保存できるようになりました。$_SESSION変数を通常通り使用することで、そのデータは自動的にデータベースに保存されます。

この例は簡単なものであり、本番環境で使用する場合にはさまざまな点(エラーハンドリング、トランザクション処理、セキュリティなど)を考慮に入れる必要があります。

LINE Message API: リッチメニューをCURLで登録

  1. 事前にLINE Developerのサイトでチャネルを作成し、さらに検証用のLINE公式アカウントを解説しておく。
    検証用のLINE公式アカウントをスマホのLINEアプリで登録し、そのLINE公式アカウントととのトーク画面を開くとリッチメニューを確認する準備が整う。
  2. LINE Developerコンソールにログインする。
  3. 対象のチャネルを選択
  4. 「Messaging API」タブをクリックすると画面下側にチャンネルアクセストークン(Channel access token)があるのでコピペで控える。
    「Messaging API」タブをクリックすると画面下側に「Channel access token」がある

  5. 適当なコマンドラインツールを開く。(今回の検証ではWindows for Gitに付属するGit Bashを用いた)
  6. 以下のコマンドのチャンネルアクセストークンを上記で控えていたものに書き換えて実行する。環境によっては日本語が使いないこともありようです。私のGitBash環境では日本語をコマンドに記述するとエラーになりました。
    
    	
    curl -v -X POST https://api.line.me/v2/bot/richmenu \
    -H 'Authorization: Bearer lPdXJ1j4doZCNpA8xxxxxxxxxxxxxxxチャンネルアクセストークンxxxxxxxxxxxxijoqzvjjocigrg7oPCwCmUZXpXSXpXvi2GqIlV5QBSagUHTzrmKLSAdB04t89/1O/w1cDnyilFU=' \
    -H 'Content-Type: application/json' \
    -d \
    '{
        "size": {
            "width": 2500,
            "height": 843
        },
        "selected": false,
        "name": "Animal Rich",
        "chatBarText": "Tap to open",
        "areas": [
            {
                "bounds": {
                    "x": 0,
                    "y": 0,
                    "width": 1250,
                    "height": 843
                },
                "action": {
                    "type": "uri",
                    "label": "Cat Tap",
                    "uri": "https://www.line-community.me/ja/"
                }
            },
            {
                "bounds": {
                    "x": 1250,
                    "y": 0,
                    "width": 1250,
                    "height": 843
                },
                "action": {
                    "type": "uri",
                    "label": "Dog Tap",
                    "uri": "https://techblog.lycorp.co.jp/ja/"
                }
            }
        ]
    }'
    	
  7. 以下のようなレスポンスが得られたら成功です。リッチメニューID(richMenuId)が発行されるので控えておきます。
    { [58 bytes data]
    100   900  100    58  100   842    195   2844 --:--:-- --:--:-- --:--:--  3050{"richMenuId":"richmenu-xxxリッチメニューIDxxxxb217e75d78d5fba09b254"}
    * Connection #0 to host api.line.me left intact
    
    
  8. リッチメニューの背景に表示する画像を用意し、任意のディレクトリに配置します。
    画像の幅サイズは上記のコマンドの設定値と整合性を取るようにしたほうが無難です。原因不明なエラーが起こることがあるので。
    例→ ~/git/tmp/rich_menu_green_2.png 横幅2500px,縦幅843pxの画像です。
  9. rich_menu_green_2.pngを配置しているディレクトリにcdコマンドで移動します。
    cd ~/git/tmp
  10. 背景画像をLINEプラットフォームのリッチメニューに送信します。
    下記コマンドのリッチメニューIDとチャンネルアクセストークンを書き換えてから実行すると送信されます。
    
    $ curl -v -X POST https://api-data.line.me/v2/bot/richmenu/richmenu-xxxリッチメニューIDxxxxb217e75d78d5fba09b254/content \
     -H "Authorization: Bearer lPdXJ1j4doZCNpA8xxxxxxxxxxxxxxxチャンネルアクセストークンxxxxxxxxxxxxijoqzvjjocigrg7oPCwCmUZXpXSXpXvi2GqIlV5QBSagUHTzrmKLSAdB04t89/1O/w1cDnyilFU=" \
     -H "Content-Type: image/png" \
     -T rich_menu_green_2.png
    
    
    特に、エラーがでなければ成功です。
  11. 上記のリッチメニューをデフォルトにします。つまり、一般ユーザーのスマホのトーク画面にリッチメニューを表示するよう設定します。。
    
    $ curl -v -X POST https://api.line.me/v2/bot/user/all/richmenu/richmenu-xxxリッチメニューIDxxxxb217e75d78d5fba09b254 \
     -H "Authorization: Bearer lPdXJ1j4doZCNpA8xxxxxxxxxxxxxxxチャンネルアクセストークンxxxxxxxxxxxxijoqzvjjocigrg7oPCwCmUZXpXSXpXvi2GqIlV5QBSagUHTzrmKLSAdB04t89/1O/w1cDnyilFU="
    			
  12. スマホのLINEアプリを開き、検証用のLINE公式アカウントととのトーク画面を開いてください。
    上記で登録したリッチメニューが表示されるはずです。
    ちなみに現時点(2023年11月)ではリッチメニューの挙動をスマホで確認するしかないようです。 LINE公式アカウントの設定画面には、CURLで登録したリッチメニューは表示されません。

LINEリッチメニューをPHPコードでCURL配信設定

LINEリッチメニューをPHPコードでCURL配信設定するクラスであるRichMenuCurl.phpを自作しました。

RichMenuCurl.phpを利用して作成したツール

RichMenuCurl.php


<?php 


class RichMenuCurl{
	
	/**
	 * LINEリッチメニューのテンプレートをLINEに送信し、LINEリッチメニューIDを取得する
	 * 
	 * @param array $param
	 */
	public function curlTemplateToLine($param = []){
		
		$access_token = $param['access_token'];

		// LINE APIのURL
		$url = 'https://api.line.me/v2/bot/richmenu';

		// リクエストヘッダー
		$headers = [
				"Authorization: Bearer {$access_token}",
				'Content-Type: application/json',
		];
		
		// 送信するデータ
		$data = json_encode([
						"size" => [
										"width" => 2500,
										"height" => 843
								],
						"selected" => false,
								"name" => "Animal Rich",
								"chatBarText" => "Tap to open",
								"areas" => [
												[
																"bounds" => [
																				"x" => 0,
																		"y" => 0,
																		"width" => 1250,
																		"height" => 843
																],
												"action" => [
																"type" => "uri",
														"label" => "my_home",
														"uri" => "https://amaraimusi.sakura.ne.jp/"
												]
								],
												[
																"bounds" => [
																				"x" => 1250,
																				"y" => 0,
																				"width" => 1250,
																				"height" => 843
																		],
																"action" => [
																				"type" => "uri",
																				"label" => "Jisin",
																				"uri" => "https://www.jma.go.jp/bosai/map.html#5/32.12/137.856/&contents=hypo"
																		]
																]
												]
										]);
						
		// cURLセッションの初期化
		$ch = curl_init($url);

		// オプションの設定
		curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
		curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
		curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

		// リクエストの実行
		$response = curl_exec($ch);

		// エラーチェック
		if (curl_errno($ch)) {
			echo 'Error:' . curl_error($ch);
		}
	
		// cURLセッションの終了
		curl_close($ch);

		// レスポンスの表示
		$res=json_decode($response,true);//JSON文字を配列に戻す
		$richMenuId = $res['richMenuId'];
		
		return $richMenuId;
		
		
	}
	
	
	/**
	 * LINEリッチメニューの画像をLINEに送信する
	 *
	 * @param array $param
	 */
	public function curlImgToLine($params){
		$access_token = $params['access_token'];
		$line_rich_menu_id = $params['line_rich_menu_id'];
		$img_path = $params['img_path'];
		
		// ファイルの拡張子を取得し、拡張子を小文字に変換
		$ext = pathinfo($img_path, PATHINFO_EXTENSION);
		$ext = strtolower($ext);
		
		// MIMEタイプの確認(サポートされている形式のみ)
		if ($ext != "jpg" && $ext != "jpeg" && $ext != "png") {
			echo "Unsupported file format";
			return;
		}
		
		// 正しいMIMEタイプを設定
		$mime = ($ext == "png") ? "image/png" : "image/jpeg";
		
		// LINE APIのエンドポイント
		$url = "https://api-data.line.me/v2/bot/richmenu/{$line_rich_menu_id}/content";
		
		// ヘッダー
		$headers = [
				"Authorization: Bearer {$access_token}",
				"Content-Type: {$mime}"
				];
		
		// cURLセッションの初期化
		$ch = curl_init($url);
		
		// ファイルの内容を読み込む
		$data = file_get_contents($img_path);
		if ($data === false) {
			echo "Failed to read file";
			return;
		}
		
		// オプションの設定
		curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
		curl_setopt($ch, CURLOPT_POST, true);
		curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
		
		// リクエストの実行
		$response = curl_exec($ch);
		
		// エラーチェック
		if (curl_errno($ch)) {
			echo 'Error:' . curl_error($ch);
		}
		
		// cURLセッションの終了
		curl_close($ch);
		
		return $response;
		
	}
	
	
	/**
	 * LINEリッチメニューをデフォルトに設定するようLINEに送信する(リッチメニューを適用)
	 *
	 * @param array $param
	 */
	public function curlDefaultToLine($params){
		
		$access_token = $params['access_token'];
		$line_rich_menu_id = $params['line_rich_menu_id'];

		
		// LINE APIのエンドポイント
		$url = "https://api.line.me/v2/bot/user/all/richmenu/{$line_rich_menu_id}";
		
		// ヘッダー
		$headers = [
				"Authorization: Bearer {$access_token}"
		];
		
		// cURLセッションの初期化
		$ch = curl_init($url);
		
		// オプションの設定
		curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
		curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
		
		// リクエストの実行
		$response = curl_exec($ch);
		
		// エラーチェック
		if (curl_errno($ch)) {
			echo 'Error:' . curl_error($ch);
		}
		
		// cURLセッションの終了
		curl_close($ch);
		
		// レスポンスの表示
		return $response;
		
	}
	
	/**
	 * LINEプラットフォームに登録されているリッチメニュー一覧の情報を取得する
	 *
	 * @param array $param
	 */
	public function curlListFromLine($params){
		
		$access_token = $params['access_token'];
		
		// 初期化
		$curl = curl_init();
		
		// cURLオプションの設定
		curl_setopt($curl, CURLOPT_URL, "https://api.line.me/v2/bot/richmenu/list");
		curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($curl, CURLOPT_HTTPHEADER, array(
				"Authorization: Bearer {$access_token}"
		));
		
		// HTTPリクエストの実行
		$response = curl_exec($curl);
		
		// エラーチェック
		if(curl_errno($curl)){
			echo 'Curl error: ' . curl_error($curl);
		}
		
		// cURLセッションの終了
		curl_close($curl);

		$res = json_decode($response, true);

		return $res;
		
	}
	
	
	/**
	 * LINEリッチメニューを削除するようLINEに送信する(リッチメニューIDを指定して削除)
	 *
	 * @param array $param
	 */
	public function curlDeleteToLine($params){
		
		$access_token = $params['access_token'];
		$line_rich_menu_id = $params['line_rich_menu_id'];
		
		// cURLセッションを初期化
		$curl = curl_init();
		
		// cURLオプションの設定
		curl_setopt($curl, CURLOPT_URL, "https://api.line.me/v2/bot/richmenu/{$line_rich_menu_id}");
		curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "DELETE");
		curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($curl, CURLOPT_HTTPHEADER, array(
				"Authorization: Bearer {$access_token}"
		));
		
		// HTTPリクエストを実行
		$response = curl_exec($curl);
		
		// エラーチェック
		if (curl_errno($curl)) {
			echo 'Curl error: ' . curl_error($curl);
		}
		
		// cURLセッションを閉じる
		curl_close($curl);
		
		$res = json_decode($response, true);
		
		return $res;
		
		
	}
	
	
}
	

クライアント


<?php 

// 通信元から送信されてきたパラメータを取得する。
$params_json = $_POST['key1'];
$params=json_decode($params_json,true);//JSON文字を配列に戻す

$mode = $params['mode'];

require_once 'RichMenuCurl.php';
$richMenuCurl = new RichMenuCurl();
switch ($mode) {
	case 'template_to_line':
		$line_rich_menu_id = $richMenuCurl->curlTemplateToLine($params);
		$params['line_rich_menu_id'] = $line_rich_menu_id;
		break;
		
	case 'img_to_line':
		$params['img_path'] = __DIR__ . '/img/' . $params['rich_menu_img'];
		$params['res'] = $richMenuCurl->curlImgToLine($params);
		break;
		
	case 'default_to_line':
		$params['res'] = $richMenuCurl->curlDefaultToLine($params);
		break;
		
	case 'list_from_line':
		$params['res'] = $richMenuCurl->curlListFromLine($params);
		break;
		
	case 'delete_to_line':
		$params['res'] = $richMenuCurl->curlDeleteToLine($params);
		break;
}

// JSONに変換し、通信元に返す。
$json_str = json_encode($params, JSON_HEX_TAG | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_HEX_APOS);
echo $json_str;
	

ディレクトリ削除 | 内部のファイルやフォルダごと削除

    /**
     * 指定したディレクトリを再帰的に削除するメソッド
     * 
     * @param string $dir 削除対象のディレクトリのパス
     * @throws InvalidArgumentException
     */
    public function rmdirEx($dir)
    {
        if (!is_dir($dir)) {
            return;
        }

        $this->deleteDirectoryContents($dir);

        // 最後にディレクトリ自体を削除
        rmdir($dir);
    }

    /**
     * ディレクトリの中身を再帰的に削除する
     * 
     * @param string $dir
     */
    private function deleteDirectoryContents($dir)
    {
        $items = scandir($dir);

        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }

            $path = $dir . DIRECTORY_SEPARATOR . $item;

            if (is_dir($path)) {
                // サブディレクトリの場合、再帰的に削除
                $this->rmdirEx($path);
            } else {
                // パーミッションを変更してファイルを削除
                if (!is_writable($path)) {
                    chmod($path, 0666);
                }
                unlink($path);
            }
        }
    }
	

ディレクトリコピー | 内部のファイルやフォルダごとコピー


	 /**
     * 再帰的にディレクトリをコピーするメソッド
     *
     * @param string $sourceDir コピー元のディレクトリパス
     * @param string $destDir コピー先のディレクトリパス
     * @return bool コピーが成功した場合は true、失敗した場合は false
     */
    public function copyDirEx(string $sourceDir, string $destDir): bool
    {
        // コピー元のディレクトリが存在しない場合、falseを返す
        if (!is_dir($sourceDir)) {
            return false;
        }

        // コピー先のディレクトリが存在しない場合、作成する
        if (!is_dir($destDir)) {
            mkdir($destDir, 0755, true);
        }

        // ディレクトリハンドルを開く
        $dirHandle = opendir($sourceDir);
        if ($dirHandle === false) {
            return false;
        }

        // ディレクトリ内のファイルやフォルダをループ
        while (($file = readdir($dirHandle)) !== false) {
            if ($file === '.' || $file === '..') {
                continue;
            }

            $sourcePath = $sourceDir . DIRECTORY_SEPARATOR . $file;
            $destPath = $destDir . DIRECTORY_SEPARATOR . $file;

            if (is_dir($sourcePath)) {
                // 再帰的にディレクトリをコピー
                if (!$this->copyDirEx($sourcePath, $destPath)) {
                    closedir($dirHandle);
                    return false;
                }
            } else {
                // ファイルをコピー
                if (!copy($sourcePath, $destPath)) {
                    closedir($dirHandle);
                    return false;
                }
            }
        }

        // ディレクトリハンドルを閉じる
        closedir($dirHandle);
        return true;
    }