将整数转换为 Excel 列(字母或字母组合)-完整示例 PHP、单元测试和解释
Convert integer to Excel column (letter or letter combination) - complete example PHP, unit test and explanation
假设我们有一个像这样的函数,它接受一个 PDOStatement(任何查询)并自动生成一个 excel 文件(使用 PHPExcel 库):
/**
* Create an Excel file from an opened PDOStatement. Return the path to access the Excel file
* or an empty string if we were not able to create the Excel File
*
*
* @param $errorMessage String to return the error message.
* @param $source PDOStatement containing the data to export to Excel.
* @param $rows Int Use to return the number of rows exported (the header row isn't counted).
* @param $name String name to give to the Excel file (no need to specify the extension).
* @param $columnName (optional) String Array used for the name of the row in the Excel file.
*
* @return String
*/
public static function createExcelFromRS(&$errorMessage, PDOStatement &$source, &$rows , $name, array $columnName = array()){
$errorMessage = "";
$name = self::validateFileExtention($name, "xlsx");
$path = realpath(dirname(__FILE__)) . '/Archive/' . $name;
$rows = 0;
$totalCols = 0;
$excel = new PHPExcel();
$writer = PHPExcel_IOFactory::createWriter($excel, "Excel2007");
$sheet = $excel->getActiveSheet();
$sheet->setTitle($name);
while ($row = $source->fetch(PDO::FETCH_ASSOC)){
if ($rows === 0){
$columnName = self::validateColumnNameArray($columnName, $row);
$totalCols = count($row);
$sheet->getStyle('A1:' . self::convertNumberToExcelCol($totalCols) . '1')->getFont()->setBold(true)->setSize(12);
for ($column = 1; $column <= $totalCols; $column++){
$sheet->getCell(self::convertNumberToExcelCol($column) . '1')->setValue($columnName[$column - 1]);
$sheet->getColumnDimension(self::convertNumberToExcelCol($column))->setAutoSize(true);
}
$rows = 1;
}
$rows++;
$column = 1;
foreach ($row as $field){
$sheet->getCell(self::convertNumberToExcelCol($column) . $rows)->setValue($field);
$column++;
}
}
$writer->save($path);
unset($sheet, $writer, $excel);
if ($rows < 1){
if (is_file($path)){
unlink($path);
}
$errorMessage =str_replace("[TYPE]", "EXCEL", GeneralDbManager::getInstance()->getErrorMessage('NO_DATA_TO_EXPORT_FILE_ERR', 'There is no data to export to the [TYPE] file.'));
}
elseif(!is_file($path)){
$errorMessage = str_replace(array("[TYPE]", "[NAME]"), array("EXCEL", $name), GeneralDbManager::getInstance()->getErrorMessage('EXPORT_NO_CREATED_FILE_ERR', 'We were not able to create the [TYPE] file: [NAME].'));
}
else{
$rows --;
}
return (empty($errorMessage) ? $path : "");
}
并且我们想使用 convertNumberToExcelCol
函数将整数值转换为 excel 列。
我们先讲解一下这种构建excel文件的方法,下一个post会讲解获取列的算法。
方法作为参数:
- ErrorMessage:用于return错误信息
- source:包含要推送到 excel 文件的数据
- Rows:用于return导出的数据行数
- name: excel 文件的名称
- columnName:一个可选的数组,与人类可读的列名(或翻译)一起使用。如果省略,该方法使用查询中的字段名称。
第一行用于初始化参数(PHP使用松散类型,所以我们必须小心参数)。
此函数确保名称具有有效 name/extension:
/**
* Validate that the file $name has the proper file $extension
* and return the fixed name with the proper extension
*
* Note: No modification will be made if the extension is not a string or is empty
*
* @param $name String file name with or without extension
* @param $extension String example: csv, xls
*
* @return String
*/
public static function validateFileExtention($name, $extension){
if (is_string($extension)){
$extension = "." . str_replace(".", "", $extension);
if (strlen($extension) > 1){
if (!is_string($name) or empty($name) or strpos($name, ".") === 0){
$name = "my_file" . $extension;
}
elseif(strpos(strtolower($name), $extension) === false){
if (strrpos($name, ".") === false){
$name .= $extension;
}
else{
if (substr_count($name, ".") > 1){
$name = str_replace(".", "", $name) . $extension;
}
else{
$name = str_replace(substr($name, strrpos($name, ".")), $extension, $name);
}
}
}
}
}
return $name;
}
然后我们打开与excel文件的连接:
$excel = new PHPExcel();
$writer = PHPExcel_IOFactory::createWriter($excel, "Excel2007");
$sheet = $excel->getActiveSheet();
$sheet->setTitle($name);
此函数确保列名数组的长度与行数组中的字段数相同。
/**
* Take the array containing the $columnName for data export (CSV, Excel) and make sure
* that it is the number of entry as there are fields in $row.
*
* If column name are missing, we will use the column name used in the query.
*
* Return the merged array
*
* @param $columnName Array containing the column names
* @param $row Array produce by fetch(PDO::FETCH_ASSOC).
*
* @return Array ($columnName)
*/
private static function validateColumnNameArray(array &$columnName, array &$row){
$buffer = array();
$colPDO = count($row);
$count = count($columnName);
if ($count < $colPDO){
foreach ($row as $key => $value){
$buffer[] = $key;
}
for($index = $count; $index < $colPDO; $index++){
$columnName[] = $buffer[$index];
}
}
unset($buffer);
return $columnName;
}
validateFileExtention
和 validateColumnNameArray
都是针对具有 CSV 创建功能的共享代码:
/**
* Create a CSV file from an opened PDOStatement. Return the path to access the CSV file
* or an empty string if we were not able to create the CSV File
*
*
* @param $errorMessage String to return the error message.
* @param $source PDOStatement containing the data to export to CSV
* @param $rows Int Use to return the number of rows exported (the header row isn't counted).
* @param $name String name to give to the CSV file (no need to specify the extension).
* @param $columnName (optional) String Array used for the name of the row in the CSV file.
*
* @return String
*/
public static function createCSVFromRS(&$errorMessage, PDOStatement &$source, &$rows , $name, array $columnName = array()){
$errorMessage = "";
$name = self::validateFileExtention($name, "csv");
$path = realpath(dirname(__FILE__)) . '/Archive/' . $name;
$rows = 0;
$file = fopen($path, "w");
while ($row = $source->fetch(PDO::FETCH_ASSOC)){
if ($rows === 0){
fputcsv($file, array_map('utf8_decode',self::validateColumnNameArray($columnName, $row)));
}
fputcsv($file, array_map('utf8_decode',array_values($row)));
$rows++;
}
fclose($file);
if ($rows < 1){
if (is_file($path)){
unlink($path);
}
$errorMessage =str_replace("[TYPE]", "CSV", GeneralDbManager::getInstance()->getErrorMessage('NO_DATA_TO_EXPORT_FILE_ERR', 'There is no data to export to the [TYPE] file.'));
}
elseif(!is_file($path)){
$errorMessage = str_replace(array("[TYPE]", "[NAME]"), array("CSV", $name), GeneralDbManager::getInstance()->getErrorMessage('EXPORT_NO_CREATED_FILE_ERR', 'We were not able to create the [TYPE] file: [NAME].'));
}
return (empty($errorMessage) ? $path : "");
}
如果这是我们添加到 excel 文件的第一行,那么我们设置基本格式:
if ($rows === 0){
$columnName = self::validateColumnNameArray($columnName, $row);
$totalCols = count($row);
$sheet->getStyle('A1:' . self::convertNumberToExcelCol($totalCols) . '1')->getFont()->setBold(true)->setSize(12);
for ($column = 1; $column <= $totalCols; $column++){
$sheet->getCell(self::convertNumberToExcelCol($column) . '1')->setValue($columnName[$column - 1]);
$sheet->getColumnDimension(self::convertNumberToExcelCol($column))->setAutoSize(true);
}
$rows = 1;
}
我们使用 getStyle 方法将 header 行设置为粗体和 12 大小。
getCOlumnDimension 方法用于设置自动调整大小,因此用户在打开文件时不必调整列 himself/herself 的大小。
循环的其余部分是将数据从行数组传输到 excel 文件。
循环后,我们关闭连接并取消设置用于管理的变量 Excel。
然后是错误管理:
if ($rows < 1){
if (is_file($path)){
unlink($path);
}
$errorMessage =str_replace("[TYPE]", "EXCEL", GeneralDbManager::getInstance()->getErrorMessage('NO_DATA_TO_EXPORT_FILE_ERR', 'There is no data to export to the [TYPE] file.'));
}
elseif(!is_file($path)){
$errorMessage = str_replace(array("[TYPE]", "[NAME]"), array("EXCEL", $name), GeneralDbManager::getInstance()->getErrorMessage('EXPORT_NO_CREATED_FILE_ERR', 'We were not able to create the [TYPE] file: [NAME].'));
}
else{
$rows --;
}
消息存储在数据库中,因此我们可以向用户提供翻译后的消息。我在我的消息中使用了通用的 [TYPE] 和 [NAME] 标签,我将其替换为正确的文件类型和文件名。
这让我可以在我的 excel 和我正在生成的 CSV 文件(或任何类型的文件)中重复使用这条通用消息。
如果创建的文件是空的,我将其删除。此操作是可选的,但我喜欢在完成后立即从磁盘中清除未使用的文件。
另一种方法是使用函数清除存储目录:
/**
* Clear all the archives (zip) files in the archive folder.
*/
public static function emptyArchiveFolder(){
$handle = NULL;
$path = realpath(dirname(__FILE__)) . '/Archive/';
if (is_dir($path) and $handle = opendir($path)) {
while (false !== ($entry = readdir($handle))) {
$file = $path . $entry;
if (is_file($file)){
unlink($file);
}
}
unset($handle);
}
}
我个人只在午夜对源文件和数据库进行自动备份时使用这种方法。 运行 它在白天增加了删除其他用户使用的文件的机会。
这就是为什么我正在考虑最好的做法是在文件通过浏览器发送给用户后立即将其删除,并将清理方法仅用于维护目的。
如果没有错误,我将行数减一,因为我不想计算 header 行。如果您将 header 行视为数据行,则可以删除该行。
最后是方法return访问新建文件的路径:
return (empty($errorMessage) ? $path : "");
但前提是没有错误。因此,如果函数 return 是一个空字符串,则表示发生了错误。
PHP 类型松散,你可以 return 任何东西,包括布尔值甚至错误消息,但我更喜欢 return 始终使用相同的数据类型以保持不变。我个人最喜欢的方法是布尔 return 值和通过引用传递的错误消息变量。所以我可以使用这样的代码:
$errorMessage = "";
if ($_SESSION["adminAccount"]->updateAccountInfo($errorMessage,
(isset($_POST['FIRST_NAME_TEXT']) ? $_POST['FIRST_NAME_TEXT'] : $_SESSION["adminAccount"]->getFirstName()),
(isset($_POST['LAST_NAME_TEXT']) ? $_POST['LAST_NAME_TEXT'] : $_SESSION["adminAccount"]->getLastName()),
(isset($_POST['EMAIL_TEXT']) ? $_POST['EMAIL_TEXT'] : $_SESSION["adminAccount"]->getEmail()))){
PageManager::displaySuccessMessage("Your account information were saved with success.", "USER_ACCOUNT_INFORMATION_SAVED");
}
else{
PageManager::displayErrorMessage($errorMessage);
}
这样,错误由 class 方法在内部管理,并且可以根据视图上下文调整成功消息。布尔值 return 用于确定我们是否必须显示错误或成功消息。
注意:单元测试将包含在我的答案中。
Jonathan Parent-L来自蒙特利尔的évesque
下面是允许将整数转换为 excel 列的方法。
excel 列可以是一到三个字母的组合,最大为 16383 (XFD),这是 excel 文件中列的当前限制:
我在我的函数旁边使用了一个包含所有英文字母的数组:
public static $letters = array(1 => "A", 2 => "B", 3=> "C", 4 => "D", 5 => "E", 6 => "F", 7 => "G", 8=> "H", 9=> "I", 10 => "J", 11 =>"K", 12 => "L", 13 => "M", 14 => "N", 15=> "O", 16 => "P", 17 => "Q", 18 => "R", 19 => "S", 20 => "T", 21 => "U", 22 => "V", 23 => "W", 24 => "X", 25 => "Y", 26 => "Z");
该函数采用单个参数并断言该值为数字并且在 excel 限制之间:
public static function convertNumberToExcelCol($number){
$column = "";
if (is_numeric($number) and $number > 0 and $number < 16385){
如果数字在 1 到 26 之间(A-Z 列),那就小菜一碟了。我们只是直接在数组中获取字母。
$column = self::$letters[$number];
让我们测试一下:
for ($index = 1; $index < 27; $index++){
$this->assertEquals(FileManager::$letters[$index], FileManager::convertNumberToExcelCol($index));
}
如果列介于 27 和 702 (AA-ZZ) 之间,编码仍然非常简单:
if ($number % 26 === 0){
$first = floor($number / 26) - 1;
$second = 26;
}
else{
$first = floor($number / 26);
$second = $number % 26;
}
$column = self::$letters[$first] . self::$letters[$second];
由于英文字母有26个字母,所以整个算法也是以26为底的
对于大多数值,我们可以简单地通过向下舍入 [number]/26 得到第一个字母,使用数字的整个除法(除法的余数)得到第二个字母:[number] % 26.
$first = floor($number / 26);
$second = $number % 26;
这里有一些例子:
- 27 = AA
- 28=AB
- 51 = 年
- 53 = 文学学士
如果[number] % 26 = 0,例如:52、78、104等,那么我们必须使用一些不同的编码。
例如,52 = AZ 但是,52/26=2(第一个字母)和 52%26=0(第二个字母)。
2=B 并且 0 在我们的字母数组中超出范围。这就是为什么,我们必须将第一个字母的值减一,并强制将 26 作为第二个字母的值。
$first = floor($number / 26) - 1;
$second = 26;
测试时间:
for ($first = 1; $first < 27; $first++){
$temp = $first * 26;
for ($second = 1; $second < 27; $second++){
$this->assertEquals(FileManager::$letters[$first] . FileManager::$letters[$second], FileManager::convertNumberToExcelCol($temp + $second));
}
}
当我们试图管理三个字母时,真正的挑战就来了。当然,你可能不会有超过 702 个字段的查询,但是为了方法的完整性和一些真正的编程挑战,让我们来看看如何做到这一点!
起初,我不得不尝试错误测试并最终得到以下编码:
elseif($number < 1379){
$column = self::$letters[floor($number / 702)] . self::convertNumberToExcelCol($number % 702 + 26);
}
elseif($number < 2028){
$column = self::$letters[floor($number / 676)] . self::convertNumberToExcelCol($number % 676);
}
elseif ($number < 2055){
$column = self::$letters[floor($number / 702)] . self::convertNumberToExcelCol($number % 702 + 52);
}
elseif($number < 2704){
$column = self::$letters[floor($number / 676)] . self::convertNumberToExcelCol($number % 676);
}
elseif ($number < 2731) {
$column = self::$letters[floor($number / 702)] . self::convertNumberToExcelCol($number % 702 + 78);
}
elseif ($number < 3380) {
$column = self::$letters[floor($number / 676)] . self::convertNumberToExcelCol($number % 676);
}
elseif ($number < 3407){
$column = self::$letters[floor($number / 702)] . self::convertNumberToExcelCol($number % 702 + 104);
}
是的,这并不严重,你可以像这样一路上升到 16k...
如果你仔细看,你会发现有一个图案就是从这里画出来的。有些数字可以除以 676 得到第一个字母,模 676 得到第二个和第三个字母。例如,2027 = BBY.
第二个模式是可以将第一个字母除以 702 + 补偿的模数(26、52、78、104,...)的数字。这包括像 703=AAA.
这样的数字
当然676和702都是26的倍数
我花了很多计算,但我开始意识到第二个模式总是 27 个数字的范围,并且这些数字总是产生一个低于 27 (0-26) 的数字作为这个模数数.
elseif($number < 2028){
$column = self::$letters[floor($number / 676)] . self::convertNumberToExcelCol($number % 676);
}
elseif ($number < 2055){
例如:
- 2028 % 676 = 0
- 2029 % 676 = 1
- ...
- 2054 % 676 = 26
- 2055 % 676 = 27(现在超出范围)
既然找到了使用702算法的人数范围,就要确定如何计算补偿了。薪酬始终基于 26,因此可能与此相关...
对于范围2028-2054,补偿为52。
- 2028 / 26 = 78
- 2029 / 26 = 78.03846
- ...
- 2054 / 26 = 79
对于范围2704-2730,补偿为78。
- 2704 / 26 = 104
- 2730 / 26 = 105
如你所见,规则是[补偿] = [数量] / 26 - 26(上限减1)。
所以三个字母的编码(以前是无穷无尽的if/else)可以分5行恢复:
if($number % 676 < 27){
$compensation = floor($number / 26) - 26;
$column = self::$letters[floor($number / 702)] . self::convertNumberToExcelCol($number % 702 + ($compensation % 26 === 0 ? $compensation : $compensation - 1));
}
else{
$column = self::$letters[floor($number / 676)] . self::convertNumberToExcelCol($number % 676);
}
函数 returns 如果参数值无效,则为字母组合或空字符串。
让我们测试一下,以确保我没有骗你:
for ($first = 1; $first < 27; $first++){
for ($second = 1; $second < 27; $second++){
for ($third = 1; $third < 27; $third++){
$temp = $first * 676 + (($second * 26) + $third);
if ($temp < 16385){
$this->assertEquals(FileManager::$letters[$first] . FileManager::$letters[$second] . FileManager::$letters[$third], FileManager::convertNumberToExcelCol($temp));
}
else{
$this->assertEmpty(FileManager::convertNumberToExcelCol($temp + $index));
}
}
}
}
完整函数如下:
/**
* Convert a $number to the letter (or combination of letters) representing a column in excel.
* Will return an empty string if $number is not a valid value.
*
* @param number Int must be is_numeric() and > 0 and < 16,385.
*
* @return String
*/
public static function convertNumberToExcelCol($number){
$column = "";
if (is_numeric($number) and $number > 0 and $number < 16385){
if ($number < 27){
$column = self::$letters[$number];
}
elseif ($number < 703){
if ($number % 26 === 0){
$first = floor($number / 26) - 1;
$second = 26;
}
else{
$first = floor($number / 26);
$second = $number % 26;
}
$column = self::$letters[$first] . self::$letters[$second];
}
else{
if($number % 676 < 27){
$compensation = floor($number / 26) - 26;
$column = self::$letters[floor($number / 702)] . self::convertNumberToExcelCol($number % 702 + ($compensation % 26 === 0 ? $compensation : $compensation - 1));
}
else{
$column = self::$letters[floor($number / 676)] . self::convertNumberToExcelCol($number % 676);
}
}
}
return $column;
}
当然,正如所承诺的,这里是 excel/csv 文件创建的单元测试代码。
public function testvalidateFileExtention(){
//invalid extension
$this->assertEquals("woot", FileManager::validateFileExtention("woot", true));
$this->assertEquals("woot", FileManager::validateFileExtention("woot", ""));
$this->assertEquals("woot", FileManager::validateFileExtention("woot", "."));
$this->assertEquals("woot.blu", FileManager::validateFileExtention("woot", ".b.l.u.."));
//invalid name
$this->assertEquals("my_file.blu", FileManager::validateFileExtention(true, ".blu"));
$this->assertEquals("my_file.blu", FileManager::validateFileExtention("", ".blu"));
$this->assertEquals("my_file.blu", FileManager::validateFileExtention(".woot", ".blu"));
$this->assertEquals("woot.blu", FileManager::validateFileExtention("w.o.o.t.", ".blu"));
//valid file name and extension
$this->assertEquals("woot.blu", FileManager::validateFileExtention("woot", "blu"));
$this->assertEquals("woot.blu", FileManager::validateFileExtention("woot", ".blu"));
}
public function testCreateCSVFromRS(){
FileManager::emptyArchiveFolder();
$errorMessage = "";
$path = realpath(dirname(__FILE__)) . '/../Archive/woot.csv';
$rows = 0;
//no data to export
$rs = $this->fetchData("SELECT id, field_id, field_value, language FROM error_message_table WHERE field_id='woot for loots'");
$this->assertInstanceOf('PDOStatement', $rs);
$this->assertEmpty(FileManager::createCSVFromRS($errorMessage, $rs, $rows, "woot"));
$this->assertNotEmpty($errorMessage);
$this->assertEquals(0, $rows);
$this->assertFileNotExists($path);
//data, but missing columns in the header array
$rs = $this->fetchData("SELECT id, field_id, field_value, language FROM error_message_table LIMIT 100");
$this->assertNotEmpty(FileManager::createCSVFromRS($errorMessage, $rs, $rows, "woot", array("homer", "simpson")));
$this->assertInstanceOf('PDOStatement', $rs);
$this->assertEmpty($errorMessage);
$this->assertEquals(100, $rows);
$this->assertFileExists($path);
$handle = fopen($path, "r");
$this->assertNotEquals(false, $handle);
$row = fgetcsv($handle);
$this->assertContains("homer", $row);
$this->assertNotContains("id", $row);
$this->assertContains("simpson", $row);
$this->assertNotContains("field_id", $row);
$this->assertContains("field_value", $row);
$this->assertContains("language", $row);
fclose($handle);
//data, changing all columns in the header array
$rs = $this->fetchData("SELECT id, field_id, field_value, language FROM error_message_table LIMIT 10");
$this->assertNotEmpty(FileManager::createCSVFromRS($errorMessage, $rs, $rows, "woot", array("kyle", "eric", "kenny", "stan")));
$this->assertInstanceOf('PDOStatement', $rs);
$this->assertEmpty($errorMessage);
$this->assertEquals(10, $rows);
$this->assertFileExists($path);
$handle = fopen($path, "r");
$this->assertNotEquals(false, $handle);
$row = fgetcsv($handle);
$this->assertContains("kyle", $row);
$this->assertNotContains("id", $row);
$this->assertContains("eric", $row);
$this->assertNotContains("field_id", $row);
$this->assertContains("kenny", $row);
$this->assertNotContains("field_value", $row);
$this->assertContains("stan", $row);
$this->assertNotContains("language", $row);
fclose($handle);
unlink($path);
}
public function testConvertNumberToExcelCol(){
//invalid paramter
$this->assertEmpty(FileManager::convertNumberToExcelCol("a"));
$this->assertEmpty(FileManager::convertNumberToExcelCol(array()));
$this->assertEmpty(FileManager::convertNumberToExcelCol(-1));
$this->assertEmpty(FileManager::convertNumberToExcelCol(1000000000));
//single letter
for ($index = 1; $index < 27; $index++){
$this->assertEquals(FileManager::$letters[$index], FileManager::convertNumberToExcelCol($index));
}
//double letters
for ($first = 1; $first < 27; $first++){
$temp = $first * 26;
for ($second = 1; $second < 27; $second++){
$this->assertEquals(FileManager::$letters[$first] . FileManager::$letters[$second], FileManager::convertNumberToExcelCol($temp + $second));
}
}
//tripple letters
for ($first = 1; $first < 27; $first++){
for ($second = 1; $second < 27; $second++){
for ($third = 1; $third < 27; $third++){
$temp = $first * 676 + (($second * 26) + $third);
if ($temp < 16385){
$this->assertEquals(FileManager::$letters[$first] . FileManager::$letters[$second] . FileManager::$letters[$third], FileManager::convertNumberToExcelCol($temp));
}
else{
$this->assertEmpty(FileManager::convertNumberToExcelCol($temp + $index));
}
}
}
}
}
public function testCreateExcelFromRS(){
$errorMessage = "";
$path = realpath(dirname(__FILE__)) . '/../Archive/woot.xlsx';
$rows = 0;
//no data to export
$rs = $this->fetchData("SELECT id, field_id, field_value, language FROM error_message_table WHERE field_id='woot for loots'");
$this->assertInstanceOf('PDOStatement', $rs);
$this->assertEmpty(FileManager::createExcelFromRS($errorMessage, $rs, $rows, "woot"));
$this->assertNotEmpty($errorMessage);
$this->assertEquals(0, $rows);
$this->assertFileNotExists($path);
//data, but missing columns in the header array
$rs = $this->fetchData("SELECT id, field_id, field_value, language FROM error_message_table LIMIT 100");
$this->assertNotEmpty(FileManager::createExcelFromRS($errorMessage, $rs, $rows, "woot", array("homer", "simpson")));
$this->assertInstanceOf('PDOStatement', $rs);
$this->assertEmpty($errorMessage);
$this->assertEquals(100, $rows);
$this->assertFileExists($path);
$reader = PHPExcel_IOFactory::createReaderForFile($path);
$reader->setReadDataOnly(true);
$excel = $reader->load($path);
$this->assertEquals("homer", $excel->getSheet(0)->getCell('A1')->getValue());
$this->assertEquals("simpson", $excel->getSheet(0)->getCell('B1')->getValue());
$this->assertEquals("field_value", $excel->getSheet(0)->getCell('C1')->getValue());
$this->assertContains("language", $excel->getSheet(0)->getCell('D1')->getValue());
$excel->disconnectWorksheets();
unset($excel);
//data, changing all columns in the header array
$rs = $this->fetchData("SELECT id, field_id, field_value, language FROM error_message_table LIMIT 10");
$this->assertNotEmpty(FileManager::createExcelFromRS($errorMessage, $rs, $rows, "woot", array("kyle", "eric", "kenny", "stan")));
$this->assertInstanceOf('PDOStatement', $rs);
$this->assertEmpty($errorMessage);
$this->assertEquals(10, $rows);
$this->assertFileExists($path);
$reader = PHPExcel_IOFactory::createReaderForFile($path);
$reader->setReadDataOnly(true);
$excel = $reader->load($path);
$this->assertEquals("kyle", $excel->getSheet(0)->getCell('A1')->getValue());
$this->assertEquals("eric", $excel->getSheet(0)->getCell('B1')->getValue());
$this->assertEquals("kenny", $excel->getSheet(0)->getCell('C1')->getValue());
$this->assertContains("stan", $excel->getSheet(0)->getCell('D1')->getValue());
$excel->disconnectWorksheets();
unlink($path);
}
private function fetchData($query, $db = "language_manager"){
$_SESSION['domain'] = $db;
$errorMessage = "";
$dbManager = GeneralDBManager::getInstance();
$rs = $dbManager->fetchData($query . ";/*th1s 1s a v4l1d qu3ry*/", $errorMessage);
unset($dbManager);
return $rs;
}
结论:示例代码允许您在一行代码中通过 PDO 从任何查询自动生成 excel(或 CSV)文件。
在我以前的工作中,缺乏集中化导致我编写了无数函数(在 VB6 中)以从软件(GEM-CAR)导出数据。当然,写一个基本的数据导出函数没有什么深奥的,但是每一个新的导出都需要写一个新的函数。随着时间的推移,每个功能本身最终都会成为维护的挑战。
使用这些集中化的方法,只需要一行代码就可以生成一个文件。这意味着您只需更改 SQL 查询即可更改文件的内容(如果您需要多语言支持或避免在数据库中显示字段的真实名称,则为 columnName 数组的值)。
我自己可能是乌托邦,但我假设您在盲目地将我的代码粘贴到您的软件之前阅读了整个解释。 Copying/pasting 直接从 Internet 编码实际上是最糟糕的事情。
不仅您不能确定您没有将无用代码紧紧地绑定到它的原始上下文,您还可以确定您没有正确使用代码。
更糟糕的是,如果从未知来源复制整个模块,甚至可能导致系统安全漏洞。人们永远无法确定互联网用户的意图是好是坏...
愿原力与你们同在,
来自蒙特利尔的 Jonathan Parent-Lévesque
假设我们有一个像这样的函数,它接受一个 PDOStatement(任何查询)并自动生成一个 excel 文件(使用 PHPExcel 库):
/**
* Create an Excel file from an opened PDOStatement. Return the path to access the Excel file
* or an empty string if we were not able to create the Excel File
*
*
* @param $errorMessage String to return the error message.
* @param $source PDOStatement containing the data to export to Excel.
* @param $rows Int Use to return the number of rows exported (the header row isn't counted).
* @param $name String name to give to the Excel file (no need to specify the extension).
* @param $columnName (optional) String Array used for the name of the row in the Excel file.
*
* @return String
*/
public static function createExcelFromRS(&$errorMessage, PDOStatement &$source, &$rows , $name, array $columnName = array()){
$errorMessage = "";
$name = self::validateFileExtention($name, "xlsx");
$path = realpath(dirname(__FILE__)) . '/Archive/' . $name;
$rows = 0;
$totalCols = 0;
$excel = new PHPExcel();
$writer = PHPExcel_IOFactory::createWriter($excel, "Excel2007");
$sheet = $excel->getActiveSheet();
$sheet->setTitle($name);
while ($row = $source->fetch(PDO::FETCH_ASSOC)){
if ($rows === 0){
$columnName = self::validateColumnNameArray($columnName, $row);
$totalCols = count($row);
$sheet->getStyle('A1:' . self::convertNumberToExcelCol($totalCols) . '1')->getFont()->setBold(true)->setSize(12);
for ($column = 1; $column <= $totalCols; $column++){
$sheet->getCell(self::convertNumberToExcelCol($column) . '1')->setValue($columnName[$column - 1]);
$sheet->getColumnDimension(self::convertNumberToExcelCol($column))->setAutoSize(true);
}
$rows = 1;
}
$rows++;
$column = 1;
foreach ($row as $field){
$sheet->getCell(self::convertNumberToExcelCol($column) . $rows)->setValue($field);
$column++;
}
}
$writer->save($path);
unset($sheet, $writer, $excel);
if ($rows < 1){
if (is_file($path)){
unlink($path);
}
$errorMessage =str_replace("[TYPE]", "EXCEL", GeneralDbManager::getInstance()->getErrorMessage('NO_DATA_TO_EXPORT_FILE_ERR', 'There is no data to export to the [TYPE] file.'));
}
elseif(!is_file($path)){
$errorMessage = str_replace(array("[TYPE]", "[NAME]"), array("EXCEL", $name), GeneralDbManager::getInstance()->getErrorMessage('EXPORT_NO_CREATED_FILE_ERR', 'We were not able to create the [TYPE] file: [NAME].'));
}
else{
$rows --;
}
return (empty($errorMessage) ? $path : "");
}
并且我们想使用 convertNumberToExcelCol
函数将整数值转换为 excel 列。
我们先讲解一下这种构建excel文件的方法,下一个post会讲解获取列的算法。
方法作为参数:
- ErrorMessage:用于return错误信息
- source:包含要推送到 excel 文件的数据
- Rows:用于return导出的数据行数
- name: excel 文件的名称
- columnName:一个可选的数组,与人类可读的列名(或翻译)一起使用。如果省略,该方法使用查询中的字段名称。
第一行用于初始化参数(PHP使用松散类型,所以我们必须小心参数)。
此函数确保名称具有有效 name/extension:
/**
* Validate that the file $name has the proper file $extension
* and return the fixed name with the proper extension
*
* Note: No modification will be made if the extension is not a string or is empty
*
* @param $name String file name with or without extension
* @param $extension String example: csv, xls
*
* @return String
*/
public static function validateFileExtention($name, $extension){
if (is_string($extension)){
$extension = "." . str_replace(".", "", $extension);
if (strlen($extension) > 1){
if (!is_string($name) or empty($name) or strpos($name, ".") === 0){
$name = "my_file" . $extension;
}
elseif(strpos(strtolower($name), $extension) === false){
if (strrpos($name, ".") === false){
$name .= $extension;
}
else{
if (substr_count($name, ".") > 1){
$name = str_replace(".", "", $name) . $extension;
}
else{
$name = str_replace(substr($name, strrpos($name, ".")), $extension, $name);
}
}
}
}
}
return $name;
}
然后我们打开与excel文件的连接:
$excel = new PHPExcel();
$writer = PHPExcel_IOFactory::createWriter($excel, "Excel2007");
$sheet = $excel->getActiveSheet();
$sheet->setTitle($name);
此函数确保列名数组的长度与行数组中的字段数相同。
/**
* Take the array containing the $columnName for data export (CSV, Excel) and make sure
* that it is the number of entry as there are fields in $row.
*
* If column name are missing, we will use the column name used in the query.
*
* Return the merged array
*
* @param $columnName Array containing the column names
* @param $row Array produce by fetch(PDO::FETCH_ASSOC).
*
* @return Array ($columnName)
*/
private static function validateColumnNameArray(array &$columnName, array &$row){
$buffer = array();
$colPDO = count($row);
$count = count($columnName);
if ($count < $colPDO){
foreach ($row as $key => $value){
$buffer[] = $key;
}
for($index = $count; $index < $colPDO; $index++){
$columnName[] = $buffer[$index];
}
}
unset($buffer);
return $columnName;
}
validateFileExtention
和 validateColumnNameArray
都是针对具有 CSV 创建功能的共享代码:
/**
* Create a CSV file from an opened PDOStatement. Return the path to access the CSV file
* or an empty string if we were not able to create the CSV File
*
*
* @param $errorMessage String to return the error message.
* @param $source PDOStatement containing the data to export to CSV
* @param $rows Int Use to return the number of rows exported (the header row isn't counted).
* @param $name String name to give to the CSV file (no need to specify the extension).
* @param $columnName (optional) String Array used for the name of the row in the CSV file.
*
* @return String
*/
public static function createCSVFromRS(&$errorMessage, PDOStatement &$source, &$rows , $name, array $columnName = array()){
$errorMessage = "";
$name = self::validateFileExtention($name, "csv");
$path = realpath(dirname(__FILE__)) . '/Archive/' . $name;
$rows = 0;
$file = fopen($path, "w");
while ($row = $source->fetch(PDO::FETCH_ASSOC)){
if ($rows === 0){
fputcsv($file, array_map('utf8_decode',self::validateColumnNameArray($columnName, $row)));
}
fputcsv($file, array_map('utf8_decode',array_values($row)));
$rows++;
}
fclose($file);
if ($rows < 1){
if (is_file($path)){
unlink($path);
}
$errorMessage =str_replace("[TYPE]", "CSV", GeneralDbManager::getInstance()->getErrorMessage('NO_DATA_TO_EXPORT_FILE_ERR', 'There is no data to export to the [TYPE] file.'));
}
elseif(!is_file($path)){
$errorMessage = str_replace(array("[TYPE]", "[NAME]"), array("CSV", $name), GeneralDbManager::getInstance()->getErrorMessage('EXPORT_NO_CREATED_FILE_ERR', 'We were not able to create the [TYPE] file: [NAME].'));
}
return (empty($errorMessage) ? $path : "");
}
如果这是我们添加到 excel 文件的第一行,那么我们设置基本格式:
if ($rows === 0){
$columnName = self::validateColumnNameArray($columnName, $row);
$totalCols = count($row);
$sheet->getStyle('A1:' . self::convertNumberToExcelCol($totalCols) . '1')->getFont()->setBold(true)->setSize(12);
for ($column = 1; $column <= $totalCols; $column++){
$sheet->getCell(self::convertNumberToExcelCol($column) . '1')->setValue($columnName[$column - 1]);
$sheet->getColumnDimension(self::convertNumberToExcelCol($column))->setAutoSize(true);
}
$rows = 1;
}
我们使用 getStyle 方法将 header 行设置为粗体和 12 大小。
getCOlumnDimension 方法用于设置自动调整大小,因此用户在打开文件时不必调整列 himself/herself 的大小。
循环的其余部分是将数据从行数组传输到 excel 文件。
循环后,我们关闭连接并取消设置用于管理的变量 Excel。
然后是错误管理:
if ($rows < 1){
if (is_file($path)){
unlink($path);
}
$errorMessage =str_replace("[TYPE]", "EXCEL", GeneralDbManager::getInstance()->getErrorMessage('NO_DATA_TO_EXPORT_FILE_ERR', 'There is no data to export to the [TYPE] file.'));
}
elseif(!is_file($path)){
$errorMessage = str_replace(array("[TYPE]", "[NAME]"), array("EXCEL", $name), GeneralDbManager::getInstance()->getErrorMessage('EXPORT_NO_CREATED_FILE_ERR', 'We were not able to create the [TYPE] file: [NAME].'));
}
else{
$rows --;
}
消息存储在数据库中,因此我们可以向用户提供翻译后的消息。我在我的消息中使用了通用的 [TYPE] 和 [NAME] 标签,我将其替换为正确的文件类型和文件名。
这让我可以在我的 excel 和我正在生成的 CSV 文件(或任何类型的文件)中重复使用这条通用消息。
如果创建的文件是空的,我将其删除。此操作是可选的,但我喜欢在完成后立即从磁盘中清除未使用的文件。
另一种方法是使用函数清除存储目录:
/**
* Clear all the archives (zip) files in the archive folder.
*/
public static function emptyArchiveFolder(){
$handle = NULL;
$path = realpath(dirname(__FILE__)) . '/Archive/';
if (is_dir($path) and $handle = opendir($path)) {
while (false !== ($entry = readdir($handle))) {
$file = $path . $entry;
if (is_file($file)){
unlink($file);
}
}
unset($handle);
}
}
我个人只在午夜对源文件和数据库进行自动备份时使用这种方法。 运行 它在白天增加了删除其他用户使用的文件的机会。
这就是为什么我正在考虑最好的做法是在文件通过浏览器发送给用户后立即将其删除,并将清理方法仅用于维护目的。
如果没有错误,我将行数减一,因为我不想计算 header 行。如果您将 header 行视为数据行,则可以删除该行。
最后是方法return访问新建文件的路径:
return (empty($errorMessage) ? $path : "");
但前提是没有错误。因此,如果函数 return 是一个空字符串,则表示发生了错误。
PHP 类型松散,你可以 return 任何东西,包括布尔值甚至错误消息,但我更喜欢 return 始终使用相同的数据类型以保持不变。我个人最喜欢的方法是布尔 return 值和通过引用传递的错误消息变量。所以我可以使用这样的代码:
$errorMessage = "";
if ($_SESSION["adminAccount"]->updateAccountInfo($errorMessage,
(isset($_POST['FIRST_NAME_TEXT']) ? $_POST['FIRST_NAME_TEXT'] : $_SESSION["adminAccount"]->getFirstName()),
(isset($_POST['LAST_NAME_TEXT']) ? $_POST['LAST_NAME_TEXT'] : $_SESSION["adminAccount"]->getLastName()),
(isset($_POST['EMAIL_TEXT']) ? $_POST['EMAIL_TEXT'] : $_SESSION["adminAccount"]->getEmail()))){
PageManager::displaySuccessMessage("Your account information were saved with success.", "USER_ACCOUNT_INFORMATION_SAVED");
}
else{
PageManager::displayErrorMessage($errorMessage);
}
这样,错误由 class 方法在内部管理,并且可以根据视图上下文调整成功消息。布尔值 return 用于确定我们是否必须显示错误或成功消息。
注意:单元测试将包含在我的答案中。
Jonathan Parent-L来自蒙特利尔的évesque
下面是允许将整数转换为 excel 列的方法。
excel 列可以是一到三个字母的组合,最大为 16383 (XFD),这是 excel 文件中列的当前限制:
我在我的函数旁边使用了一个包含所有英文字母的数组:
public static $letters = array(1 => "A", 2 => "B", 3=> "C", 4 => "D", 5 => "E", 6 => "F", 7 => "G", 8=> "H", 9=> "I", 10 => "J", 11 =>"K", 12 => "L", 13 => "M", 14 => "N", 15=> "O", 16 => "P", 17 => "Q", 18 => "R", 19 => "S", 20 => "T", 21 => "U", 22 => "V", 23 => "W", 24 => "X", 25 => "Y", 26 => "Z");
该函数采用单个参数并断言该值为数字并且在 excel 限制之间:
public static function convertNumberToExcelCol($number){
$column = "";
if (is_numeric($number) and $number > 0 and $number < 16385){
如果数字在 1 到 26 之间(A-Z 列),那就小菜一碟了。我们只是直接在数组中获取字母。
$column = self::$letters[$number];
让我们测试一下:
for ($index = 1; $index < 27; $index++){
$this->assertEquals(FileManager::$letters[$index], FileManager::convertNumberToExcelCol($index));
}
如果列介于 27 和 702 (AA-ZZ) 之间,编码仍然非常简单:
if ($number % 26 === 0){
$first = floor($number / 26) - 1;
$second = 26;
}
else{
$first = floor($number / 26);
$second = $number % 26;
}
$column = self::$letters[$first] . self::$letters[$second];
由于英文字母有26个字母,所以整个算法也是以26为底的
对于大多数值,我们可以简单地通过向下舍入 [number]/26 得到第一个字母,使用数字的整个除法(除法的余数)得到第二个字母:[number] % 26.
$first = floor($number / 26);
$second = $number % 26;
这里有一些例子:
- 27 = AA
- 28=AB
- 51 = 年
- 53 = 文学学士
如果[number] % 26 = 0,例如:52、78、104等,那么我们必须使用一些不同的编码。
例如,52 = AZ 但是,52/26=2(第一个字母)和 52%26=0(第二个字母)。
2=B 并且 0 在我们的字母数组中超出范围。这就是为什么,我们必须将第一个字母的值减一,并强制将 26 作为第二个字母的值。
$first = floor($number / 26) - 1;
$second = 26;
测试时间:
for ($first = 1; $first < 27; $first++){
$temp = $first * 26;
for ($second = 1; $second < 27; $second++){
$this->assertEquals(FileManager::$letters[$first] . FileManager::$letters[$second], FileManager::convertNumberToExcelCol($temp + $second));
}
}
当我们试图管理三个字母时,真正的挑战就来了。当然,你可能不会有超过 702 个字段的查询,但是为了方法的完整性和一些真正的编程挑战,让我们来看看如何做到这一点!
起初,我不得不尝试错误测试并最终得到以下编码:
elseif($number < 1379){
$column = self::$letters[floor($number / 702)] . self::convertNumberToExcelCol($number % 702 + 26);
}
elseif($number < 2028){
$column = self::$letters[floor($number / 676)] . self::convertNumberToExcelCol($number % 676);
}
elseif ($number < 2055){
$column = self::$letters[floor($number / 702)] . self::convertNumberToExcelCol($number % 702 + 52);
}
elseif($number < 2704){
$column = self::$letters[floor($number / 676)] . self::convertNumberToExcelCol($number % 676);
}
elseif ($number < 2731) {
$column = self::$letters[floor($number / 702)] . self::convertNumberToExcelCol($number % 702 + 78);
}
elseif ($number < 3380) {
$column = self::$letters[floor($number / 676)] . self::convertNumberToExcelCol($number % 676);
}
elseif ($number < 3407){
$column = self::$letters[floor($number / 702)] . self::convertNumberToExcelCol($number % 702 + 104);
}
是的,这并不严重,你可以像这样一路上升到 16k...
如果你仔细看,你会发现有一个图案就是从这里画出来的。有些数字可以除以 676 得到第一个字母,模 676 得到第二个和第三个字母。例如,2027 = BBY.
第二个模式是可以将第一个字母除以 702 + 补偿的模数(26、52、78、104,...)的数字。这包括像 703=AAA.
这样的数字当然676和702都是26的倍数
我花了很多计算,但我开始意识到第二个模式总是 27 个数字的范围,并且这些数字总是产生一个低于 27 (0-26) 的数字作为这个模数数.
elseif($number < 2028){
$column = self::$letters[floor($number / 676)] . self::convertNumberToExcelCol($number % 676);
}
elseif ($number < 2055){
例如:
- 2028 % 676 = 0
- 2029 % 676 = 1
- ...
- 2054 % 676 = 26
- 2055 % 676 = 27(现在超出范围)
既然找到了使用702算法的人数范围,就要确定如何计算补偿了。薪酬始终基于 26,因此可能与此相关...
对于范围2028-2054,补偿为52。
- 2028 / 26 = 78
- 2029 / 26 = 78.03846
- ...
- 2054 / 26 = 79
对于范围2704-2730,补偿为78。
- 2704 / 26 = 104
- 2730 / 26 = 105
如你所见,规则是[补偿] = [数量] / 26 - 26(上限减1)。
所以三个字母的编码(以前是无穷无尽的if/else)可以分5行恢复:
if($number % 676 < 27){
$compensation = floor($number / 26) - 26;
$column = self::$letters[floor($number / 702)] . self::convertNumberToExcelCol($number % 702 + ($compensation % 26 === 0 ? $compensation : $compensation - 1));
}
else{
$column = self::$letters[floor($number / 676)] . self::convertNumberToExcelCol($number % 676);
}
函数 returns 如果参数值无效,则为字母组合或空字符串。
让我们测试一下,以确保我没有骗你:
for ($first = 1; $first < 27; $first++){
for ($second = 1; $second < 27; $second++){
for ($third = 1; $third < 27; $third++){
$temp = $first * 676 + (($second * 26) + $third);
if ($temp < 16385){
$this->assertEquals(FileManager::$letters[$first] . FileManager::$letters[$second] . FileManager::$letters[$third], FileManager::convertNumberToExcelCol($temp));
}
else{
$this->assertEmpty(FileManager::convertNumberToExcelCol($temp + $index));
}
}
}
}
完整函数如下:
/**
* Convert a $number to the letter (or combination of letters) representing a column in excel.
* Will return an empty string if $number is not a valid value.
*
* @param number Int must be is_numeric() and > 0 and < 16,385.
*
* @return String
*/
public static function convertNumberToExcelCol($number){
$column = "";
if (is_numeric($number) and $number > 0 and $number < 16385){
if ($number < 27){
$column = self::$letters[$number];
}
elseif ($number < 703){
if ($number % 26 === 0){
$first = floor($number / 26) - 1;
$second = 26;
}
else{
$first = floor($number / 26);
$second = $number % 26;
}
$column = self::$letters[$first] . self::$letters[$second];
}
else{
if($number % 676 < 27){
$compensation = floor($number / 26) - 26;
$column = self::$letters[floor($number / 702)] . self::convertNumberToExcelCol($number % 702 + ($compensation % 26 === 0 ? $compensation : $compensation - 1));
}
else{
$column = self::$letters[floor($number / 676)] . self::convertNumberToExcelCol($number % 676);
}
}
}
return $column;
}
当然,正如所承诺的,这里是 excel/csv 文件创建的单元测试代码。
public function testvalidateFileExtention(){
//invalid extension
$this->assertEquals("woot", FileManager::validateFileExtention("woot", true));
$this->assertEquals("woot", FileManager::validateFileExtention("woot", ""));
$this->assertEquals("woot", FileManager::validateFileExtention("woot", "."));
$this->assertEquals("woot.blu", FileManager::validateFileExtention("woot", ".b.l.u.."));
//invalid name
$this->assertEquals("my_file.blu", FileManager::validateFileExtention(true, ".blu"));
$this->assertEquals("my_file.blu", FileManager::validateFileExtention("", ".blu"));
$this->assertEquals("my_file.blu", FileManager::validateFileExtention(".woot", ".blu"));
$this->assertEquals("woot.blu", FileManager::validateFileExtention("w.o.o.t.", ".blu"));
//valid file name and extension
$this->assertEquals("woot.blu", FileManager::validateFileExtention("woot", "blu"));
$this->assertEquals("woot.blu", FileManager::validateFileExtention("woot", ".blu"));
}
public function testCreateCSVFromRS(){
FileManager::emptyArchiveFolder();
$errorMessage = "";
$path = realpath(dirname(__FILE__)) . '/../Archive/woot.csv';
$rows = 0;
//no data to export
$rs = $this->fetchData("SELECT id, field_id, field_value, language FROM error_message_table WHERE field_id='woot for loots'");
$this->assertInstanceOf('PDOStatement', $rs);
$this->assertEmpty(FileManager::createCSVFromRS($errorMessage, $rs, $rows, "woot"));
$this->assertNotEmpty($errorMessage);
$this->assertEquals(0, $rows);
$this->assertFileNotExists($path);
//data, but missing columns in the header array
$rs = $this->fetchData("SELECT id, field_id, field_value, language FROM error_message_table LIMIT 100");
$this->assertNotEmpty(FileManager::createCSVFromRS($errorMessage, $rs, $rows, "woot", array("homer", "simpson")));
$this->assertInstanceOf('PDOStatement', $rs);
$this->assertEmpty($errorMessage);
$this->assertEquals(100, $rows);
$this->assertFileExists($path);
$handle = fopen($path, "r");
$this->assertNotEquals(false, $handle);
$row = fgetcsv($handle);
$this->assertContains("homer", $row);
$this->assertNotContains("id", $row);
$this->assertContains("simpson", $row);
$this->assertNotContains("field_id", $row);
$this->assertContains("field_value", $row);
$this->assertContains("language", $row);
fclose($handle);
//data, changing all columns in the header array
$rs = $this->fetchData("SELECT id, field_id, field_value, language FROM error_message_table LIMIT 10");
$this->assertNotEmpty(FileManager::createCSVFromRS($errorMessage, $rs, $rows, "woot", array("kyle", "eric", "kenny", "stan")));
$this->assertInstanceOf('PDOStatement', $rs);
$this->assertEmpty($errorMessage);
$this->assertEquals(10, $rows);
$this->assertFileExists($path);
$handle = fopen($path, "r");
$this->assertNotEquals(false, $handle);
$row = fgetcsv($handle);
$this->assertContains("kyle", $row);
$this->assertNotContains("id", $row);
$this->assertContains("eric", $row);
$this->assertNotContains("field_id", $row);
$this->assertContains("kenny", $row);
$this->assertNotContains("field_value", $row);
$this->assertContains("stan", $row);
$this->assertNotContains("language", $row);
fclose($handle);
unlink($path);
}
public function testConvertNumberToExcelCol(){
//invalid paramter
$this->assertEmpty(FileManager::convertNumberToExcelCol("a"));
$this->assertEmpty(FileManager::convertNumberToExcelCol(array()));
$this->assertEmpty(FileManager::convertNumberToExcelCol(-1));
$this->assertEmpty(FileManager::convertNumberToExcelCol(1000000000));
//single letter
for ($index = 1; $index < 27; $index++){
$this->assertEquals(FileManager::$letters[$index], FileManager::convertNumberToExcelCol($index));
}
//double letters
for ($first = 1; $first < 27; $first++){
$temp = $first * 26;
for ($second = 1; $second < 27; $second++){
$this->assertEquals(FileManager::$letters[$first] . FileManager::$letters[$second], FileManager::convertNumberToExcelCol($temp + $second));
}
}
//tripple letters
for ($first = 1; $first < 27; $first++){
for ($second = 1; $second < 27; $second++){
for ($third = 1; $third < 27; $third++){
$temp = $first * 676 + (($second * 26) + $third);
if ($temp < 16385){
$this->assertEquals(FileManager::$letters[$first] . FileManager::$letters[$second] . FileManager::$letters[$third], FileManager::convertNumberToExcelCol($temp));
}
else{
$this->assertEmpty(FileManager::convertNumberToExcelCol($temp + $index));
}
}
}
}
}
public function testCreateExcelFromRS(){
$errorMessage = "";
$path = realpath(dirname(__FILE__)) . '/../Archive/woot.xlsx';
$rows = 0;
//no data to export
$rs = $this->fetchData("SELECT id, field_id, field_value, language FROM error_message_table WHERE field_id='woot for loots'");
$this->assertInstanceOf('PDOStatement', $rs);
$this->assertEmpty(FileManager::createExcelFromRS($errorMessage, $rs, $rows, "woot"));
$this->assertNotEmpty($errorMessage);
$this->assertEquals(0, $rows);
$this->assertFileNotExists($path);
//data, but missing columns in the header array
$rs = $this->fetchData("SELECT id, field_id, field_value, language FROM error_message_table LIMIT 100");
$this->assertNotEmpty(FileManager::createExcelFromRS($errorMessage, $rs, $rows, "woot", array("homer", "simpson")));
$this->assertInstanceOf('PDOStatement', $rs);
$this->assertEmpty($errorMessage);
$this->assertEquals(100, $rows);
$this->assertFileExists($path);
$reader = PHPExcel_IOFactory::createReaderForFile($path);
$reader->setReadDataOnly(true);
$excel = $reader->load($path);
$this->assertEquals("homer", $excel->getSheet(0)->getCell('A1')->getValue());
$this->assertEquals("simpson", $excel->getSheet(0)->getCell('B1')->getValue());
$this->assertEquals("field_value", $excel->getSheet(0)->getCell('C1')->getValue());
$this->assertContains("language", $excel->getSheet(0)->getCell('D1')->getValue());
$excel->disconnectWorksheets();
unset($excel);
//data, changing all columns in the header array
$rs = $this->fetchData("SELECT id, field_id, field_value, language FROM error_message_table LIMIT 10");
$this->assertNotEmpty(FileManager::createExcelFromRS($errorMessage, $rs, $rows, "woot", array("kyle", "eric", "kenny", "stan")));
$this->assertInstanceOf('PDOStatement', $rs);
$this->assertEmpty($errorMessage);
$this->assertEquals(10, $rows);
$this->assertFileExists($path);
$reader = PHPExcel_IOFactory::createReaderForFile($path);
$reader->setReadDataOnly(true);
$excel = $reader->load($path);
$this->assertEquals("kyle", $excel->getSheet(0)->getCell('A1')->getValue());
$this->assertEquals("eric", $excel->getSheet(0)->getCell('B1')->getValue());
$this->assertEquals("kenny", $excel->getSheet(0)->getCell('C1')->getValue());
$this->assertContains("stan", $excel->getSheet(0)->getCell('D1')->getValue());
$excel->disconnectWorksheets();
unlink($path);
}
private function fetchData($query, $db = "language_manager"){
$_SESSION['domain'] = $db;
$errorMessage = "";
$dbManager = GeneralDBManager::getInstance();
$rs = $dbManager->fetchData($query . ";/*th1s 1s a v4l1d qu3ry*/", $errorMessage);
unset($dbManager);
return $rs;
}
结论:示例代码允许您在一行代码中通过 PDO 从任何查询自动生成 excel(或 CSV)文件。
在我以前的工作中,缺乏集中化导致我编写了无数函数(在 VB6 中)以从软件(GEM-CAR)导出数据。当然,写一个基本的数据导出函数没有什么深奥的,但是每一个新的导出都需要写一个新的函数。随着时间的推移,每个功能本身最终都会成为维护的挑战。
使用这些集中化的方法,只需要一行代码就可以生成一个文件。这意味着您只需更改 SQL 查询即可更改文件的内容(如果您需要多语言支持或避免在数据库中显示字段的真实名称,则为 columnName 数组的值)。
我自己可能是乌托邦,但我假设您在盲目地将我的代码粘贴到您的软件之前阅读了整个解释。 Copying/pasting 直接从 Internet 编码实际上是最糟糕的事情。
不仅您不能确定您没有将无用代码紧紧地绑定到它的原始上下文,您还可以确定您没有正确使用代码。
更糟糕的是,如果从未知来源复制整个模块,甚至可能导致系统安全漏洞。人们永远无法确定互联网用户的意图是好是坏...
愿原力与你们同在,
来自蒙特利尔的 Jonathan Parent-Lévesque