|
<?php |
|
|
|
namespace App\Commons; |
|
|
|
use Illuminate\Support\Facades\Storage; |
|
|
|
class CsvFileEditor |
|
{ |
|
/** @var string csv檔案路徑 */ |
|
public $filepath; |
|
|
|
function __construct($filepath, $disk = null) |
|
{ |
|
// 確定 $filepath 是否為完整路徑 |
|
if (!realpath($filepath)) |
|
$filepath = Storage::disk($disk)->path('csv_file_editor/' . ltrim($filepath, '/')); |
|
$this->filepath = $filepath; |
|
} |
|
|
|
/** |
|
* 將陣列轉為 csv 字串 |
|
* |
|
* @param mixed $data |
|
* @return string |
|
*/ |
|
private function convertArrayToCsvString(array $data) |
|
{ |
|
$fp = fopen('php://temp', 'r+'); |
|
fputcsv($fp, array_values($data)); |
|
|
|
rewind($fp); |
|
$csvData = stream_get_contents($fp); |
|
fclose($fp); |
|
|
|
return $csvData; |
|
} |
|
|
|
/** |
|
* 將 csv 字串轉為陣列 |
|
* |
|
* @param mixed $csvString |
|
* @return array |
|
*/ |
|
private function convertCsvStringToArray($csvString) |
|
{ |
|
// 將字串轉換為類似文件的資源 |
|
$handle = fopen('php://memory', 'r+'); |
|
fwrite($handle, $csvString); |
|
// 將文件指標指回到文件開頭 |
|
rewind($handle); |
|
// 取得第一行 csv |
|
$csvData = fgetcsv($handle); |
|
fclose($handle); |
|
return $csvData; |
|
} |
|
|
|
/** |
|
* combineArraysWithPadding |
|
* |
|
* @param mixed $keys |
|
* @param mixed $values |
|
* @return array |
|
*/ |
|
private function combineArrayWithPaddingOrTrimming($keys, $values) |
|
{ |
|
$numKeys = count($keys); |
|
$numValues = count($values); |
|
|
|
// 如果 keys 的數量少於 values,則補足 |
|
if ($numKeys < $numValues) { |
|
// 使用一個範圍來生成補足的鍵名 |
|
$keys = array_merge($keys, range($numKeys, $numValues - 1)); |
|
} |
|
// 只保留前面的鍵名 |
|
$keys = array_slice($keys, 0, $numValues); |
|
|
|
// 使用 array_combine 來合併 |
|
return array_combine($keys, $values); |
|
} |
|
|
|
/** |
|
* 檔案是否存在 |
|
* |
|
* @return string|bool |
|
*/ |
|
public function exists() |
|
{ |
|
return realpath($this->filepath) ? $this->filepath : false; |
|
} |
|
|
|
/** |
|
* 檔案路徑 |
|
* |
|
* @return string |
|
*/ |
|
public function filePath() |
|
{ |
|
return $this->filepath; |
|
} |
|
|
|
/** |
|
* 取得指定行數的資料 |
|
* |
|
* @param mixed $line 指定行數,預設第1行 |
|
* @param mixed $withColumnName 是否加入欄位名稱(第一行) |
|
* @return array |
|
*/ |
|
public function fetchAtLine($line = 1, $withColumnName = false) |
|
{ |
|
if($filePath = $this->exists()) { |
|
if ($handle = fopen($filePath, 'r')) { |
|
// 標題欄 |
|
$header = []; |
|
$currentLine = 0; |
|
while (($data = fgetcsv($handle)) !== false) { |
|
if (empty($header)) |
|
$header = $data; |
|
if (++$currentLine === $line) { |
|
fclose($handle); |
|
if ($withColumnName) |
|
return $this->combineArrayWithPaddingOrTrimming($header, $data); |
|
return $data; |
|
} |
|
} |
|
fclose($handle); |
|
} |
|
} |
|
return []; |
|
} |
|
|
|
/** |
|
* 找到第一個 $prefix 文字開頭的行內容 |
|
* |
|
* @param mixed $prefix |
|
* @param mixed $findAtLine 在第 n 行找到的資料,找不到資料時值為最後一行 |
|
* @param mixed $withColumnName 是否加入欄位名稱(第一行) |
|
* @return array |
|
*/ |
|
public function findPrefixFirst ($prefix, &$findAtLine = 0, $withColumnName = false) |
|
{ |
|
if($filePath = $this->exists()) { |
|
if ($handle = fopen($filePath, 'r')) { |
|
// 標題欄 |
|
$header = []; |
|
// 逐行讀取 |
|
while (($line = fgets($handle)) !== false) { |
|
if (empty($header)) |
|
$header = $this->convertCsvStringToArray($line); |
|
++$findAtLine; |
|
// 檢查該行是否以指定字串開頭 |
|
if (strpos($line, $prefix) === 0) { |
|
fclose($handle); |
|
if ($withColumnName) |
|
return $this->combineArrayWithPaddingOrTrimming($header, $this->convertCsvStringToArray($line)); |
|
return $this->convertCsvStringToArray($line); |
|
} |
|
} |
|
fclose($handle); |
|
} |
|
} |
|
return []; |
|
} |
|
|
|
/** |
|
* 將 $data 寫入最後一行 |
|
* |
|
* @param mixed $data |
|
* @return self |
|
*/ |
|
public function appendData(array $data) |
|
{ |
|
$dirPath = dirname($this->filepath); |
|
if (!is_dir($dirPath)) { |
|
mkdir($dirPath, 0755, true); // 創建目錄及其父目錄 |
|
} |
|
|
|
$csvData = $this->convertArrayToCsvString($data); |
|
file_put_contents($this->filepath, $csvData, FILE_APPEND | LOCK_EX); |
|
return $this; |
|
} |
|
|
|
/** |
|
* 更新指定行數的資料 |
|
* |
|
* @param mixed $data |
|
* @param mixed $line |
|
* @return void |
|
*/ |
|
public function updateAtLine($data, $line = 1) |
|
{ |
|
if($filePath = $this->exists()) { |
|
// 檢查是否存在 sed 命令 |
|
$hasSed = shell_exec('command -v sed'); |
|
if ($hasSed) { |
|
// 如果有 sed,使用 sed -i 來修改第一行 |
|
$escapedLine = escapeshellarg($this->convertArrayToCsvString($data)); // 避免命令注入風險 |
|
exec("sed -i $line . 's/.*/$escapedLine/' $filePath"); |
|
} |
|
// 否則使用 PHP 方法來修改第一行 |
|
else { |
|
// 打開文件,讀取內容 |
|
$fileContents = file($filePath); |
|
// 行數存在才更新 |
|
if (!empty($fileContents) && isset($fileContents[$line - 1])) { |
|
$fileContents[$line - 1] = $this->convertArrayToCsvString($data); |
|
file_put_contents($filePath, implode("", $fileContents), LOCK_EX); |
|
} |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* 將檔案輸出陣列 |
|
* |
|
* @return array |
|
*/ |
|
public function toArray() |
|
{ |
|
if($filePath = $this->exists()) { |
|
if ($fileContents = file($filePath)) { |
|
// 將字串轉換為類似文件的資源 |
|
$handle = fopen('php://temp', 'r+'); |
|
foreach($fileContents as $csvString) { |
|
fwrite($handle, $csvString); |
|
} |
|
// 將文件指標指回到文件開頭 |
|
rewind($handle); |
|
$array = []; |
|
while (($data = fgetcsv($handle)) !== false) { |
|
array_push($array, $data); |
|
} |
|
fclose($handle); |
|
return $array; |
|
} |
|
} |
|
return []; |
|
} |
|
} |