├── BatchChildEditor.css ├── BatchChildEditor.js ├── BatchChildEditor.module.php ├── LICENSE ├── ProcessChildrenCsvExport.module.php ├── README.md └── parsecsv-for-php ├── .editorconfig ├── .gitignore ├── .travis.yml ├── ChangeLog.txt ├── License.txt ├── Makefile ├── README.md ├── composer.json ├── examples ├── _books.csv ├── basic.php ├── conditions.php ├── download.php ├── limit.php └── save_to_file_without_header_row.php ├── parsecsv.lib.php ├── src ├── Csv.php ├── enums │ ├── AbstractEnum.php │ ├── DatatypeEnum.php │ ├── FileProcessingModeEnum.php │ └── SortEnum.php └── extensions │ └── DatatypeTrait.php └── tests ├── Bootstrap.php ├── PHPUnit_Framework_TestCase.inc.php ├── example_files ├── Piwik_API_download.csv ├── UTF-16LE_with_BOM_and_sep_row.csv ├── UTF-8_sep_row_but_no_BOM.csv ├── UTF-8_with_BOM_and_sep_row.csv ├── multiple_empty_lines.csv ├── single_column.csv └── waiver_field_separator.csv ├── methods ├── ConstructTest.php ├── DataRowCountTest.php ├── DatatypeTest.php ├── ExampleStream.php ├── ObjectThatHasToStringMethod.php ├── OldRequireTest.php ├── OutputTest.php ├── ParseTest.php ├── SaveTest.php ├── StreamTest.php ├── UnparseTest.php └── fixtures │ ├── auto-double-enclosure.csv │ ├── auto-single-enclosure.csv │ └── datatype.csv ├── phpunit.xml └── properties ├── BaseClass.php ├── ConditionsTest.php ├── DefaultValuesPropertiesTest.php ├── OffsetTest.php ├── PublicPropertiesTest.php └── SortByTest.php /BatchChildEditor.css: -------------------------------------------------------------------------------- 1 | .bcePageName { 2 | display: inline-block; 3 | } 4 | 5 | table#bceLanguages { 6 | border-collapse: collapse; 7 | padding: 5px; 8 | } 9 | 10 | table#bceLanguages tr td { 11 | background: #F0F3F7; 12 | } 13 | 14 | table#bceLanguages td, table#bceLanguages th { 15 | border: 1px solid #C9D4E1 !important; 16 | padding: 5px; 17 | } 18 | 19 | .ui-tooltip { 20 | max-width: 95% !important; 21 | background: #FFFFFF !important; 22 | padding: 0 !important; 23 | color: #222222 !important; 24 | border: 1px solid #979797; 25 | border-radius: 0 !important; 26 | text-align: left !important; 27 | font-weight: normal !important; 28 | font-size: 13px !important; 29 | font-stretch: none !important; 30 | text-decoration: none !important; 31 | box-shadow: none !important 32 | } 33 | 34 | .batchChildTableContainer .bceLangInfoRow { 35 | padding: 0; 36 | background: #F0F3F7; 37 | } 38 | 39 | .batchChildTableContainer .bceLangInfoRow > td > div { 40 | max-height: 0; 41 | overflow: hidden; 42 | transition: max-height 0.5s; 43 | } 44 | 45 | .batchChildTableContainer .pageinfo-visible + tr.bceLangInfoRow > td > div { 46 | max-height: 600px; 47 | } 48 | 49 | .batchChildTableContainer .pageinfo-visible .bceTogglePageInfo:before { 50 | content: "\f147" 51 | } 52 | 53 | .batchChildTableContainer tr.bceLangInfoRow { 54 | border: none !important; 55 | } 56 | 57 | .batchChildTableContainer table.bceLanguages { 58 | width: 100%; 59 | font-size: 0.9em; 60 | margin-bottom: 0.5em; 61 | } 62 | 63 | .batchChildTableContainer .bceLangInfoRow:hover td { 64 | background: inherit !important; 65 | } 66 | 67 | .bceTogglePageInfo { 68 | cursor: pointer; 69 | padding: 0.2em; 70 | position: relative; 71 | top: 1px; 72 | margin-right: 0.2em; 73 | } 74 | 75 | .batchChildTableContainer table.bceLanguages th { 76 | border-bottom-style: solid; 77 | color: inherit; 78 | opacity: 0.84; 79 | cursor: default; 80 | } 81 | 82 | .batchChildTableContainer .bceLanguages td { 83 | padding: 0.2em 0.5em 0.2em 0 !important; 84 | width: 35%; 85 | border-right: none !important; 86 | border-left: none !important; 87 | } 88 | 89 | .batchChildTableContainer .bceLanguages tr td:first-child { 90 | width: 20%; 91 | } 92 | 93 | .batchChildTableContainer .bceLanguages tr td:last-child { 94 | width: 10%; 95 | } 96 | 97 | .batchChildTableContainer .bceLanguages tr:hover td { 98 | background: #E2E9EF !important; 99 | } 100 | 101 | .batchChildTableContainer .bceLangInfoRow > td { 102 | background: inherit !important; 103 | padding: 0 !important; 104 | border: none !important; 105 | } 106 | 107 | /* drag-and-drop handle icon */ 108 | .batchChildTableContainer > table > tbody > tr > td:first-child { 109 | text-align: center; 110 | } 111 | 112 | .bcePageNameContainer { 113 | width:100%; 114 | max-width:200px; 115 | text-overflow:ellipsis; 116 | overflow:hidden; 117 | display:inline-block; 118 | } -------------------------------------------------------------------------------- /BatchChildEditor.js: -------------------------------------------------------------------------------- 1 | function getUrlVars(url) { 2 | var vars = {}; 3 | var parts = url.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (m, key, value) { 4 | vars[key] = value; 5 | }); 6 | return vars; 7 | } 8 | 9 | 10 | function batchChildTableDialog() { 11 | 12 | var $a = $(this); 13 | var url = $a.attr('data-url') 14 | var closeOnSave = true; 15 | var $iframe = pwModalWindow(url, {}, 'large'); 16 | 17 | var dialogPageID = 0; 18 | 19 | $iframe.on('load', function () { 20 | 21 | var buttons = []; 22 | var pid = getUrlVars(url)["id"]; 23 | var langid = getUrlVars(url)["lang"]; 24 | var $icontents = $iframe.contents(); 25 | var initTemplate = $icontents.find('#template option:selected').text(); 26 | var n = 0; 27 | 28 | dialogPageID = $icontents.find('#Inputfield_id').val(); // page ID that will get added if not already present 29 | 30 | // hide things we don't need in a modal context 31 | //$icontents.find('#breadcrumbs ul.nav, #Inputfield_submit_save_field_copy').hide(); 32 | 33 | closeOnSave = $icontents.find('#ProcessPageAdd').length == 0; 34 | 35 | // copy buttons in iframe to dialog 36 | $icontents.find("#content form button.ui-button[type=submit], button.ui-button[name=submit_delete]").each(function () { 37 | var $button = $(this); 38 | var text = $button.text(); 39 | var skip = false; 40 | // avoid duplicate buttons 41 | for (i = 0; i < buttons.length; i++) { 42 | if (buttons[i].text == text || text.length < 1) skip = true; 43 | } 44 | // ignore Move to Trash button because we need to add click event to it separately 45 | if (text == 'Move to Trash') skip = true; 46 | 47 | if (!skip) { 48 | buttons[n] = { 49 | 'text': text, 50 | 'class': ($button.is('.ui-priority-secondary') ? 'ui-priority-secondary' : ''), 51 | 'click': function () { 52 | $button.click(); 53 | //if template has changed, then don't close on save as may need to accept confirmation of change 54 | if (closeOnSave && initTemplate == $icontents.find('#template option:selected').text()) setTimeout(function () { 55 | $iframe.dialog('close'); 56 | //var titleFieldId = langid ? '#Inputfield_title__' + langid : '#Inputfield_title'; 57 | //var titleVal = $icontents.find(titleFieldId).val(); 58 | var titleVal = $icontents.find('#Inputfield_title').val() 59 | $('#' + pid).val(titleVal); 60 | $('#' + pid).attr('placeholder', titleVal); 61 | var nameFieldId = langid ? '#Inputfield__pw_page_name' + langid : '#Inputfield__pw_page_name'; 62 | var nameVal = $icontents.find(nameFieldId).val(); 63 | $('#name_' + pid).text(nameVal); 64 | if (langid) $('input[id=langActiveStatus_' + pid + ']').prop('checked', $icontents.find('input[name=status' + langid + ']').is(':checked')); 65 | //version for when template is changeable and in a select dropdown 66 | $('select[id=template_' + pid).val($icontents.find('#template option:selected').val()); 67 | //version for when edit mode doesn't allow changing template, so it's just in a span 68 | $('span#template_' + pid).text($icontents.find('#template option:selected').text()); 69 | $('input[id=hiddenStatus_' + pid + ']').prop('checked', $icontents.find('#Inputfield_status_1024').is(':checked')); 70 | $('input[id=unpublishedStatus_' + pid + ']').prop('checked', $icontents.find('#Inputfield_status_2048').is(':checked')); 71 | //if the user clicks the Publish button rather than unchecking the "Unpublished" checkbox 72 | if (text == 'Publish') $('input[id=unpublishedStatus_' + pid + ']').prop('checked', false); 73 | }, 500); 74 | closeOnSave = true; // only let closeOnSave happen once 75 | } 76 | }; 77 | n++; 78 | } 79 | ; 80 | 81 | // don't hide "Move to Trash" button 82 | if (text == 'Move to Trash') { 83 | $button.on("click", function () { 84 | //delete page from table row if page deleted in modal 85 | if ($icontents.find('input[id=delete_page][value=' + pid + ']').is(':checked')) { 86 | $('tr.pid_' + pid).remove(); 87 | } 88 | $iframe.dialog('close'); 89 | }); 90 | } 91 | else { 92 | // hide original versions of buttons 93 | $button.hide(); 94 | } 95 | 96 | }); 97 | 98 | $iframe.setButtons(buttons); 99 | 100 | }); 101 | 102 | return false; 103 | } 104 | 105 | 106 | function batchChildTableSortable($table) { 107 | if (!$table.is("tbody")) $table = $table.find("tbody"); 108 | $table.sortable({ 109 | axis: 'y', 110 | handle: '.InputfieldChildTableRowSortHandle' 111 | }); 112 | } 113 | 114 | $(document).ready(function () { 115 | 116 | $(document).one('mouseover', '.batchChildTableContainer table', function () { 117 | 118 | $('.batchChildTableContainer table > tbody > tr').each(function () { 119 | 120 | var row = $(this), 121 | bcePageNameCell = $(this).find('.bcePageName[data-langinfo]'); 122 | 123 | if (!bcePageNameCell.length) return false; 124 | 125 | var cellCount = $(this).find('td').length - 1, 126 | multilangPageInfo = bcePageNameCell.attr('data-langinfo'); 127 | 128 | row.after('
' + multilangPageInfo + '
'); 129 | bcePageNameCell.parent('span').css('white-space', 'nowrap') 130 | .prepend(''); 131 | }); 132 | 133 | $('.bceTogglePageInfo').on('click', function () { 134 | $('.pageinfo-visible').not($(this).parents('.pageinfo-visible').first()).removeClass('pageinfo-visible'); 135 | $(this).parents('tr').first().toggleClass('pageinfo-visible'); 136 | }) 137 | }); 138 | 139 | //csv export 140 | //$('.Inputfield_iframe').hide(); 141 | $(document).on('click', '.children_export_csv', function () { 142 | $('#download').attr('src', config.urls.admin + 143 | "setup/children-csv-export/?pid=" + 144 | $(this).attr('data-pageid') + 145 | "&pti=" + ($("#Inputfield_pagesToInclude").val() ? $("#Inputfield_pagesToInclude").val() : $("#Inputfield_pagesToInclude").val()) + 146 | "&fns=" + ($("#Inputfield_userExportFields").val() ? $("#Inputfield_userExportFields").val() : $("#Inputfield_exportFields").val()) + 147 | "&cs=" + $("#Inputfield_export_column_separator").val() + 148 | "&ce=" + $("#Inputfield_export_column_enclosure").val() + 149 | "&ext=" + $("#Inputfield_export_extension").val() + 150 | "&nfr=" + ($("#Inputfield_export_names_first_row").is(':checkbox') ? $("#Inputfield_export_names_first_row").attr('checked') : $("#Inputfield_export_names_first_row").val()) + 151 | "&mvs=" + $("#Inputfield_export_multiple_values_separator").val() + 152 | "&fe=" + ($("#Inputfield_format_export").is(':checkbox') ? $("#Inputfield_format_export").attr('checked') : $("#Inputfield_format_export").val()) 153 | ); 154 | return false; 155 | }); 156 | 157 | 158 | /** 159 | * Add toggle controls to column headers (check/uncheck all items in a column) 160 | * 161 | * @author tpr 162 | * @updated 2015-09-15 163 | */ 164 | 165 | var bce_adminDataTableSelector = '.batchChildTableContainer .AdminDataTable', 166 | bce_columnControlClass = 'bce-column-toggle', 167 | bce_allowedColumnControls = ['input.langActiveStatus', 'input.hiddenStatus', 'input.unpublishedStatus', 'i.InputfieldChildTableRowDeleteLink'], 168 | bce_toggleControl = '', 169 | bce_controlEventType = 'change', 170 | bce_deletedRowClass = 'InputfieldChildTableRowDeleted', 171 | bce_isColumnControlsAdded = false; 172 | 173 | $(document).one('mouseover', bce_adminDataTableSelector, function () { 174 | addBceColumnControls(); 175 | }); 176 | 177 | /** 178 | * Set column header checkbox state based on all items of the column. 179 | * 180 | * @param $obj jQuery object 181 | */ 182 | function setColumnControlStates($obj) { 183 | 184 | var elem = $obj.is('input') ? 'input' : 'i', 185 | index = $obj.parent('td, th').index(), 186 | columnControl = $(bce_adminDataTableSelector + ' th:eq(' + index + ') input'), 187 | columnItems = $(bce_adminDataTableSelector).find('td:nth-child(' + parseInt(index + 1) + ')'), 188 | checkedItems = (elem == 'input') ? columnItems.find(':checked') : $(bce_adminDataTableSelector).find('.' + bce_deletedRowClass), 189 | allItems = columnItems.find(elem); 190 | 191 | if (checkedItems.length !== 0 && checkedItems.length === allItems.length) { 192 | columnControl.prop('checked', 1); 193 | } else { 194 | columnControl.prop('checked', 0); 195 | } 196 | } 197 | 198 | /** 199 | * Add control toggle checkboxes to BCE table. 200 | * 201 | * @returns {boolean} 202 | */ 203 | function addBceColumnControls() { 204 | 205 | if (bce_isColumnControlsAdded) { 206 | return false; 207 | } 208 | 209 | if ($(bce_adminDataTableSelector).length === 0) { 210 | return false; 211 | } 212 | 213 | // do not add controls if there is no more than 1 row 214 | if ($(bce_adminDataTableSelector + ' tbody tr').length <= 1) { 215 | return false; 216 | } 217 | 218 | //$(bce_adminDataTableSelector + ' tbody').on('click', 'input[type="checkbox"], i.InputfieldChildTableRowDeleteLink', function () { 219 | $(bce_adminDataTableSelector + ' tbody').on('click', 'input[type="checkbox"]', function () { 220 | setColumnControlStates($(this)); 221 | }); 222 | 223 | // add new controls 224 | for (var i = 0; i < bce_allowedColumnControls.length; i++) { 225 | 226 | var currentControl = bce_allowedColumnControls[i]; 227 | 228 | // skip non-existing elements 229 | if (!$(currentControl).length) { 230 | continue; 231 | } 232 | 233 | // get index of first checkbox in the first row 234 | var index = $(bce_adminDataTableSelector + ' ' + currentControl + ':eq(0)').parent().index(); 235 | 236 | // do the add 237 | $(bce_adminDataTableSelector + ' th:eq(' + index + ')').prepend($(bce_toggleControl)); 238 | 239 | // set initial checkbox states 240 | setColumnControlStates($(bce_adminDataTableSelector + ' th:eq(' + index + ') input')); 241 | 242 | // add event 243 | addColumnControlEvent(bce_adminDataTableSelector, currentControl, index); 244 | } 245 | 246 | // disable thead break to multiline 247 | $(bce_adminDataTableSelector + ' thead').css('white-space', 'nowrap'); 248 | 249 | bce_isColumnControlsAdded = true; 250 | 251 | return true; 252 | } 253 | 254 | 255 | /** 256 | * Add event on column toggle checkboxes. 257 | * 258 | * @param bce_adminDataTableSelector 259 | * @param currentControl 260 | * @param index 261 | */ 262 | function addColumnControlEvent(bce_adminDataTableSelector, currentControl, index) { 263 | 264 | var currentColumnControlSelector = bce_adminDataTableSelector + ' thead th:eq(' + index + ') .' + bce_columnControlClass; 265 | 266 | $(currentColumnControlSelector).on(bce_controlEventType, function () { 267 | 268 | var currentColumnControl = $(currentColumnControlSelector), 269 | toggleState = currentColumnControl.is(':checked'); 270 | 271 | $(bce_adminDataTableSelector + ' tbody tr').each(function () { 272 | 273 | var currentRow = $(this), 274 | currentItem = currentRow.find('td:eq(' + index + ') ' + currentControl); 275 | 276 | // toggle checkboxes state or trigger clicks 277 | if (currentItem.is('input')) { 278 | currentItem.prop('checked', toggleState); 279 | 280 | } else if (currentItem.is('i')) { 281 | if (toggleState) { 282 | if (!currentRow.hasClass(bce_deletedRowClass)) { 283 | currentItem.trigger('bce-delete-row'); 284 | } 285 | } else { 286 | if (currentRow.hasClass(bce_deletedRowClass)) { 287 | currentItem.trigger('bce-delete-row'); 288 | } 289 | } 290 | } 291 | }); 292 | }); 293 | } 294 | 295 | // End of adding toggle controls to column headers. 296 | 297 | 298 | $(document).on('click', '.batchChildTableEdit', batchChildTableDialog); 299 | 300 | var i = 0; 301 | $(document).on('click', 'button.InputfieldChildTableAddRow', function () { 302 | var c = 1; 303 | i++; 304 | var $table = $(this).closest('.Inputfield').find('table.bceEditTable'); 305 | var $tbody = $table.find('tbody:first'); 306 | var numRows = $tbody.children('tr').length; 307 | var $row = $tbody.children(":first").clone(true); 308 | var $titleCell = $row.find("td:eq(" + (c++) + ")").find(':input'); 309 | $titleCell.attr('placeholder', ''); // empty the title placeholder text 310 | $row.find("td:eq(" + (c++) + ")").html(''); //empty the name cell 311 | 312 | var colCount = 0; 313 | $row.find("td").each(function () { 314 | colCount++; 315 | }); 316 | if (colCount == 10) { 317 | $row.find("td:eq(" + (c++) + ")").find(':checkbox').prop('checked', false); //uncheck active checkbox 318 | } 319 | 320 | $row.find("td:eq(" + (c++) + ")").html($('#defaultTemplates').html()); //set template data 321 | $row.find("td:eq(" + (c++) + ")").find(':checkbox').prop('checked', false); //uncheck hidden checkbox 322 | $row.find("td:eq(" + (c++) + ")").find(':checkbox').prop('checked', false); //uncheck unpublished checkbox 323 | $row.find("td:eq(" + (c++) + ")").html(''); //empty the view button cell 324 | $row.find("td:eq(" + (c++) + ")").html(''); //empty the edit button cell 325 | $row.find("td:eq(" + (c++) + ")").html(''); //empty the delete button cell 326 | 327 | //in case the first row was set for deletion - the new row, cloned from this, would also be set for deletion, so need to remove class and restore opacity 328 | $row.removeClass('InputfieldChildTableRowDeleted'); 329 | $row.css('opacity', 1.0); 330 | 331 | $row.find(":input").each(function () { 332 | var $input = $(this); 333 | if ($($input).is("select")) { 334 | $input.attr("name", "templateId[new_" + i + "]"); 335 | } 336 | else if ($($input).hasClass('langActiveStatus')) { 337 | $input.attr("name", "langActiveStatus[new_" + i + "]"); 338 | } 339 | else if ($($input).hasClass('hiddenStatus')) { 340 | $input.attr("name", "hiddenStatus[new_" + i + "]"); 341 | } 342 | else if ($($input).hasClass('unpublishedStatus')) { 343 | $input.attr("name", "unpublishedStatus[new_" + i + "]"); 344 | } 345 | else if ($input.is('.InputfieldChildTableRowSort')) $input.val(numRows); 346 | else { 347 | $input.attr("name", "individualChildTitles[new_" + i + "]"); 348 | $input.attr('value', ''); 349 | $input.attr('id', ''); 350 | } 351 | }); 352 | 353 | $tbody.append($row); 354 | $table.show(); 355 | $titleCell.focus(); 356 | return false; 357 | }); 358 | 359 | // make rows sortable - trigger this on first ("one") mouseover of a sort handle in case BCE fieldset is being opened via AJAX 360 | $(document).one('mouseover', '.InputfieldChildTableRowSortHandle', function () { 361 | $("table.AdminDataTable").each(function () { 362 | batchChildTableSortable($(this)); 363 | }); 364 | }); 365 | 366 | // row deletion 367 | var deleteIds; 368 | $(document).on('click bce-delete-row', '.InputfieldChildTableRowDeleteLink', function () { 369 | var $row = $(this).closest('tr'); 370 | var $input = $('.InputfieldChildTableRowDelete'); 371 | 372 | if ($row.is('.InputfieldChildTableRowDeleted')) { 373 | // undelete 374 | $row.removeClass('InputfieldChildTableRowDeleted'); 375 | $row.css('opacity', 1.0); 376 | deleteIds = $input.val().replace($row.find("td:eq(1)").find("input").attr("id") + ',', ''); 377 | $input.val(deleteIds); 378 | 379 | } else { 380 | // delete 381 | $row.addClass('InputfieldChildTableRowDeleted'); 382 | $row.css('opacity', 0.3); 383 | deleteIds = $input.val() + $row.find("td:eq(1)").find("input").attr("id") + ','; 384 | $input.val(deleteIds); 385 | } 386 | 387 | setColumnControlStates($(this)); 388 | }); 389 | 390 | //Add or remove "Title" label from Text/Paste CSV textarea if user changes ignore first row setting 391 | $(document).on('change', '#Inputfield_userIgnoreFirstRow', function () { 392 | var initialAddText = $('textarea[name=childPagesAdd]').val(); 393 | var initialUpdateText = $('textarea[name=childPagesUpdate]').val(); 394 | var initialReplaceText = $('textarea[name=childPagesReplace]').val(); 395 | if ($(this).is(':checked')) { 396 | if ($('textarea[name=childPagesAdd]').length) $('textarea[name=childPagesAdd]').val("Title\n" + initialAddText); 397 | if ($('textarea[name=childPagesUpdate]').length) $('textarea[name=childPagesUpdate]').val("Title\n" + initialUpdateText); 398 | if ($('textarea[name=childPagesReplace]').length) $('textarea[name=childPagesReplace]').val("Title\n" + initialReplaceText); 399 | } 400 | else { 401 | if ($('textarea[name=childPagesAdd]').length) $('textarea[name=childPagesAdd]').val(removeFirstLine(initialAddText)); 402 | if ($('textarea[name=childPagesUpdate]').length) $('textarea[name=childPagesUpdate]').val(removeFirstLine(initialUpdateText)); 403 | if ($('textarea[name=childPagesReplace]').length) $('textarea[name=childPagesReplace]').val(removeFirstLine(initialReplaceText)); 404 | } 405 | }); 406 | 407 | }); 408 | 409 | function removeFirstLine(text) { 410 | // break the textblock into an array of lines 411 | var lines = text.split('\n'); 412 | // remove one line, starting at the first position 413 | lines.splice(0, 1); 414 | // join the array back into a single string 415 | return lines.join('\n'); 416 | } 417 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. -------------------------------------------------------------------------------- /ProcessChildrenCsvExport.module.php: -------------------------------------------------------------------------------- 1 | __('Process Children CSV Export'), 25 | 'version' => '1.8.31', 26 | 'summary' => __('Helper module for BatchChildEditor for creating CSV to export'), 27 | 'href' => 'http://modules.processwire.com/modules/batch-child-editor/', 28 | 'singular' => true, 29 | 'autoload' => false, 30 | 'permission' => 'batch-child-editor', 31 | 'requires' => 'BatchChildEditor', 32 | 'page' => array( 33 | 'name' => 'children-csv-export', 34 | 'parent' => 'setup', 35 | 'title' => 'Children CSV Export', 36 | 'status' => 'hidden' 37 | ) 38 | ); 39 | } 40 | 41 | /** 42 | * Name used for the page created in the admin 43 | * 44 | */ 45 | const adminPageName = 'children-csv-export'; 46 | 47 | 48 | 49 | /** 50 | * Initialize the module 51 | * 52 | */ 53 | public function init() { 54 | parent::init(); 55 | $this->addHook('Page::exportCsv', $this, 'exportCsv'); /* not limited to table-csv-export permission because only relevant to front-end */ 56 | } 57 | 58 | /** 59 | * Executed when root url for module is accessed 60 | * 61 | */ 62 | public function ___execute() { 63 | $this->exportCsv(); 64 | } 65 | 66 | 67 | public function outputCSV($data, $delimiter, $enclosure) { 68 | $output = fopen("php://output", "w"); 69 | foreach ($data as $row) { 70 | fputcsv($output, $row, $delimiter == "tab" ? chr(9) : $delimiter, $enclosure, '\\'); 71 | } 72 | fclose($output); 73 | } 74 | 75 | 76 | public function exportCsv($event = NULL) { 77 | 78 | $systemFields = wire('modules')->get("BatchChildEditor")->systemFields; 79 | 80 | $configSettings = wire('modules')->getModuleConfigData("BatchChildEditor"); 81 | 82 | $pp = !is_null($event) ? $event->object : wire('pages')->get((int) wire('input')->get->pid); 83 | $delimiter = !is_null($event) ? $event->arguments(0) : wire('input')->get->cs; 84 | $enclosure = !is_null($event) ? $event->arguments(1) : wire('input')->get->ce; 85 | $extension = !is_null($event) ? $event->arguments(2) : wire('input')->get->ext; 86 | $namesFirstRow = !is_null($event) ? $event->arguments(3) : wire('input')->get->nfr; 87 | $namesFirstRow = $namesFirstRow == 'checked' || $namesFirstRow == '1' ? true : false; 88 | $exportMultipleValuesSeparator = !is_null($event) ? $event->arguments(4) : wire('input')->get->mvs; 89 | $formatExport = !is_null($event) ? $event->arguments(5) : wire('input')->get->fe; 90 | $formatExport = $formatExport == 'checked' || $formatExport == 1 ? true : false; 91 | $pagesToInclude = !is_null($event) ? $event->arguments(6) : wire('input')->get->pti; 92 | $fieldNames = !is_null($event) ? $event->arguments(7) : explode(',', wire('input')->get->fns); 93 | 94 | //if settings not supplied, use defaults from page or module config settings 95 | $currentData = isset($configSettings['configurablePages']) && in_array($pp->id, $configSettings['configurablePages']) && isset($configSettings['pageSettings'][$pp->id]) && $configSettings['pageSettings'][$pp->id] ? $configSettings['pageSettings'][$pp->id] : $configSettings; 96 | 97 | if(isset($currentData['parentPage']) && $currentData['parentPage']) $pp = wire('pages')->get($currentData['parentPage']); 98 | 99 | $delimiter = $delimiter ? $delimiter : $currentData['csvExportFieldSeparator']; 100 | $enclosure = $enclosure ? $enclosure : $currentData['csvExportFieldEnclosure']; 101 | $extension = $extension ? $extension : $currentData['csvExportExtension']; 102 | $namesFirstRow = isset($namesFirstRow) ? $namesFirstRow : $currentData['columnsFirstRow']; 103 | $exportMultipleValuesSeparator = $exportMultipleValuesSeparator ? $exportMultipleValuesSeparator : $currentData['exportMultipleValuesSeparator']; 104 | if($exportMultipleValuesSeparator == '\r') $exportMultipleValuesSeparator = chr(13); 105 | if($exportMultipleValuesSeparator == '\n') $exportMultipleValuesSeparator = chr(10); 106 | $formatExport = isset($formatExport) ? $formatExport : $currentData['formatExport']; 107 | if(!$pagesToInclude) { 108 | $pagesToInclude = isset($currentData['pagesToInclude']) ? $currentData['pagesToInclude'] : ''; 109 | } 110 | $fieldNames = $fieldNames ? $fieldNames : $currentData['exportFields']; 111 | 112 | if($fieldNames[0] == 'undefined') { 113 | $fieldNames = array(); 114 | // if fields not defined, then get list from first child 115 | foreach ($pp->child()->fields as $f) $fieldNames[] = $f->name; 116 | } 117 | 118 | $csv = array(); 119 | $i=0; 120 | $children = $pagesToInclude != '' && $pagesToInclude != 'undefined' ? $pp->children($pagesToInclude) : $pp->children(); 121 | foreach($children as $p) { 122 | 123 | $p->of($formatExport); //needed to have fields formatted in the CSV 124 | 125 | //Names in First Row 126 | if($i==0 && $namesFirstRow == true) { 127 | foreach($fieldNames as $fieldName) { 128 | 129 | //exclude unsupported field types 130 | if(wire('fields')->$fieldName && (wire('fields')->$fieldName->type == 'FieldtypeTable' || 131 | wire('fields')->$fieldName->type == 'FieldtypeRepeater' || 132 | wire('fields')->$fieldName->type == 'FieldtypePageTable' || 133 | wire('fields')->$fieldName->type == 'FieldsetOpen' || 134 | wire('fields')->$fieldName->type == 'FieldsetClose' || 135 | wire('fields')->$fieldName->type == 'FieldsetTabOpen' || 136 | wire('fields')->$fieldName->type == 'FieldsetTabClose' 137 | )) continue; 138 | 139 | //FieldtypeTextareas 140 | if(wire('fields')->$fieldName && wire('fields')->$fieldName->type == 'FieldtypeTextareas') { 141 | $field = wire('fields')->get($fieldName); 142 | $subfields = $field->type->getBlankValue(new Page(), $field); 143 | foreach($subfields as $subFieldName => $value) { 144 | $csv[$i][] = $field->type->getLabel($field, $subFieldName) ? $field->type->getLabel($field, $subFieldName) : $subFieldName; 145 | } 146 | } 147 | /* 148 | //Repeaters 149 | elseif(wire('fields')->$fieldName && wire('fields')->$fieldName->type == 'FieldtypeRepeater') { 150 | $field = wire('fields')->get($fieldName); 151 | $subfields = $field->repeaterFields; 152 | foreach($subfields as $sf) { 153 | $subField = wire('fields')->get($sf); 154 | $csv[$i][] = $subField->label ? $subField->label : $subField->name; 155 | } 156 | } 157 | */ 158 | //All other fieldtypes 159 | elseif(isset($fieldName) && $fieldName !== 'undefined') { 160 | if(array_key_exists($fieldName, $systemFields)) { 161 | $fieldLabel = $systemFields[$fieldName]; 162 | } 163 | else { 164 | $label = wire('fields')->get($fieldName)->label; 165 | $fieldLabel = $label ? $label : $fieldName; 166 | } 167 | $csv[$i][] = $fieldLabel; 168 | } 169 | } 170 | } 171 | 172 | //All Data Rows 173 | $i++; 174 | foreach($fieldNames as $fieldName) { 175 | 176 | $formattedValue = ''; 177 | 178 | if($fieldName == 'created_formatted' || $fieldName == 'modified_formatted' || $fieldName == 'published_formatted') { 179 | $dateFieldName = str_replace('_formatted', '', $fieldName); 180 | $formattedValue = date('Y-m-d H:i:s', $p->$dateFieldName); 181 | } 182 | //exclude unsupported field types 183 | elseif(wire('fields')->$fieldName && (wire('fields')->$fieldName->type == 'FieldtypeTable' || 184 | wire('fields')->$fieldName->type == 'FieldtypeRepeater' || 185 | wire('fields')->$fieldName->type == 'FieldtypePageTable' || 186 | wire('fields')->$fieldName->type == 'FieldsetOpen' || 187 | wire('fields')->$fieldName->type == 'FieldsetClose' || 188 | wire('fields')->$fieldName->type == 'FieldsetTabOpen' || 189 | wire('fields')->$fieldName->type == 'FieldsetTabClose' 190 | )) { 191 | continue; 192 | } 193 | elseif(!$p->$fieldName) { 194 | $formattedValue = ''; 195 | } 196 | //FieldtypeTextareas 197 | elseif(wire('fields')->$fieldName && wire('fields')->$fieldName->type == 'FieldtypeTextareas') { 198 | $field = wire('fields')->get($fieldName); 199 | $subfields = $field->type->getBlankValue(new Page(), $field); 200 | foreach($subfields as $subFieldName => $value) { 201 | $csv[$i][] = $p->$fieldName->$subFieldName; 202 | } 203 | } 204 | /* 205 | //FieldtypeRepeaters 206 | elseif(wire('fields')->$fieldName && wire('fields')->$fieldName->type == 'FieldtypeRepeater') { 207 | foreach($p->$fieldName as $item) { 208 | foreach($item->fields as $subField) { 209 | $csv[$i][] = $item->$subField; 210 | } 211 | } 212 | } 213 | */ 214 | //Page fields 215 | elseif(wire('fields')->$fieldName && wire('fields')->$fieldName->type instanceof FieldtypePage) { 216 | if(method_exists($p->$fieldName,'implode')) { 217 | if($p->$fieldName->implode($exportMultipleValuesSeparator, 'title')) { // title available 218 | $formattedValue = $p->$fieldName->implode($exportMultipleValuesSeparator, 'title'); 219 | } 220 | else { // no title so use name - eg a page field selecting from the user template 221 | $formattedValue = $p->$fieldName->implode($exportMultipleValuesSeparator, 'name'); 222 | } 223 | } 224 | else { 225 | $formattedValue = $p->$fieldName->title ? $p->$fieldName->title : $p->$fieldName->name; 226 | } 227 | } 228 | //FieldtypeMultiplier and FieldtypeFile 229 | elseif(wire('fields')->$fieldName && (wire('fields')->$fieldName->type == 'FieldtypeMultiplier' || wire('fields')->$fieldName->type == 'FieldtypeOptions' || wire('fields')->$fieldName->type instanceof FieldtypeFile)) { 230 | if(wire('fields')->$fieldName->type instanceof FieldtypeFile) $p->of(false); //formatting off required if output format is "Rendered string of text" 231 | if(count($p->$fieldName)>0) { 232 | $values = array(); 233 | foreach($p->$fieldName as $value) { 234 | if(wire('fields')->$fieldName->type instanceof FieldtypeFile) { 235 | $values[] = $value->filename; 236 | } 237 | elseif($formatExport && wire('fields')->$fieldName->type == 'FieldtypeOptions') { 238 | $values[] = $value->title; 239 | } 240 | else { 241 | $values[] = $value; 242 | } 243 | } 244 | $formattedValue = implode($exportMultipleValuesSeparator, $values); 245 | } 246 | $p->of($formatExport); 247 | } 248 | elseif(wire('fields')->$fieldName && wire('fields')->$fieldName->type == "FieldtypeMapMarker") { 249 | foreach(array('address', 'lat', 'lng', 'zoom', 'status') as $subFieldName) { 250 | $values[] = $p->$fieldName->$subFieldName; 251 | } 252 | $formattedValue = implode($exportMultipleValuesSeparator, $values); 253 | } 254 | elseif(wire('fields')->$fieldName && wire('fields')->$fieldName->type instanceof FieldtypeMulti && count($p->$fieldName) === 0) { 255 | $formattedValue = ''; 256 | } 257 | //All other fields 258 | else { 259 | $formattedValue = $p->$fieldName; 260 | } 261 | 262 | //Populate $csv array for all fields that don't have subfields and are therefore already populated, like FieldtypeTextareas 263 | if(array_key_exists($fieldName, $systemFields) || (wire('fields')->$fieldName && wire('fields')->$fieldName->type != 'FieldtypeTextareas')) { 264 | $csv[$i][] = $formattedValue; 265 | } 266 | 267 | } 268 | 269 | } 270 | 271 | header("Content-type: text/csv"); 272 | header("Content-Disposition: attachment; filename=".$pp->name .".".$extension); 273 | header("Pragma: no-cache"); 274 | header("Expires: 0"); 275 | 276 | $this->outputCSV($csv, $delimiter, $enclosure); 277 | exit; 278 | 279 | } 280 | 281 | } 282 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | BatchChildEditor 2 | ================ 3 | 4 | Processwire module for quick batch creation (titles only or CSV import for other fields), editing, sorting, deletion, and CSV export of all children under a given page. 5 | 6 | This module adds a variety of importing, editing, and exporting tools. The interface can be added to the Children Tab, or in a new dedicated tab, or placed inline with other fields in the Content tab. 7 | 8 | #### Modes 9 | 1. Lister - Embeds a customized Lister interface. Installation of ListerPro will allow inline ajax editing of displayed fields. 10 | 2. Edit - Allows you to quidkly rename existing child pages, add new child pages, sort, and delete pages. It also has a modal edit link from the page name to allow easy access to edit all the fields on the page. 11 | 3. Add - Adds newly entered page titles as child pages to the list of existing siblings. You could create a list of pages in Word or whatever and just paste them in here and viola! 12 | 4. Update - This allows updating of existing pages - the title, name and all other fields. 13 | 5. Replace - This completely replaces all existing child pages with new pages. There are checks that prevent this method working if there are any child pages with their own children or other content fields that are not empty. This check can be disabled in the module config settings, but please be very careful with this. 14 | 6. Export to CSV - Generates a CSV file containing the fields for all child pages. Fields to be exported can be fixed or customized by the user. Also includes an API export method. 15 | 16 | In Add, Update, and Replace modes you can enter CSV formatted rows to populate all text/numeric fields. This can be used to create new pages or to update existing pages. CSV field pairings can be defined to make it easy for editors to periodically create new pages, or update the fields in existing pages. 17 | 18 | There is also an exportCsv() API method that can be used like this: 19 | ``` 20 | get->csv_export==1){ 23 | $modules->get('ProcessChildrenCsvExport'); // load module 24 | // delimiter, enclosure, file extension, names in first row, multiple field separator, format values, pages to include (selector string), array of field names 25 | $page->exportCsv(',', '"', 'csv', true, "\r", true, 'include=all', array('title','body','images','textareas')); 26 | //$page->exportCsv() - this version uses the defaults from the module or page specific settings 27 | } 28 | // display content of template with link to same page with appended csv_export=1 29 | else{ 30 | include("./head.inc"); 31 | 32 | echo "Export Child Pages as CSV"; //link to initiate export 33 | 34 | include("./foot.inc"); 35 | } 36 | ``` 37 | 38 | 39 | ### Access permission 40 | 41 | This module requires a new permission: "batch-child-editor". This permission is created automatically on install and is added to the superuser role, but it is up to the developer to add the permission to other roles as required. 42 | 43 | 44 | ### Config Settings 45 | 46 | There are module-wide config settings, but these can be overwritten with page specific permissions which allows for highly customized creation and editing tools. 47 | 48 | * Which pages and templates will have the editor available and which can be separately configured. 49 | * Which edit modes should be availble to the user. 50 | * Alternate parent page - allows editing of external page tree. 51 | * Which data entry options (Text, Upload, URL link) should be availble to the user. 52 | * CSV import options. 53 | * CSV field pairings - really powerful for creating and updating pages - read more about it in the config settings. 54 | * CSV export options. 55 | * Whether the name of the page should also be changed along with the title. This is a very important setting and should be considered carefully, especially is the child pages are URL accessible. 56 | * Whether users can decide whether the name is also changed or not. 57 | * Whether to disable content protection for existing child pages and their children. 58 | * Trash or Delete. 59 | * Load Batch interface Open or Collapsed (open for quicker access). 60 | * Position interface (top, bottom, replace, new tab, inline fieldset). 61 | * Custom Title, Description, and Notes for each mode - allows you to tailor the editing interface specifically to your content editors and to specific content. 62 | 63 | #### Support forum: 64 | https://processwire.com/talk/topic/6102-batch-child-editor/ 65 | 66 | 67 | ## License 68 | 69 | This program is free software; you can redistribute it and/or 70 | modify it under the terms of the GNU General Public License 71 | as published by the Free Software Foundation; either version 2 72 | of the License, or (at your option) any later version. 73 | 74 | This program is distributed in the hope that it will be useful, 75 | but WITHOUT ANY WARRANTY; without even the implied warranty of 76 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 77 | GNU General Public License for more details. 78 | 79 | You should have received a copy of the GNU General Public License 80 | along with this program; if not, write to the Free Software 81 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 82 | 83 | (See included LICENSE file for full license text.) 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /parsecsv-for-php/.editorconfig: -------------------------------------------------------------------------------- 1 | # @see http://editorconfig.org/ 2 | 3 | # This is the top-most .editorconfig file; do not search in parent directories. 4 | root = true 5 | 6 | # All files. 7 | [*] 8 | indent_style = space 9 | indent_size = 4 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [composer.json] 15 | indent_size = 4 16 | 17 | [.travis.yml] 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /parsecsv-for-php/.gitignore: -------------------------------------------------------------------------------- 1 | *.bak 2 | /.idea 3 | /phive.xml 4 | /tools 5 | composer.lock 6 | vendor/ 7 | -------------------------------------------------------------------------------- /parsecsv-for-php/.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: php 3 | dist: trusty 4 | 5 | php: 6 | - '7.4' 7 | - '7.3' 8 | - '7.2' 9 | - '7.1' 10 | - '7.0' 11 | - '5.6' 12 | - '5.5' 13 | 14 | before_install: 15 | - composer update 16 | 17 | script: 18 | - composer validate 19 | - vendor/bin/phpunit --version 20 | - vendor/bin/phpunit --configuration tests/phpunit.xml 21 | 22 | notifications: 23 | email: 24 | recipients: 25 | - will.knauss@gmail.com 26 | on_success: never 27 | on_failure: always 28 | -------------------------------------------------------------------------------- /parsecsv-for-php/ChangeLog.txt: -------------------------------------------------------------------------------- 1 | ParseCSV 1.2.0 2 | ----------------------------------- 3 | Date: 07-Jan-2020 4 | 5 | Breaking changes: none 6 | 7 | New features: 8 | - Compatible with PHP 7.4. Thanks to @andreybolonin 9 | @morrislaptop @martijnengler and @fjf2002. 10 | - unparse() now also understands $use_mb_convert_encoding. 11 | - Verbal condition operators are now allowed to contain 12 | upper case letters, for example: 13 | $csv->conditions = 'rating IS GREATER THAN 4'; 14 | 15 | Bug fixes: 16 | - All filter condition operators containing "is" or "equals" 17 | were broken. 18 | 19 | Code quality: 20 | - Improved test coverage. 21 | ----------------------------------- 22 | 23 | 24 | ParseCSV 1.1.1 25 | ----------------------------------- 26 | Date: 2-Feb-2019 27 | 28 | Breaking changes: none 29 | 30 | New features: none 31 | 32 | Bug fixes: 33 | - Function load_data: check length of input, prevents E_NOTICE 34 | if too long. 35 | - Fixed bugs in unparse(). 36 | 37 | Code quality: 38 | - Improved test coverage. 39 | ----------------------------------- 40 | 41 | 42 | ParseCSV 1.1.0 43 | ----------------------------------- 44 | Date: 9-Aug-2018 45 | 46 | Breaking changes: 47 | - Ignore entirely empty lines at the end of files 48 | See https://github.com/parsecsv/parsecsv-for-php/pull/142 49 | - Dropped support for PHP 5.4. Now, you need at leas PHP 5.5. 50 | - Fixed parse()'s return value: return true only if $data is useful. 51 | 52 | New features: 53 | - Added support for Laravel-style collections via the 54 | new getCollection() function - see 55 | https://github.com/parsecsv/parsecsv-for-php/pull/134 56 | - New function getTotalDataRowCount() - useful if 57 | $limit is set - see pull request #122. 58 | - Added requires to keep Composer-free environments working. 59 | 60 | Bug fixes: 61 | - Better support for streams. 62 | See https://github.com/parsecsv/parsecsv-for-php/pull/147 63 | - Fixed output() with custom header. 64 | See https://github.com/parsecsv/parsecsv-for-php/issues/132 65 | - Fixed bug on _validate_fields_for_unparse() if titles property 66 | is used instead of fields parameter for changing the titles for 67 | unparsing. 68 | - Fixed bug in unparse() that caused incorrect column order 69 | (Issue #41). 70 | 71 | 72 | Code quality: 73 | - Improved test coverage. 74 | ----------------------------------- 75 | 76 | 77 | ParseCSV 1.0.0 78 | ----------------------------------- 79 | Date: 3-March-2018 80 | 81 | - Renamed class from parseCSV to Csv and added name- 82 | space "ParseCsv" for PSR compliance. 83 | 84 | - Added support for MS Excel's "sep=" to detect the 85 | delimiter (Issue #60). 86 | 87 | - Added data type detection - function getDatatypes() 88 | guesses the type of each column. 89 | 90 | - MIME: output() sends correct MIME type to browser 91 | if the separator is a tab char (Issue #79). 92 | 93 | - Added support for mb_convert_encoding() instead of 94 | iconv() - see issue #109. 95 | 96 | - A number of minor bug fixes - see GitHub issues. 97 | 98 | - Added many more unit tests. 99 | 100 | ----------------------------------- 101 | 102 | 103 | parseCSV 0.4.3 beta 104 | ----------------------------------- 105 | Date: 1-July-2008 106 | 107 | - Issue #4. Added an option for setting sorting 108 | type behavior when sorting data. 109 | Simply set $csv->sort_type to "regular", "numeric", 110 | or "string". 111 | 112 | - Issue #6. Raw loaded file data is now cleared from 113 | file_data property when it has been successfully 114 | parsed to keep parseCSV's memory footprint to a 115 | minimum. Specifically handy when using multiple 116 | instances of parseCSV to process large files. 117 | 118 | ----------------------------------- 119 | 120 | 121 | parseCSV 0.4.2 beta 122 | ----------------------------------- 123 | Date: 31-May-2008 124 | 125 | - IMPORTANT! If you're using the output(), 126 | method please note that the first parameter 127 | has been completely removed as it was 128 | technically just useless. Instead, the second 129 | parameter (filename) doubles as its replacement. 130 | Simply put, if filename is not set or null, the 131 | output() method will not output a downloadable 132 | file. Please update your existing code 133 | when using 0.4.2 and later :) 134 | 135 | - Small fix to the headers sent by the output() 136 | method. 137 | 138 | - Added a download example using the output() 139 | method to the examples folder. 140 | 141 | ----------------------------------- 142 | 143 | 144 | parseCSV 0.4.1 beta 145 | ----------------------------------- 146 | Date: 29-May-2008 147 | 148 | - Fixed a small bug in how the output() method 149 | handles input data. 150 | 151 | ----------------------------------- 152 | 153 | 154 | parseCSV 0.4 beta 155 | ----------------------------------- 156 | Date: 11-Apr-2008 157 | 158 | - Error reporting for files/data which is corrupt 159 | or has formatting errors like using double 160 | quotes in a field without enclosing quotes. Or 161 | not escaping double quotes with a second one. 162 | 163 | - parse() method does not require input anymore 164 | if the "$object->file" property has been set. 165 | 166 | I'm calling this a beta release due to the heavy 167 | modifications to the core parsing logic required 168 | for error reporting to work. I have tested the 169 | new code quite extensively, I'm fairly confident 170 | that it still parses exactly as it always has. 171 | 172 | The second reason I'm calling it a beta release 173 | is cause I'm sure the error reporting code will 174 | need more refinements and tweaks to detect more 175 | types of errors, as it's only picking two types 176 | or syntax errors right now. However, it seems 177 | these two are the most common errors that you 178 | would be likely to come across. 179 | 180 | ----------------------------------- 181 | 182 | 183 | parseCSV 0.3.2 184 | ----------------------------------- 185 | Date: 1-Apr-2008 186 | 187 | This is primarily a bug-fix release for a critical 188 | bug which was brought to my attention. 189 | 190 | - Fixed a critical bug in conditions parsing which 191 | would generate corrupt matching patterns causing 192 | the condition(s) to not work at all in some 193 | situations. 194 | 195 | - Fixed a small code error which would cause PHP to 196 | generate a invalid offset notice when zero length 197 | values were fed into the unparse() method to 198 | generate CSV data from an array. 199 | 200 | Notice: If you have been using the "parsecsv-stable" 201 | branch as an external in any of your projects, 202 | please use the "stable/parsecsv" branch from this 203 | point on as I will eventually remove the former due 204 | to it's stupid naming. 205 | 206 | ----------------------------------- 207 | 208 | 209 | parseCSV 0.3.1 210 | ----------------------------------- 211 | Date: 1-Sep-2007 212 | 213 | - Small change to default output settings to 214 | conform with RFC 4180 (http://rfc.net/rfc4180.html). 215 | Only the LF (line feed) character was used 216 | by default to separate rows, rather than 217 | CRLF (carriage return & line feed). 218 | 219 | ----------------------------------- 220 | 221 | 222 | parseCSV 0.3.0 223 | ----------------------------------- 224 | Date: 9-Aug-2007 225 | 226 | - Changed to the MIT license. 227 | 228 | - Added offset and limit options. 229 | 230 | - Added SQL-like conditions for quickly 231 | filtering out entries. Documentation on the 232 | condition syntax is forthcoming. 233 | 234 | - Small parsing modification to comply 235 | with some recent changes to the specifications 236 | outlined on Wikipedia's Comma-separated values 237 | article. 238 | 239 | - Minor changes and optimizations, and a few 240 | spelling corrections. Oops :) 241 | 242 | - Included more complex code examples in the 243 | parseCSV download. 244 | 245 | ----------------------------------- 246 | 247 | 248 | parseCSV 0.2.1 249 | ----------------------------------- 250 | Date: 8-Aug-2007 251 | 252 | - Fixed stupid code which caused auto function 253 | to not work in some situations. 254 | 255 | ----------------------------------- 256 | 257 | 258 | parseCSV 0.2.0 beta 259 | ----------------------------------- 260 | Date: 2-Jan-2007 261 | 262 | - Added auto() function to automatically detect 263 | delimiter character. 264 | Useful for user upload in case delimiter is 265 | comma (,), tab, or semi-colon (;). Some 266 | versions of MS Excel for Windows use 267 | semi-colons instead of commas when saving to 268 | CSV files. 269 | It uses a process of elimination to eliminate 270 | characters that can not be the delimiter, 271 | so it should work on all CSV-structured files 272 | almost no matter what the delimiter is. 273 | 274 | - Generally updated some of the core workings 275 | to increase performance, and offer better 276 | support for large (1MB and up) files. 277 | 278 | - Added code examples to header comment. 279 | 280 | ----------------------------------- 281 | 282 | 283 | parseCSV 0.1.6 beta 284 | ----------------------------------- 285 | Date: 22-Dec-2006 286 | 287 | - Updated output() function. 288 | 289 | ----------------------------------- 290 | 291 | 292 | parseCSV 0.1.5 beta 293 | ----------------------------------- 294 | Date: 22-Dec-2006 295 | 296 | - Added output() function for easy output to 297 | browser, for downloading features for example. 298 | 299 | ----------------------------------- 300 | 301 | 302 | parseCSV 0.1.4 beta 303 | ----------------------------------- 304 | Date: 17-Dec-2006 305 | 306 | - Minor changes and fixes 307 | 308 | ----------------------------------- 309 | 310 | 311 | parseCSV 0.1.3 beta 312 | ----------------------------------- 313 | Date: 17-Dec-2006 314 | 315 | - Added GPL v2.0 license. 316 | 317 | ----------------------------------- 318 | 319 | 320 | parseCSV 0.1.2 beta 321 | ----------------------------------- 322 | Date: 17-Dec-2006 323 | 324 | - Added encoding() function for easier character 325 | encoding configuration. 326 | 327 | ----------------------------------- 328 | 329 | 330 | parseCSV 0.1.1 beta 331 | ----------------------------------- 332 | Date: 24-Nov-2006 333 | 334 | - Added support for a PHP die command on first 335 | line of csv files if they have a .php extension 336 | to protect secure data from being displayed 337 | directly to the browser. 338 | 339 | ----------------------------------- 340 | 341 | 342 | parseCSV 0.1 beta 343 | ----------------------------------- 344 | Date: 23-Nov-2006 345 | 346 | - Initial release 347 | 348 | ----------------------------------- 349 | -------------------------------------------------------------------------------- /parsecsv-for-php/License.txt: -------------------------------------------------------------------------------- 1 | (The MIT license) 2 | 3 | Copyright (c) 2014 Jim Myhrberg. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /parsecsv-for-php/Makefile: -------------------------------------------------------------------------------- 1 | COMPOSER_BIN_DIR := vendor/bin 2 | PHPUNIT_ARGS = -c tests/phpunit.xml 3 | 4 | test: phpunit-dep 5 | ${COMPOSER_BIN_DIR}/phpunit ${PHPUNIT_ARGS} 6 | 7 | phpunit-dep: 8 | test -f ${COMPOSER_BIN_DIR}/phpunit || ( \ 9 | echo "phpunit is required to run tests." \ 10 | "Please run: composer install" >&2 && \ 11 | exit 1 \ 12 | ) 13 | 14 | .SILENT: 15 | .PHONY: test phpunit-dep 16 | -------------------------------------------------------------------------------- /parsecsv-for-php/README.md: -------------------------------------------------------------------------------- 1 | # ParseCsv 2 | [![Financial Contributors on Open Collective](https://opencollective.com/parsecsv/all/badge.svg?label=financial+contributors)](https://opencollective.com/parsecsv) 3 | 4 | ParseCsv is an easy-to-use PHP class that reads and writes CSV data properly. It 5 | fully conforms to the specifications outlined on the on the 6 | [Wikipedia article][CSV] (and thus RFC 4180). It has many advanced features which help make your 7 | life easier when dealing with CSV data. 8 | 9 | You may not need a library at all: before using ParseCsv, please make sure if PHP's own `str_getcsv()`, ``fgetcvs()`` or `fputcsv()` meets your needs. 10 | 11 | This library was originally created in early 2007 by [jimeh](https://github.com/jimeh) due to the lack of built-in 12 | and third-party support for handling CSV data in PHP. 13 | 14 | [csv]: http://en.wikipedia.org/wiki/Comma-separated_values 15 | 16 | ## Features 17 | 18 | * ParseCsv is a complete and fully featured CSV solution for PHP 19 | * Supports enclosed values, enclosed commas, double quotes and new lines. 20 | * Automatic delimiter character detection. 21 | * Sort data by specific fields/columns. 22 | * Easy data manipulation. 23 | * Basic SQL-like _conditions_, _offset_ and _limit_ options for filtering 24 | data. 25 | * Error detection for incorrectly formatted input. It attempts to be 26 | intelligent, but can not be trusted 100% due to the structure of CSV, and 27 | how different programs like Excel for example outputs CSV data. 28 | * Support for character encoding conversion using PHP's 29 | `iconv()` and `mb_convert_encoding()` functions. 30 | * Supports PHP 5.5 and higher. 31 | It certainly works with PHP 7.2 and all versions in between. 32 | 33 | ## Installation 34 | 35 | Installation is easy using Composer. Just run the following on the 36 | command line: 37 | ``` 38 | composer require parsecsv/php-parsecsv 39 | ``` 40 | 41 | If you don't use a framework such as Drupal, Laravel, Symfony, Yii etc., 42 | you may have to manually include Composer's autoloader file in your PHP 43 | script: 44 | ```php 45 | require_once __DIR__ . '/vendor/autoload.php'; 46 | ``` 47 | 48 | #### Without composer 49 | Not recommended, but technically possible: you can also clone the 50 | repository or extract the 51 | [ZIP](https://github.com/parsecsv/parsecsv-for-php/archive/master.zip). 52 | To use ParseCSV, you then have to add a `require 'parsecsv.lib.php';` line. 53 | 54 | ## Example Usage 55 | 56 | **General** 57 | 58 | ```php 59 | $csv = new ParseCsv\Csv('data.csv'); 60 | print_r($csv->data); 61 | ``` 62 | 63 | **Tab delimited, and encoding conversion** 64 | 65 | ```php 66 | $csv = new ParseCsv\Csv(); 67 | $csv->encoding('UTF-16', 'UTF-8'); 68 | $csv->delimiter = "\t"; 69 | $csv->parse('data.tsv'); 70 | print_r($csv->data); 71 | ``` 72 | 73 | **Auto-detect delimiter character** 74 | 75 | ```php 76 | $csv = new ParseCsv\Csv(); 77 | $csv->auto('data.csv'); 78 | print_r($csv->data); 79 | ``` 80 | 81 | **Parse data with offset** 82 | * ignoring the first X (e.g. two) rows 83 | ```php 84 | $csv = new ParseCsv\Csv(); 85 | $csv->offset = 2; 86 | $csv->parse('data.csv'); 87 | print_r($csv->data); 88 | ``` 89 | 90 | **Limit the number of returned data rows** 91 | ```php 92 | $csv = new ParseCsv\Csv(); 93 | $csv->limit = 5; 94 | $csv->parse('data.csv'); 95 | print_r($csv->data); 96 | ``` 97 | 98 | **Get total number of data rows without parsing whole data** 99 | * Excluding heading line if present (see $csv->header property) 100 | ```php 101 | $csv = new ParseCsv\Csv(); 102 | $csv->load_data('data.csv'); 103 | $count = $csv->getTotalDataRowCount(); 104 | print_r($count); 105 | ``` 106 | 107 | **Get most common data type for each column (Requires PHP >= 5.5)** 108 | 109 | ```php 110 | $csv = new ParseCsv\Csv('data.csv'); 111 | $csv->getDatatypes() 112 | print_r($csv->data_types); 113 | ``` 114 | 115 | **Modify data in a CSV file** 116 | 117 | Change data values: 118 | ```php 119 | $csv = new ParseCsv\Csv(); 120 | $csv->sort_by = 'id'; 121 | $csv->parse('data.csv'); 122 | # "4" is the value of the "id" column of the CSV row 123 | $csv->data[4] = array('firstname' => 'John', 'lastname' => 'Doe', 'email' => 'john@doe.com'); 124 | $csv->save(); 125 | ``` 126 | 127 | Enclose each data value by quotes: 128 | ```php 129 | $csv = new ParseCsv\Csv(); 130 | $csv->parse('data.csv'); 131 | $csv->enclose_all = true; 132 | $csv->save(); 133 | ``` 134 | 135 | **Replace field names or set ones if missing** 136 | 137 | ```php 138 | $csv = new ParseCsv\Csv(); 139 | $csv->fields = ['id', 'name', 'category'] 140 | $csv->parse('data.csv'); 141 | ``` 142 | 143 | **Add row/entry to end of CSV file** 144 | 145 | _Only recommended when you know the exact structure of the file._ 146 | 147 | ```php 148 | $csv = new ParseCsv\Csv(); 149 | $csv->save('data.csv', array(array('1986', 'Home', 'Nowhere', '')), true); 150 | ``` 151 | 152 | **Convert 2D array to CSV data and send headers to browser to treat output as 153 | a file and download it** 154 | 155 | ```php 156 | $csv = new ParseCsv\Csv(); 157 | $csv->output('movies.csv', $array, array('field 1', 'field 2'), ','); 158 | ``` 159 | 160 | For more complex examples, see the ``tests`` and `examples` directories. 161 | 162 | ## Credits 163 | 164 | * ParseCsv is based on the concept of [Ming Hong Ng][ming]'s [CsvFileParser][] 165 | class. 166 | 167 | [ming]: http://minghong.blogspot.com/ 168 | [CsvFileParser]: http://minghong.blogspot.com/2006/07/csv-parser-for-php.html 169 | 170 | 171 | ## Contributors 172 | 173 | Please find a complete list on the project's [contributors][] page. 174 | 175 | [contributors]: https://github.com/parsecsv/parsecsv-for-php/graphs/contributors 176 | 177 | 178 | 179 | ## Contributors 180 | 181 | ### Code Contributors 182 | 183 | This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. 184 | 185 | 186 | ### Financial Contributors 187 | 188 | Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/parsecsv/contribute)] 189 | 190 | #### Individuals 191 | 192 | 193 | 194 | #### Organizations 195 | 196 | Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/parsecsv/contribute)] 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | ## License 210 | 211 | (The MIT license) 212 | 213 | Copyright (c) 2014 Jim Myhrberg. 214 | 215 | Permission is hereby granted, free of charge, to any person obtaining a copy 216 | of this software and associated documentation files (the "Software"), to deal 217 | in the Software without restriction, including without limitation the rights 218 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 219 | copies of the Software, and to permit persons to whom the Software is 220 | furnished to do so, subject to the following conditions: 221 | 222 | The above copyright notice and this permission notice shall be included in all 223 | copies or substantial portions of the Software. 224 | 225 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 226 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 227 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 228 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 229 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 230 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 231 | SOFTWARE. 232 | 233 | [![Build Status](https://travis-ci.org/parsecsv/parsecsv-for-php.svg?branch=master)](https://travis-ci.org/parsecsv/parsecsv-for-php) 234 | -------------------------------------------------------------------------------- /parsecsv-for-php/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parsecsv/php-parsecsv", 3 | "description": "CSV data parser for PHP", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Jim Myhrberg", 8 | "email": "contact@jimeh.me" 9 | }, 10 | { 11 | "name": "William Knauss", 12 | "email": "will.knauss@gmail.com" 13 | }, 14 | { 15 | "name": "Susann Sgorzaly", 16 | "homepage": "https://github.com/susgo" 17 | }, 18 | { 19 | "name": "Christian Bläul", 20 | "homepage": "https://github.com/Fonata" 21 | } 22 | ], 23 | "autoload": { 24 | "psr-4": { 25 | "ParseCsv\\": "src" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "ParseCsv\\tests\\": "tests" 31 | } 32 | }, 33 | "require": { 34 | "php": ">=5.5" 35 | }, 36 | "require-dev": { 37 | "phpunit/phpunit": "4.1.*" 38 | }, 39 | "suggest": { 40 | "illuminate/support": "Fluent array interface for map functions" 41 | }, 42 | "extra": { 43 | "branch-alias": { 44 | "dev-master": "1.0.x-dev" 45 | } 46 | }, 47 | "support": { 48 | "issues": "https://github.com/parsecsv/parsecsv-for-php/issues", 49 | "source": "https://github.com/parsecsv/parsecsv-for-php" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /parsecsv-for-php/examples/_books.csv: -------------------------------------------------------------------------------- 1 | rating,title,author,type,asin,tags,review 2 | 0,The Killing Kind,John Connolly,Book,0340771224,,i still haven't had time to read this one... 3 | 0,The Third Secret,Steve Berry,Book,0340899263,,need to find time to read this book 4 | 3,The Last Templar,Raymond Khoury,Book,0752880705,, 5 | 5,The Traveller,John Twelve Hawks,Book,059305430X,, 6 | 4,Crisis Four,Andy Mcnab,Book,0345428080,, 7 | 5,Prey,Michael Crichton,Book,0007154534,, 8 | 3,The Broker (Paperback),John Grisham,Book,0440241588,book johngrisham,"good book, but is slow in the middle" 9 | 3,Without Blood (Paperback),Alessandro Baricco,Book,1841955744,, 10 | 5,State of Fear (Paperback),Michael Crichton,Book,0061015733,, 11 | 4,The Rule of Four (Paperback),Ian Caldwell,Book,0099451956,book bestseller, 12 | 4,Deception Point (Paperback),Dan Brown,Book,0671027387,book danbrown bestseller, 13 | 5,Digital Fortress : A Thriller (Mass Market Paperback),Dan Brown,Book,0312995423,book danbrown bestseller, 14 | 5,Angels & Demons (Mass Market Paperback),Dan Brown,Book,0671027360,book danbrown bestseller, 15 | 4,The Da Vinci Code (Hardcover),Dan Brown," Book ",0385504209,book movie danbrown bestseller davinci, -------------------------------------------------------------------------------- /parsecsv-for-php/examples/basic.php: -------------------------------------------------------------------------------- 1 |
 2 | auto('_books.csv');
16 | 
17 | # ...or if you know the delimiter, set the delimiter character
18 | # if its not the default comma...
19 | // $csv->delimiter = "\t";   # tab delimited
20 | 
21 | # ...and then use the parse() function.
22 | // $csv->parse('_books.csv');
23 | 
24 | 
25 | # Output result.
26 | // print_r($csv->data);
27 | 
28 | 
29 | ?>
30 | 
31 | 44 | 45 | 46 | titles as $value): ?> 47 | 48 | 49 | 50 | data as $key => $row): ?> 51 | 52 | 53 | 54 | 55 | 56 | 57 |
58 | -------------------------------------------------------------------------------- /parsecsv-for-php/examples/conditions.php: -------------------------------------------------------------------------------- 1 |
 2 | conditions = 'title contains paperback OR title contains hardcover';
17 | $csv->conditions = 'author does not contain dan brown';
18 | // $csv->conditions = 'rating < 4 OR author is John Twelve Hawks';
19 | // $csv->conditions = 'rating > 4 AND author is Dan Brown';
20 | // $csv->conditions = 'rating is greater than 4';
21 | 
22 | 
23 | # Parse '_books.csv' using automatic delimiter detection.
24 | $csv->auto('_books.csv');
25 | 
26 | 
27 | # Output result.
28 | // print_r($csv->data);
29 | 
30 | 
31 | ?>
32 | 
33 | 46 | 47 | 48 | titles as $value): ?> 49 | 50 | 51 | 52 | data as $key => $row): ?> 53 | 54 | 55 | 56 | 57 | 58 | 59 |
60 | -------------------------------------------------------------------------------- /parsecsv-for-php/examples/download.php: -------------------------------------------------------------------------------- 1 | auto('_books.csv'); 16 | 17 | # ...or if you know the delimiter, set the delimiter character 18 | # if its not the default comma... 19 | // $csv->delimiter = "\t"; # tab delimited 20 | 21 | # ...and then use the parse() function. 22 | // $csv->parse('_books.csv'); 23 | 24 | # now we have data in $csv->data, at which point we can modify 25 | # it to our hearts content, like removing the last item... 26 | array_pop($csv->data); 27 | 28 | # then we output the file to the browser as a downloadable file... 29 | $csv->output('books.csv'); 30 | # ...when the first parameter is given and is not null, the 31 | # output method will itself send the correct headers and the 32 | # data to download the output as a CSV file. if it's not set 33 | # or is set to null, output will only return the generated CSV 34 | # output data, and will not output to the browser itself. 35 | -------------------------------------------------------------------------------- /parsecsv-for-php/examples/limit.php: -------------------------------------------------------------------------------- 1 |
 2 | sort_by = 'title';
25 | 
26 | 
27 | # offset from the beginning of the file,
28 | # ignoring the first X number of rows.
29 | $csv->offset = 2;
30 | 
31 | # limit the number of returned rows.
32 | $csv->limit = 3;
33 | 
34 | 
35 | # Parse '_books.csv' using automatic delimiter detection.
36 | $csv->auto('_books.csv');
37 | 
38 | 
39 | # Output result.
40 | // print_r($csv->data);
41 | 
42 | 
43 | ?>
44 | 
45 | 58 | 59 | 60 | titles as $value): ?> 61 | 62 | 63 | 64 | data as $key => $row): ?> 65 | 66 | 67 | 68 | 69 | 70 | 71 |
72 | -------------------------------------------------------------------------------- /parsecsv-for-php/examples/save_to_file_without_header_row.php: -------------------------------------------------------------------------------- 1 | heading = false; 15 | 16 | # Specify which columns to write, and in which order. 17 | # We won't output the 'Awesome' column this time. 18 | $csv->titles = ['Age', 'Name']; 19 | 20 | # Data to write: 21 | $csv->data = [ 22 | 0 => ['Name' => 'Anne', 'Age' => 45, 'Awesome' => true], 23 | 1 => ['Name' => 'John', 'Age' => 44, 'Awesome' => false], 24 | ]; 25 | 26 | # Then we save the file to the file system: 27 | $csv->save('people.csv'); 28 | -------------------------------------------------------------------------------- /parsecsv-for-php/parsecsv.lib.php: -------------------------------------------------------------------------------- 1 | var_name = 'value'; 49 | */ 50 | 51 | /** 52 | * Heading 53 | * Use first line/entry as field names 54 | * 55 | * @var bool 56 | */ 57 | public $heading = true; 58 | 59 | /** 60 | * Fields 61 | * Override field names 62 | * 63 | * @var array 64 | */ 65 | public $fields = array(); 66 | 67 | /** 68 | * Sort By 69 | * Sort CSV by this field 70 | * 71 | * @var string|null 72 | */ 73 | public $sort_by = null; 74 | 75 | /** 76 | * Sort Reverse 77 | * Reverse the sort function 78 | * 79 | * @var bool 80 | */ 81 | public $sort_reverse = false; 82 | 83 | /** 84 | * Sort Type 85 | * Sort behavior passed to sort methods 86 | * 87 | * regular = SORT_REGULAR 88 | * numeric = SORT_NUMERIC 89 | * string = SORT_STRING 90 | * 91 | * @var string|null 92 | */ 93 | public $sort_type = SortEnum::SORT_TYPE_REGULAR; 94 | 95 | /** 96 | * Delimiter 97 | * Delimiter character 98 | * 99 | * @var string 100 | */ 101 | public $delimiter = ','; 102 | 103 | /** 104 | * Enclosure 105 | * Enclosure character 106 | * 107 | * @var string 108 | */ 109 | public $enclosure = '"'; 110 | 111 | /** 112 | * Enclose All 113 | * Force enclosing all columns 114 | * 115 | * @var bool 116 | */ 117 | public $enclose_all = false; 118 | 119 | /** 120 | * Conditions 121 | * Basic SQL-Like conditions for row matching 122 | * 123 | * @var string|null 124 | */ 125 | public $conditions = null; 126 | 127 | /** 128 | * Offset 129 | * Number of rows to ignore from beginning of data. If present, the heading 130 | * row is also counted (if $this->heading == true). In other words, 131 | * $offset == 1 and $offset == 0 have the same meaning in that situation. 132 | * 133 | * @var int|null 134 | */ 135 | public $offset = null; 136 | 137 | /** 138 | * Limit 139 | * Limits the number of returned rows to the specified amount 140 | * 141 | * @var int|null 142 | */ 143 | public $limit = null; 144 | 145 | /** 146 | * Auto Depth 147 | * Number of rows to analyze when attempting to auto-detect delimiter 148 | * 149 | * @var int 150 | */ 151 | public $auto_depth = 15; 152 | 153 | /** 154 | * Auto Non Chars 155 | * Characters that should be ignored when attempting to auto-detect delimiter 156 | * 157 | * @var string 158 | */ 159 | public $auto_non_chars = "a-zA-Z0-9\n\r"; 160 | 161 | /** 162 | * Auto Preferred 163 | * preferred delimiter characters, only used when all filtering method 164 | * returns multiple possible delimiters (happens very rarely) 165 | * 166 | * @var string 167 | */ 168 | public $auto_preferred = ",;\t.:|"; 169 | 170 | /** 171 | * Convert Encoding 172 | * Should we convert the CSV character encoding? 173 | * 174 | * @var bool 175 | */ 176 | public $convert_encoding = false; 177 | 178 | /** 179 | * Input Encoding 180 | * Set the input encoding 181 | * 182 | * @var string 183 | */ 184 | public $input_encoding = 'ISO-8859-1'; 185 | 186 | /** 187 | * Output Encoding 188 | * Set the output encoding 189 | * 190 | * @var string 191 | */ 192 | public $output_encoding = 'ISO-8859-1'; 193 | 194 | /** 195 | * Whether to use mb_convert_encoding() instead of iconv(). 196 | * 197 | * The former is platform-independent whereas the latter is the traditional 198 | * default go-to solution. 199 | * 200 | * @var bool (if false, iconv() is used) 201 | */ 202 | public $use_mb_convert_encoding = false; 203 | 204 | /** 205 | * Linefeed 206 | * Line feed characters used by unparse, save, and output methods 207 | * 208 | * @var string 209 | */ 210 | public $linefeed = "\r"; 211 | 212 | /** 213 | * Output Delimiter 214 | * Sets the output delimiter used by the output method 215 | * 216 | * @var string 217 | */ 218 | public $output_delimiter = ','; 219 | 220 | /** 221 | * Output filename 222 | * Sets the output filename 223 | * 224 | * @var string 225 | */ 226 | public $output_filename = 'data.csv'; 227 | 228 | /** 229 | * Keep File Data 230 | * keep raw file data in memory after successful parsing (useful for debugging) 231 | * 232 | * @var bool 233 | */ 234 | public $keep_file_data = false; 235 | 236 | /** 237 | * Internal variables 238 | */ 239 | 240 | /** 241 | * File 242 | * Current Filename 243 | * 244 | * @var string 245 | */ 246 | public $file; 247 | 248 | /** 249 | * File Data 250 | * Current file data 251 | * 252 | * @var string 253 | */ 254 | public $file_data; 255 | 256 | /** 257 | * Error 258 | * Contains the error code if one occurred 259 | * 260 | * 0 = No errors found. Everything should be fine :) 261 | * 1 = Hopefully correctable syntax error was found. 262 | * 2 = Enclosure character (double quote by default) 263 | * was found in non-enclosed field. This means 264 | * the file is either corrupt, or does not 265 | * standard CSV formatting. Please validate 266 | * the parsed data yourself. 267 | * 268 | * @var int 269 | */ 270 | public $error = 0; 271 | 272 | /** 273 | * Error Information 274 | * Detailed error information 275 | * 276 | * @var array 277 | */ 278 | public $error_info = array(); 279 | 280 | /** 281 | * $titles has 4 distinct tasks: 282 | * 1. After reading in CSV data, $titles will contain the column headers 283 | * present in the data. 284 | * 285 | * 2. It defines which fields from the $data array to write e.g. when 286 | * calling unparse(), and in which order. This lets you skip columns you 287 | * don't want in your output, but are present in $data. 288 | * See examples/save_to_file_without_header_row.php. 289 | * 290 | * 3. It lets you rename columns. See StreamTest::testWriteStream for an 291 | * example. 292 | * 293 | * 4. When writing data and $header is true, then $titles is also used for 294 | * the first row. 295 | * 296 | * @var array 297 | */ 298 | public $titles = array(); 299 | 300 | /** 301 | * Data 302 | * Two-dimensional array of CSV data 303 | * 304 | * @var array 305 | */ 306 | public $data = array(); 307 | 308 | use DatatypeTrait; 309 | 310 | /** 311 | * Constructor 312 | * Class constructor 313 | * 314 | * @param string|null $input The CSV string or a direct file path 315 | * @param integer|null $offset Number of rows to ignore from the 316 | * beginning of the data 317 | * @param integer|null $limit Limits the number of returned rows 318 | * to specified amount 319 | * @param string|null $conditions Basic SQL-like conditions for row 320 | * matching 321 | * @param null|true $keep_file_data Keep raw file data in memory after 322 | * successful parsing 323 | * (useful for debugging) 324 | */ 325 | public function __construct($input = null, $offset = null, $limit = null, $conditions = null, $keep_file_data = null) { 326 | $this->init($offset, $limit, $conditions, $keep_file_data); 327 | 328 | if (!empty($input)) { 329 | $this->parse($input); 330 | } 331 | } 332 | 333 | /** 334 | * @param integer|null $offset Number of rows to ignore from the 335 | * beginning of the data 336 | * @param integer|null $limit Limits the number of returned rows 337 | * to specified amount 338 | * @param string|null $conditions Basic SQL-like conditions for row 339 | * matching 340 | * @param null|true $keep_file_data Keep raw file data in memory after 341 | * successful parsing 342 | * (useful for debugging) 343 | */ 344 | public function init($offset = null, $limit = null, $conditions = null, $keep_file_data = null) { 345 | if (!is_null($offset)) { 346 | $this->offset = $offset; 347 | } 348 | 349 | if (!is_null($limit)) { 350 | $this->limit = $limit; 351 | } 352 | 353 | if (!is_null($conditions)) { 354 | $this->conditions = $conditions; 355 | } 356 | 357 | if (!is_null($keep_file_data)) { 358 | $this->keep_file_data = $keep_file_data; 359 | } 360 | } 361 | 362 | // ============================================== 363 | // ----- [ Main Functions ] --------------------- 364 | // ============================================== 365 | 366 | /** 367 | * Parse 368 | * Parse a CSV file or string 369 | * 370 | * @param string|null $input The CSV string or a direct file path 371 | * @param integer $offset Number of rows to ignore from the 372 | * beginning of the data 373 | * @param integer $limit Limits the number of returned rows to 374 | * specified amount 375 | * @param string $conditions Basic SQL-like conditions for row 376 | * matching 377 | * 378 | * @return bool True on success 379 | */ 380 | public function parse($input = null, $offset = null, $limit = null, $conditions = null) { 381 | if (is_null($input)) { 382 | $input = $this->file; 383 | } 384 | 385 | if (empty($input)) { 386 | return false; 387 | } 388 | 389 | $this->init($offset, $limit, $conditions); 390 | 391 | if (strlen($input) <= PHP_MAXPATHLEN && is_readable($input)) { 392 | $this->file = $input; 393 | $this->data = $this->_parse_file(); 394 | } else { 395 | $this->file = null; 396 | $this->file_data = &$input; 397 | $this->data = $this->_parse_string(); 398 | } 399 | 400 | return $this->data !== false; 401 | } 402 | 403 | /** 404 | * Save 405 | * Save changes, or write a new file and/or data 406 | * 407 | * @param string $file File location to save to 408 | * @param array $data 2D array of data 409 | * @param bool $append Append current data to end of target CSV, if file 410 | * exists 411 | * @param array $fields Field names. Sets the header. If it is not set 412 | * $this->titles would be used instead. 413 | * 414 | * @return bool 415 | * True on success 416 | */ 417 | public function save($file = '', $data = array(), $append = FileProcessingModeEnum::MODE_FILE_OVERWRITE, $fields = array()) { 418 | if (empty($file)) { 419 | $file = &$this->file; 420 | } 421 | 422 | $mode = FileProcessingModeEnum::getAppendMode($append); 423 | $is_php = preg_match('/\.php$/i', $file) ? true : false; 424 | 425 | return $this->_wfile($file, $this->unparse($data, $fields, $append, $is_php), $mode); 426 | } 427 | 428 | /** 429 | * Output 430 | * Generate a CSV based string for output. 431 | * 432 | * @param string|null $filename If a filename is specified here or in the 433 | * object, headers and data will be output 434 | * directly to browser as a downloadable 435 | * file. This file doesn't have to exist on 436 | * the server; the parameter only affects 437 | * how the download is called to the 438 | * browser. 439 | * @param array[] $data 2D array with data 440 | * @param array $fields Field names 441 | * @param string|null $delimiter character used to separate data 442 | * 443 | * @return string The resulting CSV string 444 | */ 445 | public function output($filename = null, $data = array(), $fields = array(), $delimiter = null) { 446 | if (empty($filename)) { 447 | $filename = $this->output_filename; 448 | } 449 | 450 | if ($delimiter === null) { 451 | $delimiter = $this->output_delimiter; 452 | } 453 | 454 | $flat_string = $this->unparse($data, $fields, null, null, $delimiter); 455 | 456 | if (!is_null($filename)) { 457 | $mime = $delimiter === "\t" ? 458 | 'text/tab-separated-values' : 459 | 'application/csv'; 460 | header('Content-type: ' . $mime); 461 | header('Content-Length: ' . strlen($flat_string)); 462 | header('Cache-Control: no-cache, must-revalidate'); 463 | header('Pragma: no-cache'); 464 | header('Expires: 0'); 465 | header('Content-Disposition: attachment; filename="' . $filename . '"; modification-date="' . date('r') . '";'); 466 | 467 | echo $flat_string; 468 | } 469 | 470 | return $flat_string; 471 | } 472 | 473 | /** 474 | * Encoding 475 | * Convert character encoding 476 | * 477 | * @param string $input Input character encoding, uses default if left blank 478 | * @param string $output Output character encoding, uses default if left blank 479 | */ 480 | public function encoding($input = null, $output = null) { 481 | $this->convert_encoding = true; 482 | if (!is_null($input)) { 483 | $this->input_encoding = $input; 484 | } 485 | 486 | if (!is_null($output)) { 487 | $this->output_encoding = $output; 488 | } 489 | } 490 | 491 | /** 492 | * Auto 493 | * Auto-Detect Delimiter: Find delimiter by analyzing a specific number of 494 | * rows to determine most probable delimiter character 495 | * 496 | * @param string|null $file Local CSV file 497 | * @param bool $parse True/false parse file directly 498 | * @param int $search_depth Number of rows to analyze 499 | * @param string $preferred Preferred delimiter characters 500 | * @param string|null $enclosure Enclosure character, default is double quote ("). 501 | * 502 | * @return string The detected field delimiter 503 | */ 504 | public function auto($file = null, $parse = true, $search_depth = null, $preferred = null, $enclosure = null) { 505 | if (is_null($file)) { 506 | $file = $this->file; 507 | } 508 | 509 | if (empty($search_depth)) { 510 | $search_depth = $this->auto_depth; 511 | } 512 | 513 | if (is_null($enclosure)) { 514 | $enclosure = $this->enclosure; 515 | } else { 516 | $this->enclosure = $enclosure; 517 | } 518 | 519 | if (is_null($preferred)) { 520 | $preferred = $this->auto_preferred; 521 | } 522 | 523 | if (empty($this->file_data)) { 524 | if ($this->_check_data($file)) { 525 | $data = &$this->file_data; 526 | } else { 527 | return false; 528 | } 529 | } else { 530 | $data = &$this->file_data; 531 | } 532 | 533 | if (!$this->_detect_and_remove_sep_row_from_data($data)) { 534 | $this->_guess_delimiter($search_depth, $preferred, $enclosure, $data); 535 | } 536 | 537 | // parse data 538 | if ($parse) { 539 | $this->data = $this->_parse_string(); 540 | } 541 | 542 | return $this->delimiter; 543 | } 544 | 545 | /** 546 | * Get total number of data rows (exclusive heading line if present) in CSV 547 | * without parsing the whole data string. 548 | * 549 | * @return bool|int 550 | */ 551 | public function getTotalDataRowCount() { 552 | if (empty($this->file_data)) { 553 | return false; 554 | } 555 | 556 | $data = $this->file_data; 557 | 558 | $this->_detect_and_remove_sep_row_from_data($data); 559 | 560 | $pattern = sprintf('/%1$s[^%1$s]*%1$s/i', $this->enclosure); 561 | preg_match_all($pattern, $data, $matches); 562 | 563 | /** @var array[] $matches */ 564 | foreach ($matches[0] as $match) { 565 | if (empty($match) || (strpos($match, $this->enclosure) === false)) { 566 | continue; 567 | } 568 | 569 | $replace = str_replace(["\r", "\n"], '', $match); 570 | $data = str_replace($match, $replace, $data); 571 | } 572 | 573 | $headingRow = $this->heading ? 1 : 0; 574 | 575 | $count = substr_count($data, "\r") 576 | + substr_count($data, "\n") 577 | - substr_count($data, "\r\n") 578 | - $headingRow; 579 | 580 | return $count; 581 | } 582 | 583 | // ============================================== 584 | // ----- [ Core Functions ] --------------------- 585 | // ============================================== 586 | 587 | /** 588 | * Parse File 589 | * Read file to string and call _parse_string() 590 | * 591 | * @param string|null $file Local CSV file 592 | * 593 | * @return array|bool 594 | */ 595 | protected function _parse_file($file = null) { 596 | if (is_null($file)) { 597 | $file = $this->file; 598 | } 599 | 600 | if (empty($this->file_data)) { 601 | $this->load_data($file); 602 | } 603 | 604 | return !empty($this->file_data) ? $this->_parse_string() : false; 605 | } 606 | 607 | /** 608 | * Internal function to parse CSV strings to arrays. 609 | * 610 | * If you need BOM detection or character encoding conversion, please call 611 | * $csv->load_data($your_data_string) first, followed by a call to 612 | * $csv->parse($csv->file_data). 613 | * 614 | * To detect field separators, please use auto() instead. 615 | * 616 | * @param string $data CSV data 617 | * 618 | * @return array|false - 2D array with CSV data, or false on failure 619 | */ 620 | protected function _parse_string($data = null) { 621 | if (empty($data)) { 622 | if ($this->_check_data()) { 623 | $data = &$this->file_data; 624 | } else { 625 | return false; 626 | } 627 | } 628 | 629 | $white_spaces = str_replace($this->delimiter, '', " \t\x0B\0"); 630 | 631 | $rows = array(); 632 | $row = array(); 633 | $row_count = 0; 634 | $current = ''; 635 | $head = !empty($this->fields) ? $this->fields : array(); 636 | $col = 0; 637 | $enclosed = false; 638 | $was_enclosed = false; 639 | $strlen = strlen($data); 640 | 641 | // force the parser to process end of data as a character (false) when 642 | // data does not end with a line feed or carriage return character. 643 | $lch = $data[$strlen - 1]; 644 | if ($lch != "\n" && $lch != "\r") { 645 | $data .= "\n"; 646 | $strlen++; 647 | } 648 | 649 | // walk through each character 650 | for ($i = 0; $i < $strlen; $i++) { 651 | $ch = isset($data[$i]) ? $data[$i] : false; 652 | $nch = isset($data[$i + 1]) ? $data[$i + 1] : false; 653 | 654 | // open/close quotes, and inline quotes 655 | if ($ch == $this->enclosure) { 656 | if (!$enclosed) { 657 | if (ltrim($current, $white_spaces) == '') { 658 | $enclosed = true; 659 | $was_enclosed = true; 660 | } else { 661 | $this->error = 2; 662 | $error_row = count($rows) + 1; 663 | $error_col = $col + 1; 664 | $index = $error_row . '-' . $error_col; 665 | if (!isset($this->error_info[$index])) { 666 | $this->error_info[$index] = array( 667 | 'type' => 2, 668 | 'info' => 'Syntax error found on row ' . $error_row . '. Non-enclosed fields can not contain double-quotes.', 669 | 'row' => $error_row, 670 | 'field' => $error_col, 671 | 'field_name' => !empty($head[$col]) ? $head[$col] : null, 672 | ); 673 | } 674 | 675 | $current .= $ch; 676 | } 677 | } elseif ($nch == $this->enclosure) { 678 | $current .= $ch; 679 | $i++; 680 | } elseif ($nch != $this->delimiter && $nch != "\r" && $nch != "\n") { 681 | $x = $i + 1; 682 | while (isset($data[$x]) && ltrim($data[$x], $white_spaces) == '') { 683 | $x++; 684 | } 685 | if ($data[$x] == $this->delimiter) { 686 | $enclosed = false; 687 | $i = $x; 688 | } else { 689 | if ($this->error < 1) { 690 | $this->error = 1; 691 | } 692 | 693 | $error_row = count($rows) + 1; 694 | $error_col = $col + 1; 695 | $index = $error_row . '-' . $error_col; 696 | if (!isset($this->error_info[$index])) { 697 | $this->error_info[$index] = array( 698 | 'type' => 1, 699 | 'info' => 700 | 'Syntax error found on row ' . (count($rows) + 1) . '. ' . 701 | 'A single double-quote was found within an enclosed string. ' . 702 | 'Enclosed double-quotes must be escaped with a second double-quote.', 703 | 'row' => count($rows) + 1, 704 | 'field' => $col + 1, 705 | 'field_name' => !empty($head[$col]) ? $head[$col] : null, 706 | ); 707 | } 708 | 709 | $current .= $ch; 710 | $enclosed = false; 711 | } 712 | } else { 713 | $enclosed = false; 714 | } 715 | 716 | // end of field/row/csv 717 | } elseif (($ch === $this->delimiter || $ch == "\n" || $ch == "\r" || $ch === false) && !$enclosed) { 718 | $key = !empty($head[$col]) ? $head[$col] : $col; 719 | $row[$key] = $was_enclosed ? $current : trim($current); 720 | $current = ''; 721 | $was_enclosed = false; 722 | $col++; 723 | 724 | // end of row 725 | if (in_array($ch, ["\n", "\r", false], true)) { 726 | if ($this->_validate_offset($row_count) && $this->_validate_row_conditions($row, $this->conditions)) { 727 | if ($this->heading && empty($head)) { 728 | $head = $row; 729 | } elseif (empty($this->fields) || (!empty($this->fields) && (($this->heading && $row_count > 0) || !$this->heading))) { 730 | if (!empty($this->sort_by) && !empty($row[$this->sort_by])) { 731 | $sort_field = $row[$this->sort_by]; 732 | if (isset($rows[$sort_field])) { 733 | $rows[$sort_field . '_0'] = &$rows[$sort_field]; 734 | unset($rows[$sort_field]); 735 | $sn = 1; 736 | while (isset($rows[$sort_field . '_' . $sn])) { 737 | $sn++; 738 | } 739 | $rows[$sort_field . '_' . $sn] = $row; 740 | } else { 741 | $rows[$sort_field] = $row; 742 | } 743 | 744 | } else { 745 | $rows[] = $row; 746 | } 747 | } 748 | } 749 | 750 | $row = array(); 751 | $col = 0; 752 | $row_count++; 753 | 754 | if ($this->sort_by === null && $this->limit !== null && count($rows) == $this->limit) { 755 | $i = $strlen; 756 | } 757 | 758 | if ($ch == "\r" && $nch == "\n") { 759 | $i++; 760 | } 761 | } 762 | 763 | // append character to current field 764 | } else { 765 | $current .= $ch; 766 | } 767 | } 768 | 769 | $this->titles = $head; 770 | if (!empty($this->sort_by)) { 771 | $sort_type = SortEnum::getSorting($this->sort_type); 772 | $this->sort_reverse ? krsort($rows, $sort_type) : ksort($rows, $sort_type); 773 | 774 | if ($this->offset !== null || $this->limit !== null) { 775 | $rows = array_slice($rows, ($this->offset === null ? 0 : $this->offset), $this->limit, true); 776 | } 777 | } 778 | 779 | if (!$this->keep_file_data) { 780 | $this->file_data = null; 781 | } 782 | 783 | return $rows; 784 | } 785 | 786 | /** 787 | * Create CSV data string from array 788 | * 789 | * @param array[] $data 2D array with data 790 | * @param array $fields field names 791 | * @param bool $append if true, field names will not be output 792 | * @param bool $is_php if a php die() call should be put on the 793 | * first line of the file, this is later 794 | * ignored when read. 795 | * @param string|null $delimiter field delimiter to use 796 | * 797 | * @return string CSV data 798 | */ 799 | public function unparse($data = array(), $fields = array(), $append = FileProcessingModeEnum::MODE_FILE_OVERWRITE, $is_php = false, $delimiter = null) { 800 | if (!is_array($data) || empty($data)) { 801 | $data = &$this->data; 802 | } else { 803 | /** @noinspection ReferenceMismatchInspection */ 804 | $this->data = $data; 805 | } 806 | 807 | if (!is_array($fields) || empty($fields)) { 808 | $fields = &$this->titles; 809 | } 810 | 811 | if ($delimiter === null) { 812 | $delimiter = $this->delimiter; 813 | } 814 | 815 | $string = $is_php ? "" . $this->linefeed : ''; 816 | $entry = array(); 817 | 818 | // create heading 819 | /** @noinspection ReferenceMismatchInspection */ 820 | $fieldOrder = $this->_validate_fields_for_unparse($fields); 821 | if (!$fieldOrder && !empty($data)) { 822 | $column_count = count($data[0]); 823 | $columns = range(0, $column_count - 1, 1); 824 | $fieldOrder = array_combine($columns, $columns); 825 | } 826 | 827 | if ($this->heading && !$append && !empty($fields)) { 828 | foreach ($fieldOrder as $column_name) { 829 | $entry[] = $this->_enclose_value($column_name, $delimiter); 830 | } 831 | 832 | $string .= implode($delimiter, $entry) . $this->linefeed; 833 | $entry = array(); 834 | } 835 | 836 | // create data 837 | foreach ($data as $key => $row) { 838 | foreach (array_keys($fieldOrder) as $index) { 839 | $cell_value = $row[$index]; 840 | $entry[] = $this->_enclose_value($cell_value, $delimiter); 841 | } 842 | 843 | $string .= implode($delimiter, $entry) . $this->linefeed; 844 | $entry = array(); 845 | } 846 | 847 | if ($this->convert_encoding) { 848 | /** @noinspection PhpComposerExtensionStubsInspection 849 | * 850 | * If you receive an error at the following 3 lines, you must enable 851 | * the following PHP extension: 852 | * 853 | * - if $use_mb_convert_encoding is true: mbstring 854 | * - if $use_mb_convert_encoding is false: iconv 855 | */ 856 | $string = $this->use_mb_convert_encoding ? 857 | mb_convert_encoding($string, $this->output_encoding, $this->input_encoding) : 858 | iconv($this->input_encoding, $this->output_encoding, $string); 859 | } 860 | 861 | return $string; 862 | } 863 | 864 | private function _validate_fields_for_unparse($fields) { 865 | if (empty($fields)) { 866 | $fields = $this->titles; 867 | } 868 | 869 | if (empty($fields)) { 870 | return array(); 871 | } 872 | 873 | // this is needed because sometime titles property is overwritten instead of using fields parameter! 874 | $titlesOnParse = !empty($this->data) ? array_keys(reset($this->data)) : array(); 875 | 876 | // both are identical, also in ordering OR we have no data (only titles) 877 | if (empty($titlesOnParse) || array_values($fields) === array_values($titlesOnParse)) { 878 | return array_combine($fields, $fields); 879 | } 880 | 881 | // if renaming given by: $oldName => $newName (maybe with reorder and / or subset): 882 | // todo: this will only work if titles are unique 883 | $fieldOrder = array_intersect(array_flip($fields), $titlesOnParse); 884 | if (!empty($fieldOrder)) { 885 | return array_flip($fieldOrder); 886 | } 887 | 888 | $fieldOrder = array_intersect($fields, $titlesOnParse); 889 | if (!empty($fieldOrder)) { 890 | return array_combine($fieldOrder, $fieldOrder); 891 | } 892 | 893 | // original titles are not given in fields. that is okay if count is okay. 894 | if (count($fields) != count($titlesOnParse)) { 895 | throw new \UnexpectedValueException( 896 | "The specified fields do not match any titles and do not match column count.\n" . 897 | "\$fields was " . print_r($fields, true) . 898 | "\$titlesOnParse was " . print_r($titlesOnParse, true)); 899 | } 900 | 901 | return array_combine($titlesOnParse, $fields); 902 | } 903 | 904 | /** 905 | * Load local file or string. 906 | * 907 | * Only use this function if auto() and parse() don't handle your data well. 908 | * 909 | * This function load_data() is able to handle BOMs and encodings. The data 910 | * is stored within the $this->file_data class field. 911 | * 912 | * @param string|null $input local CSV file or CSV data as a string 913 | * 914 | * @return bool True on success 915 | */ 916 | public function load_data($input = null) { 917 | $data = null; 918 | $file = null; 919 | 920 | if (is_null($input)) { 921 | $file = $this->file; 922 | } elseif (\strlen($input) <= PHP_MAXPATHLEN && file_exists($input)) { 923 | $file = $input; 924 | } else { 925 | // It is CSV data as a string. 926 | $data = $input; 927 | } 928 | 929 | if (!empty($data) || $data = $this->_rfile($file)) { 930 | if ($this->file != $file) { 931 | $this->file = $file; 932 | } 933 | 934 | if (preg_match('/\.php$/i', $file) && preg_match('/<\?.*?\?>(.*)/ms', $data, $strip)) { 935 | $data = ltrim($strip[1]); 936 | } 937 | 938 | if (strpos($data, "\xef\xbb\xbf") === 0) { 939 | // strip off BOM (UTF-8) 940 | $data = substr($data, 3); 941 | $this->encoding('UTF-8'); 942 | } elseif (strpos($data, "\xff\xfe") === 0) { 943 | // strip off BOM (UTF-16 little endian) 944 | $data = substr($data, 2); 945 | $this->encoding("UCS-2LE"); 946 | } elseif (strpos($data, "\xfe\xff") === 0) { 947 | // strip off BOM (UTF-16 big endian) 948 | $data = substr($data, 2); 949 | $this->encoding("UTF-16"); 950 | } 951 | 952 | if ($this->convert_encoding && $this->input_encoding !== $this->output_encoding) { 953 | /** @noinspection PhpComposerExtensionStubsInspection 954 | * 955 | * If you receive an error at the following 3 lines, you must enable 956 | * the following PHP extension: 957 | * 958 | * - if $use_mb_convert_encoding is true: mbstring 959 | * - if $use_mb_convert_encoding is false: iconv 960 | */ 961 | $data = $this->use_mb_convert_encoding ? 962 | mb_convert_encoding($data, $this->output_encoding, $this->input_encoding) : 963 | iconv($this->input_encoding, $this->output_encoding, $data); 964 | } 965 | 966 | if (substr($data, -1) != "\n") { 967 | $data .= "\n"; 968 | } 969 | 970 | $this->file_data = &$data; 971 | return true; 972 | } 973 | 974 | return false; 975 | } 976 | 977 | // ============================================== 978 | // ----- [ Internal Functions ] ----------------- 979 | // ============================================== 980 | 981 | /** 982 | * Validate a row against specified conditions 983 | * 984 | * @param array $row array with values from a row 985 | * @param string|null $conditions specified conditions that the row must match 986 | * 987 | * @return true of false 988 | */ 989 | protected function _validate_row_conditions($row = array(), $conditions = null) { 990 | if (!empty($row)) { 991 | if (!empty($conditions)) { 992 | $condition_array = (strpos($conditions, ' OR ') !== false) ? 993 | explode(' OR ', $conditions) : 994 | array($conditions); 995 | $or = ''; 996 | foreach ($condition_array as $key => $value) { 997 | if (strpos($value, ' AND ') !== false) { 998 | $value = explode(' AND ', $value); 999 | $and = ''; 1000 | 1001 | foreach ($value as $k => $v) { 1002 | $and .= $this->_validate_row_condition($row, $v); 1003 | } 1004 | 1005 | $or .= (strpos($and, '0') !== false) ? '0' : '1'; 1006 | } else { 1007 | $or .= $this->_validate_row_condition($row, $value); 1008 | } 1009 | } 1010 | 1011 | return strpos($or, '1') !== false; 1012 | } 1013 | 1014 | return true; 1015 | } 1016 | 1017 | return false; 1018 | } 1019 | 1020 | /** 1021 | * Validate a row against a single condition 1022 | * 1023 | * @param array $row array with values from a row 1024 | * @param string $condition specified condition that the row must match 1025 | * 1026 | * @return string single 0 or 1 1027 | */ 1028 | protected function _validate_row_condition($row, $condition) { 1029 | $operators = array( 1030 | '=', 1031 | 'equals', 1032 | 'is', 1033 | '!=', 1034 | 'is not', 1035 | '<', 1036 | 'is less than', 1037 | '>', 1038 | 'is greater than', 1039 | '<=', 1040 | 'is less than or equals', 1041 | '>=', 1042 | 'is greater than or equals', 1043 | 'contains', 1044 | 'does not contain', 1045 | ); 1046 | 1047 | $operators_regex = array(); 1048 | 1049 | foreach ($operators as $value) { 1050 | $operators_regex[] = preg_quote($value, '/'); 1051 | } 1052 | 1053 | $operators_regex = implode('|', $operators_regex); 1054 | 1055 | if (preg_match('/^(.+) (' . $operators_regex . ') (.+)$/i', trim($condition), $capture)) { 1056 | $field = $capture[1]; 1057 | $op = strtolower($capture[2]); 1058 | $value = $capture[3]; 1059 | if ($op == 'equals' && preg_match('/^(.+) is (less|greater) than or$/i', $field, $m)) { 1060 | $field = $m[1]; 1061 | $op = strtolower($m[2]) == 'less' ? '<=' : '>='; 1062 | } 1063 | if ($op == 'is' && preg_match('/^(less|greater) than (.+)$/i', $value, $m)) { 1064 | $value = $m[2]; 1065 | $op = strtolower($m[1]) == 'less' ? '<' : '>'; 1066 | } 1067 | if ($op == 'is' && preg_match('/^not (.+)$/i', $value, $m)) { 1068 | $value = $m[1]; 1069 | $op = '!='; 1070 | } 1071 | 1072 | if (preg_match('/^([\'"])(.*)([\'"])$/', $value, $capture) && $capture[1] == $capture[3]) { 1073 | $value = strtr($capture[2], array( 1074 | "\\n" => "\n", 1075 | "\\r" => "\r", 1076 | "\\t" => "\t", 1077 | )); 1078 | 1079 | $value = stripslashes($value); 1080 | } 1081 | 1082 | if (array_key_exists($field, $row)) { 1083 | $op_equals = in_array($op, ['=', 'equals', 'is'], true); 1084 | if ($op_equals && $row[$field] == $value) { 1085 | return '1'; 1086 | } elseif (($op == '!=' || $op == 'is not') && $row[$field] != $value) { 1087 | return '1'; 1088 | } elseif (($op == '<' || $op == 'is less than') && $row[$field] < $value) { 1089 | return '1'; 1090 | } elseif (($op == '>' || $op == 'is greater than') && $row[$field] > $value) { 1091 | return '1'; 1092 | } elseif (($op == '<=' || $op == 'is less than or equals') && $row[$field] <= $value) { 1093 | return '1'; 1094 | } elseif (($op == '>=' || $op == 'is greater than or equals') && $row[$field] >= $value) { 1095 | return '1'; 1096 | } elseif ($op == 'contains' && preg_match('/' . preg_quote($value, '/') . '/i', $row[$field])) { 1097 | return '1'; 1098 | } elseif ($op == 'does not contain' && !preg_match('/' . preg_quote($value, '/') . '/i', $row[$field])) { 1099 | return '1'; 1100 | } else { 1101 | return '0'; 1102 | } 1103 | } 1104 | } 1105 | 1106 | return '1'; 1107 | } 1108 | 1109 | /** 1110 | * Validates if the row is within the offset or not if sorting is disabled 1111 | * 1112 | * @param int $current_row the current row number being processed 1113 | * 1114 | * @return true of false 1115 | */ 1116 | protected function _validate_offset($current_row) { 1117 | return 1118 | $this->sort_by !== null || 1119 | $this->offset === null || 1120 | $current_row >= $this->offset || 1121 | ($this->heading && $current_row == 0); 1122 | } 1123 | 1124 | /** 1125 | * Enclose values if needed 1126 | * - only used by unparse() 1127 | * 1128 | * @param string|null $value Cell value to process 1129 | * @param string $delimiter Character to put between cells on the same row 1130 | * 1131 | * @return string Processed value 1132 | */ 1133 | protected function _enclose_value($value, $delimiter) { 1134 | if ($value !== null && $value != '') { 1135 | $delimiter_quoted = $delimiter ? 1136 | preg_quote($delimiter, '/') . "|" 1137 | : ''; 1138 | $enclosure_quoted = preg_quote($this->enclosure, '/'); 1139 | $pattern = "/" . $delimiter_quoted . $enclosure_quoted . "|\n|\r/i"; 1140 | if ($this->enclose_all || preg_match($pattern, $value) || strpos($value, ' ') === 0 || substr($value, -1) == ' ') { 1141 | $value = str_replace($this->enclosure, $this->enclosure . $this->enclosure, $value); 1142 | $value = $this->enclosure . $value . $this->enclosure; 1143 | } 1144 | } 1145 | 1146 | return $value; 1147 | } 1148 | 1149 | /** 1150 | * Check file data 1151 | * 1152 | * @param string|null $file local filename 1153 | * 1154 | * @return bool 1155 | */ 1156 | protected function _check_data($file = null) { 1157 | if (empty($this->file_data)) { 1158 | if (is_null($file)) { 1159 | $file = $this->file; 1160 | } 1161 | 1162 | return $this->load_data($file); 1163 | } 1164 | 1165 | return true; 1166 | } 1167 | 1168 | /** 1169 | * Check if passed info might be delimiter 1170 | * Only used by find_delimiter 1171 | * 1172 | * @param string $char Potential field separating character 1173 | * @param array $array Frequency 1174 | * @param int $depth Number of analyzed rows 1175 | * @param string $preferred Preferred delimiter characters 1176 | * 1177 | * @return string|false special string used for delimiter selection, or false 1178 | */ 1179 | protected function _check_count($char, $array, $depth, $preferred) { 1180 | if ($depth === count($array)) { 1181 | $first = null; 1182 | $equal = null; 1183 | $almost = false; 1184 | foreach ($array as $key => $value) { 1185 | if ($first == null) { 1186 | $first = $value; 1187 | } elseif ($value == $first && $equal !== false) { 1188 | $equal = true; 1189 | } elseif ($value == $first + 1 && $equal !== false) { 1190 | $equal = true; 1191 | $almost = true; 1192 | } else { 1193 | $equal = false; 1194 | } 1195 | } 1196 | 1197 | if ($equal) { 1198 | $match = $almost ? 2 : 1; 1199 | $pref = strpos($preferred, $char); 1200 | $pref = ($pref !== false) ? str_pad($pref, 3, '0', STR_PAD_LEFT) : '999'; 1201 | 1202 | return $pref . $match . '.' . (99999 - str_pad($first, 5, '0', STR_PAD_LEFT)); 1203 | } else { 1204 | return false; 1205 | } 1206 | } 1207 | return false; 1208 | } 1209 | 1210 | /** 1211 | * Read local file. 1212 | * 1213 | * @param string $file local filename 1214 | * 1215 | * @return string|false Data from file, or false on failure 1216 | */ 1217 | protected function _rfile($file) { 1218 | if (is_readable($file)) { 1219 | $data = file_get_contents($file); 1220 | if ($data === false) { 1221 | return false; 1222 | } 1223 | return rtrim($data, "\r\n"); 1224 | } 1225 | 1226 | return false; 1227 | } 1228 | 1229 | /** 1230 | * Write to local file 1231 | * 1232 | * @param string $file local filename 1233 | * @param string $content data to write to file 1234 | * @param string $mode fopen() mode 1235 | * @param int $lock flock() mode 1236 | * 1237 | * @return bool 1238 | * True on success 1239 | * 1240 | */ 1241 | protected function _wfile($file, $content = '', $mode = 'wb', $lock = LOCK_EX) { 1242 | if ($fp = fopen($file, $mode)) { 1243 | flock($fp, $lock); 1244 | $re = fwrite($fp, $content); 1245 | $re2 = fclose($fp); 1246 | 1247 | if ($re !== false && $re2 !== false) { 1248 | return true; 1249 | } 1250 | } 1251 | 1252 | return false; 1253 | } 1254 | 1255 | /** 1256 | * Detect separator using a nonstandard hack: such file starts with the 1257 | * first line containing only "sep=;", where the last character is the 1258 | * separator. Microsoft Excel is able to open such files. 1259 | * 1260 | * @param string $data file data 1261 | * 1262 | * @return string|false detected delimiter, or false if none found 1263 | */ 1264 | protected function _get_delimiter_from_sep_row($data) { 1265 | $sep = false; 1266 | // 32 bytes should be quite enough data for our sniffing, chosen arbitrarily 1267 | $sepPrefix = substr($data, 0, 32); 1268 | if (preg_match('/^sep=(.)\\r?\\n/i', $sepPrefix, $sepMatch)) { 1269 | // we get separator. 1270 | $sep = $sepMatch[1]; 1271 | } 1272 | return $sep; 1273 | } 1274 | 1275 | /** 1276 | * Support for Excel-compatible sep=? row. 1277 | * 1278 | * @param string $data_string file data to be updated 1279 | * 1280 | * @return bool TRUE if sep= line was found at the very beginning of the file 1281 | */ 1282 | protected function _detect_and_remove_sep_row_from_data(&$data_string) { 1283 | $sep = $this->_get_delimiter_from_sep_row($data_string); 1284 | if ($sep === false) { 1285 | return false; 1286 | } 1287 | 1288 | $this->delimiter = $sep; 1289 | 1290 | // likely to be 5, but let's not assume we're always single-byte. 1291 | $pos = 4 + strlen($sep); 1292 | // the next characters should be a line-end 1293 | if (substr($data_string, $pos, 1) === "\r") { 1294 | $pos++; 1295 | } 1296 | if (substr($data_string, $pos, 1) === "\n") { 1297 | $pos++; 1298 | } 1299 | 1300 | // remove delimiter and its line-end (the data param is by-ref!) 1301 | /** @noinspection CallableParameterUseCaseInTypeContextInspection */ 1302 | $data_string = substr($data_string, $pos); 1303 | return true; 1304 | } 1305 | 1306 | /** 1307 | * @param int $search_depth Number of rows to analyze 1308 | * @param string $preferred Preferred delimiter characters 1309 | * @param string $enclosure Enclosure character, default is double quote 1310 | * @param string $data The file content 1311 | */ 1312 | protected function _guess_delimiter($search_depth, $preferred, $enclosure, &$data) { 1313 | $chars = []; 1314 | $strlen = strlen($data); 1315 | $enclosed = false; 1316 | $current_row = 1; 1317 | $to_end = true; 1318 | 1319 | // The dash is the only character we don't want quoted, as it would 1320 | // prevent character ranges within $auto_non_chars: 1321 | $quoted_auto_non_chars = preg_quote($this->auto_non_chars, '/'); 1322 | $quoted_auto_non_chars = str_replace('\-', '-', $quoted_auto_non_chars); 1323 | $pattern = '/[' . $quoted_auto_non_chars . ']/i'; 1324 | 1325 | // walk specific depth finding possible delimiter characters 1326 | for ($i = 0; $i < $strlen; $i++) { 1327 | $ch = $data[$i]; 1328 | $nch = isset($data[$i + 1]) ? $data[$i + 1] : false; 1329 | $pch = isset($data[$i - 1]) ? $data[$i - 1] : false; 1330 | 1331 | // open and closing quotes 1332 | $is_newline = ($ch == "\n" && $pch != "\r") || $ch == "\r"; 1333 | if ($ch == $enclosure) { 1334 | if (!$enclosed || $nch != $enclosure) { 1335 | $enclosed = $enclosed ? false : true; 1336 | } elseif ($enclosed) { 1337 | $i++; 1338 | } 1339 | 1340 | // end of row 1341 | } elseif ($is_newline && !$enclosed) { 1342 | if ($current_row >= $search_depth) { 1343 | $strlen = 0; 1344 | $to_end = false; 1345 | } else { 1346 | $current_row++; 1347 | } 1348 | 1349 | // count character 1350 | } elseif (!$enclosed) { 1351 | if (!preg_match($pattern, $ch)) { 1352 | if (!isset($chars[$ch][$current_row])) { 1353 | $chars[$ch][$current_row] = 1; 1354 | } else { 1355 | $chars[$ch][$current_row]++; 1356 | } 1357 | } 1358 | } 1359 | } 1360 | 1361 | // filtering 1362 | $depth = $to_end ? $current_row - 1 : $current_row; 1363 | $filtered = []; 1364 | foreach ($chars as $char => $value) { 1365 | if ($match = $this->_check_count($char, $value, $depth, $preferred)) { 1366 | $filtered[$match] = $char; 1367 | } 1368 | } 1369 | 1370 | // capture most probable delimiter 1371 | ksort($filtered); 1372 | $this->delimiter = reset($filtered); 1373 | } 1374 | 1375 | /** 1376 | * getCollection 1377 | * Returns a Illuminate/Collection object 1378 | * This may prove to be helpful to people who want to 1379 | * create macros, and or use map functions 1380 | * 1381 | * @access public 1382 | * @link https://laravel.com/docs/5.6/collections 1383 | * 1384 | * @throws \ErrorException - If the Illuminate\Support\Collection class is not found 1385 | * 1386 | * @return Collection 1387 | */ 1388 | public function getCollection() { 1389 | //does the Illuminate\Support\Collection class exists? 1390 | //this uses the autoloader to try to determine 1391 | //@see http://php.net/manual/en/function.class-exists.php 1392 | if (class_exists('Illuminate\Support\Collection', true) == false) { 1393 | throw new \ErrorException('It would appear you have not installed the illuminate/support package!'); 1394 | } 1395 | 1396 | //return the collection 1397 | return new Collection($this->data); 1398 | } 1399 | } 1400 | -------------------------------------------------------------------------------- /parsecsv-for-php/src/enums/AbstractEnum.php: -------------------------------------------------------------------------------- 1 | isValid($value)) { 16 | throw new \UnexpectedValueException("Value '$value' is not part of the enum " . get_called_class()); 17 | } 18 | $this->value = $value; 19 | } 20 | 21 | public static function getConstants() { 22 | $class = get_called_class(); 23 | $reflection = new \ReflectionClass($class); 24 | 25 | return $reflection->getConstants(); 26 | } 27 | 28 | /** 29 | * Check if enum value is valid 30 | * 31 | * @param $value 32 | * 33 | * @return bool 34 | */ 35 | public static function isValid($value) { 36 | return in_array($value, static::getConstants(), true); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /parsecsv-for-php/src/enums/DatatypeEnum.php: -------------------------------------------------------------------------------- 1 | null, 44 | self::TYPE_INT => 'isValidInteger', 45 | self::TYPE_BOOL => 'isValidBoolean', 46 | self::TYPE_FLOAT => 'isValidFloat', 47 | self::TYPE_DATE => 'isValidDate', 48 | ); 49 | 50 | /** 51 | * Checks data type for given string. 52 | * 53 | * @param string $value 54 | * 55 | * @return bool|string 56 | */ 57 | public static function getValidTypeFromSample($value) { 58 | $value = trim((string) $value); 59 | 60 | if (empty($value)) { 61 | return false; 62 | } 63 | 64 | foreach (self::$validators as $type => $validator) { 65 | if ($validator === null) { 66 | continue; 67 | } 68 | 69 | if (method_exists(__CLASS__, $validator) && self::$validator($value)) { 70 | return $type; 71 | } 72 | } 73 | 74 | return self::__DEFAULT; 75 | } 76 | 77 | /** 78 | * Check if string is float value. 79 | * 80 | * @param string $value 81 | * 82 | * @return bool 83 | */ 84 | private static function isValidFloat($value) { 85 | return (bool) preg_match(self::REGEX_FLOAT, $value); 86 | } 87 | 88 | /** 89 | * Check if string is integer value. 90 | * 91 | * @param string $value 92 | * 93 | * @return bool 94 | */ 95 | private static function isValidInteger($value) { 96 | return (bool) preg_match(self::REGEX_INT, $value); 97 | } 98 | 99 | /** 100 | * Check if string is boolean. 101 | * 102 | * @param string $value 103 | * 104 | * @return bool 105 | */ 106 | private static function isValidBoolean($value) { 107 | return (bool) preg_match(self::REGEX_BOOL, $value); 108 | } 109 | 110 | /** 111 | * Check if string is date. 112 | * 113 | * @param string $value 114 | * 115 | * @return bool 116 | */ 117 | private static function isValidDate($value) { 118 | return (bool) strtotime($value); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /parsecsv-for-php/src/enums/FileProcessingModeEnum.php: -------------------------------------------------------------------------------- 1 | SORT_REGULAR, 18 | self::SORT_TYPE_STRING => SORT_STRING, 19 | self::SORT_TYPE_NUMERIC => SORT_NUMERIC, 20 | ); 21 | 22 | public static function getSorting($type) { 23 | if (array_key_exists($type, self::$sorting)) { 24 | return self::$sorting[$type]; 25 | } 26 | 27 | return self::$sorting[self::__DEFAULT]; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /parsecsv-for-php/src/extensions/DatatypeTrait.php: -------------------------------------------------------------------------------- 1 | = 5.5 47 | * 48 | * @uses DatatypeEnum::getValidTypeFromSample 49 | * 50 | * @return array|bool 51 | */ 52 | public function getDatatypes() { 53 | if (empty($this->data)) { 54 | $this->data = $this->_parse_string(); 55 | } 56 | if (!is_array($this->data)) { 57 | throw new \UnexpectedValueException('No data set yet.'); 58 | } 59 | 60 | $result = []; 61 | foreach ($this->titles as $cName) { 62 | $column = array_column($this->data, $cName); 63 | $cDatatypes = array_map(DatatypeEnum::class . '::getValidTypeFromSample', $column); 64 | 65 | $result[$cName] = $this->getMostFrequentDatatypeForColumn($cDatatypes); 66 | } 67 | 68 | $this->data_types = $result; 69 | 70 | return !empty($this->data_types) ? $this->data_types : []; 71 | } 72 | 73 | /** 74 | * Check data type of titles / first row for auto detecting if this could be 75 | * a heading line. 76 | * 77 | * Requires PHP >= 5.5 78 | * 79 | * @uses DatatypeEnum::getValidTypeFromSample 80 | * 81 | * @return bool 82 | */ 83 | public function autoDetectFileHasHeading() { 84 | if (empty($this->data)) { 85 | throw new \UnexpectedValueException('No data set yet.'); 86 | } 87 | 88 | if ($this->heading) { 89 | $firstRow = $this->titles; 90 | } else { 91 | $firstRow = $this->data[0]; 92 | } 93 | 94 | $firstRow = array_filter($firstRow); 95 | if (empty($firstRow)) { 96 | return false; 97 | } 98 | 99 | $firstRowDatatype = array_map(DatatypeEnum::class . '::getValidTypeFromSample', $firstRow); 100 | 101 | return $this->getMostFrequentDatatypeForColumn($firstRowDatatype) === DatatypeEnum::TYPE_STRING; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /parsecsv-for-php/tests/Bootstrap.php: -------------------------------------------------------------------------------- 1 | csv = new Csv(null, $offset); 18 | $this->assertTrue(is_numeric($this->csv->offset)); 19 | $this->assertEquals($offset, $this->csv->offset); 20 | } 21 | 22 | public function test_limit_param() { 23 | $limit = 10; 24 | $this->csv = new Csv(null, null, $limit); 25 | $this->assertTrue(is_numeric($this->csv->limit)); 26 | $this->assertEquals($limit, $this->csv->limit); 27 | } 28 | 29 | public function test_conditions_param() { 30 | $conditions = 'some column NOT value'; 31 | $this->csv = new Csv(null, null, null, $conditions); 32 | $this->assertTrue(is_string($this->csv->conditions)); 33 | $this->assertEquals($conditions, $this->csv->conditions); 34 | } 35 | 36 | public function test_keep_file_data_param() { 37 | $keep = true; 38 | $this->csv = new Csv(null, null, null, null, $keep); 39 | $this->assertTrue(is_bool($this->csv->keep_file_data)); 40 | $this->assertEquals($keep, $this->csv->keep_file_data); 41 | } 42 | 43 | public function test_input_param() { 44 | $csv = "col1,col2,col3\r\nval1,val2,val3\r\nval1A,val2A,val3A\r\n"; 45 | $this->csv = new Csv($csv, null, null, null, true); 46 | $this->assertTrue(is_string($this->csv->file_data)); 47 | $this->assertEquals($csv, $this->csv->file_data); 48 | } 49 | 50 | /** 51 | * @runInSeparateProcess because download.php uses header() 52 | * 53 | * @see https://github.com/sebastianbergmann/phpunit/issues/720#issuecomment-10421092 54 | */ 55 | public function testCodeExamples() { 56 | chdir('examples'); 57 | foreach (glob('*.php') as $script_file) { 58 | ob_start(); 59 | /** @noinspection PhpIncludeInspection */ 60 | require $script_file; 61 | $ob_get_clean = ob_get_clean(); 62 | $verb = strtok($script_file, '_.'); 63 | 64 | if (!in_array($verb, ['download', 'save'], true)) { 65 | $this->assertContains('', $ob_get_clean); 66 | } 67 | } 68 | chdir('..'); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /parsecsv-for-php/tests/methods/DataRowCountTest.php: -------------------------------------------------------------------------------- 1 | csv = new Csv(); 28 | } 29 | 30 | public function countRowsProvider() { 31 | return [ 32 | 'auto-double-enclosure' => [ 33 | 'auto-double-enclosure.csv', 34 | 2, 35 | ], 36 | 'auto-single-enclosure' => [ 37 | 'auto-single-enclosure.csv', 38 | 2, 39 | ], 40 | 'UTF-8_sep_row' => [ 41 | 'datatype.csv', 42 | 3, 43 | ], 44 | ]; 45 | } 46 | 47 | /** 48 | * @dataProvider countRowsProvider 49 | * 50 | * @param string $file 51 | * @param int $expectedRows 52 | */ 53 | public function testGetTotalRowCountFromFile($file, $expectedRows) { 54 | $this->csv->heading = true; 55 | $this->csv->load_data(__DIR__ . '/fixtures/' . $file); 56 | $this->assertEquals($expectedRows, $this->csv->getTotalDataRowCount()); 57 | } 58 | 59 | public function testGetTotalRowCountMissingEndingLineBreak() { 60 | $this->csv->heading = false; 61 | $this->csv->enclosure = '"'; 62 | $sInput = "86545235689,a\r\n34365587654,b\r\n13469874576,\"c\r\nd\""; 63 | $this->csv->load_data($sInput); 64 | $this->assertEquals(3, $this->csv->getTotalDataRowCount()); 65 | } 66 | 67 | public function testGetTotalRowCountSingleEnclosure() { 68 | $this->csv->heading = false; 69 | $this->csv->enclosure = "'"; 70 | $sInput = "86545235689,a\r\n34365587654,b\r\n13469874576,\'c\r\nd\'"; 71 | $this->csv->load_data($sInput); 72 | $this->assertEquals(3, $this->csv->getTotalDataRowCount()); 73 | } 74 | 75 | public function testGetTotalRowCountSingleRow() { 76 | $this->csv->heading = false; 77 | $this->csv->enclosure = "'"; 78 | $sInput = "86545235689"; 79 | $this->csv->load_data($sInput); 80 | $this->assertEquals(1, $this->csv->getTotalDataRowCount()); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /parsecsv-for-php/tests/methods/DatatypeTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(DatatypeEnum::TYPE_INT, DatatypeEnum::getValidTypeFromSample('1')); 18 | $this->assertEquals(DatatypeEnum::TYPE_INT, DatatypeEnum::getValidTypeFromSample('+1')); 19 | $this->assertEquals(DatatypeEnum::TYPE_INT, DatatypeEnum::getValidTypeFromSample('-1')); 20 | $this->assertNotEquals(DatatypeEnum::TYPE_INT, DatatypeEnum::getValidTypeFromSample('--1')); 21 | $this->assertNotEquals(DatatypeEnum::TYPE_INT, DatatypeEnum::getValidTypeFromSample('test')); 22 | $this->assertNotEquals(DatatypeEnum::TYPE_INT, DatatypeEnum::getValidTypeFromSample('1.0')); 23 | $this->assertNotEquals(DatatypeEnum::TYPE_INT, DatatypeEnum::getValidTypeFromSample('1,0')); 24 | $this->assertNotEquals(DatatypeEnum::TYPE_INT, DatatypeEnum::getValidTypeFromSample('1,1')); 25 | $this->assertNotEquals(DatatypeEnum::TYPE_INT, DatatypeEnum::getValidTypeFromSample('0.1')); 26 | $this->assertNotEquals(DatatypeEnum::TYPE_INT, DatatypeEnum::getValidTypeFromSample('true')); 27 | $this->assertNotEquals(DatatypeEnum::TYPE_INT, DatatypeEnum::getValidTypeFromSample('2018-02-19')); 28 | } 29 | 30 | public function testSampleIsValidBool() { 31 | $this->assertEquals(DatatypeEnum::TYPE_BOOL, DatatypeEnum::getValidTypeFromSample('true')); 32 | $this->assertEquals(DatatypeEnum::TYPE_BOOL, DatatypeEnum::getValidTypeFromSample('TRUE')); 33 | $this->assertEquals(DatatypeEnum::TYPE_BOOL, DatatypeEnum::getValidTypeFromSample('false')); 34 | $this->assertEquals(DatatypeEnum::TYPE_BOOL, DatatypeEnum::getValidTypeFromSample('FALSE')); 35 | $this->assertNotEquals(DatatypeEnum::TYPE_BOOL, DatatypeEnum::getValidTypeFromSample('FALS')); 36 | $this->assertNotEquals(DatatypeEnum::TYPE_BOOL, DatatypeEnum::getValidTypeFromSample('test')); 37 | $this->assertNotEquals(DatatypeEnum::TYPE_BOOL, DatatypeEnum::getValidTypeFromSample('0')); 38 | $this->assertNotEquals(DatatypeEnum::TYPE_BOOL, DatatypeEnum::getValidTypeFromSample('1')); 39 | $this->assertNotEquals(DatatypeEnum::TYPE_BOOL, DatatypeEnum::getValidTypeFromSample('0.1')); 40 | } 41 | 42 | public function testSampleIsValidFloat() { 43 | $this->assertEquals(DatatypeEnum::TYPE_FLOAT, DatatypeEnum::getValidTypeFromSample('1.0')); 44 | $this->assertEquals(DatatypeEnum::TYPE_FLOAT, DatatypeEnum::getValidTypeFromSample('-1.1')); 45 | $this->assertEquals(DatatypeEnum::TYPE_FLOAT, DatatypeEnum::getValidTypeFromSample('+1,1')); 46 | $this->assertEquals(DatatypeEnum::TYPE_FLOAT, DatatypeEnum::getValidTypeFromSample('1,1')); 47 | $this->assertEquals(DatatypeEnum::TYPE_FLOAT, DatatypeEnum::getValidTypeFromSample('1,1')); 48 | $this->assertEquals(DatatypeEnum::TYPE_FLOAT, DatatypeEnum::getValidTypeFromSample('1e-03')); 49 | $this->assertEquals(DatatypeEnum::TYPE_FLOAT, DatatypeEnum::getValidTypeFromSample('1e+03')); 50 | $this->assertNotEquals(DatatypeEnum::TYPE_FLOAT, DatatypeEnum::getValidTypeFromSample('1')); 51 | $this->assertNotEquals(DatatypeEnum::TYPE_FLOAT, DatatypeEnum::getValidTypeFromSample('test')); 52 | $this->assertNotEquals(DatatypeEnum::TYPE_FLOAT, DatatypeEnum::getValidTypeFromSample('1,,1')); 53 | $this->assertNotEquals(DatatypeEnum::TYPE_FLOAT, DatatypeEnum::getValidTypeFromSample('1..1')); 54 | $this->assertNotEquals(DatatypeEnum::TYPE_FLOAT, DatatypeEnum::getValidTypeFromSample('2018-02-19')); 55 | } 56 | 57 | public function testSampleIsValidDate() { 58 | $this->assertEquals(DatatypeEnum::TYPE_DATE, DatatypeEnum::getValidTypeFromSample('2018-02-19')); 59 | $this->assertEquals(DatatypeEnum::TYPE_DATE, DatatypeEnum::getValidTypeFromSample('18-2-19')); 60 | $this->assertEquals(DatatypeEnum::TYPE_DATE, DatatypeEnum::getValidTypeFromSample('01.02.2018')); 61 | $this->assertEquals(DatatypeEnum::TYPE_DATE, DatatypeEnum::getValidTypeFromSample('1.2.18')); 62 | $this->assertEquals(DatatypeEnum::TYPE_DATE, DatatypeEnum::getValidTypeFromSample('07/31/2018')); 63 | $this->assertNotEquals(DatatypeEnum::TYPE_DATE, DatatypeEnum::getValidTypeFromSample('31/07/2018')); 64 | $this->assertNotEquals(DatatypeEnum::TYPE_DATE, DatatypeEnum::getValidTypeFromSample('1-2')); 65 | $this->assertEquals(DatatypeEnum::TYPE_DATE, DatatypeEnum::getValidTypeFromSample('1.2.')); 66 | $this->assertNotEquals(DatatypeEnum::TYPE_DATE, DatatypeEnum::getValidTypeFromSample('test')); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /parsecsv-for-php/tests/methods/ExampleStream.php: -------------------------------------------------------------------------------- 1 | strlen(self::$stream_content)]; 44 | } 45 | 46 | 47 | public function stream_tell() { 48 | return self::$position; 49 | } 50 | 51 | public function stream_eof() { 52 | return self::$position >= strlen(self::$stream_content); 53 | } 54 | 55 | public function url_stat() { 56 | return ['size' => strlen(self::$stream_content)]; 57 | } 58 | 59 | public function stream_seek($offset, $whence) { 60 | switch ($whence) { 61 | case SEEK_SET: 62 | if ($offset < strlen(self::$stream_content) && $offset >= 0) { 63 | self::$position = $offset; 64 | return true; 65 | } else { 66 | return false; 67 | } 68 | break; 69 | 70 | case SEEK_CUR: 71 | if ($offset >= 0) { 72 | self::$position += $offset; 73 | return true; 74 | } else { 75 | return false; 76 | } 77 | break; 78 | 79 | case SEEK_END: 80 | if (strlen(self::$stream_content) + $offset >= 0) { 81 | self::$position = strlen(self::$stream_content) + $offset; 82 | return true; 83 | } else { 84 | return false; 85 | } 86 | break; 87 | 88 | default: 89 | return false; 90 | } 91 | } 92 | 93 | public function stream_lock($operation) { 94 | return true; 95 | } 96 | 97 | public function stream_metadata() { 98 | return false; 99 | } 100 | } 101 | 102 | -------------------------------------------------------------------------------- /parsecsv-for-php/tests/methods/ObjectThatHasToStringMethod.php: -------------------------------------------------------------------------------- 1 | assertEquals($output, []); 31 | $this->assertEquals(0, $return_var); 32 | } 33 | 34 | /** 35 | * @runInSeparateProcess so that disabled autoloading has an effect 36 | */ 37 | public function testOldLibWithOldClassName() { 38 | 39 | file_put_contents('__eval.php', 'assertEquals($output, []); 43 | $this->assertEquals(0, $return_var); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /parsecsv-for-php/tests/methods/OutputTest.php: -------------------------------------------------------------------------------- 1 | ['a', 'b', 'c'], 1 => ['d', 'e', 'f']]; 16 | $fields = ['col1', 'col2', 'col3']; 17 | $output = $csv->output('test.csv', $data, $fields, ','); 18 | $expected = "col1,col2,col3\ra,b,c\rd,e,f\r"; 19 | self::assertEquals($expected, $output); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /parsecsv-for-php/tests/methods/ParseTest.php: -------------------------------------------------------------------------------- 1 | csv = new Csv(); 23 | } 24 | 25 | public function testParse() { 26 | // can we trick 'is_readable' into whining? See #67. 27 | $this->parseRepetitiveString('c:/looks/like/a/path'); 28 | $this->parseRepetitiveString('http://looks/like/an/url'); 29 | } 30 | 31 | private function parseRepetitiveString($content) { 32 | $this->csv->delimiter = ';'; 33 | $this->csv->heading = false; 34 | $success = $this->csv->parse(str_repeat($content . ';', 500)); 35 | $this->assertEquals(true, $success); 36 | 37 | $row = array_pop($this->csv->data); 38 | $expected_data = array_fill(0, 500, $content); 39 | $expected_data [] = ''; 40 | $this->assertEquals($expected_data, $row); 41 | } 42 | 43 | /** 44 | * @depends testParse 45 | * 46 | * @dataProvider autoDetectionProvider 47 | * 48 | * @param string $file 49 | */ 50 | public function testSepRowAutoDetection($file) { 51 | // This file (parse_test.php) is encoded in UTF-8, hence comparison will 52 | // fail unless we to this: 53 | $this->csv->output_encoding = 'UTF-8'; 54 | 55 | $this->csv->auto($file); 56 | $this->assertEquals($this->_get_magazines_data(), $this->csv->data); 57 | } 58 | 59 | public function autoDetectionProvider() { 60 | return [ 61 | 'UTF8_no_BOM' => [__DIR__ . '/../example_files/UTF-8_sep_row_but_no_BOM.csv'], 62 | 'UTF8' => [__DIR__ . '/../example_files/UTF-8_with_BOM_and_sep_row.csv'], 63 | 'UTF16' => [__DIR__ . '/../example_files/UTF-16LE_with_BOM_and_sep_row.csv'], 64 | ]; 65 | } 66 | 67 | public function testSingleColumnWithZeros() { 68 | $this->csv->delimiter = null; 69 | $this->csv->parse("URL\nhttp://www.amazon.com/ROX-Ice-Ball-Maker-Original/dp/B00MX59NMQ/ref=sr_1_1?ie=UTF8&qid=1435604374&sr=8-1&keywords=rox,+ice+molds"); 70 | $row = array_pop($this->csv->data); 71 | $expected_data = ['URL' => 'http://www.amazon.com/ROX-Ice-Ball-Maker-Original/dp/B00MX59NMQ/ref=sr_1_1?ie=UTF8&qid=1435604374&sr=8-1&keywords=rox,+ice+molds']; 72 | $this->assertEquals($expected_data, $row); 73 | } 74 | 75 | public function testAllNumericalCsv() { 76 | $this->csv->heading = false; 77 | $sInput = "86545235689\r\n34365587654\r\n13469874576"; 78 | $this->assertEquals(false, $this->csv->auto($sInput)); 79 | $this->assertEquals(null, $this->csv->delimiter); 80 | $expected_data = explode("\r\n", $sInput); 81 | $actual_data = array_map('reset', $this->csv->data); 82 | $this->assertEquals($expected_data, $actual_data); 83 | } 84 | 85 | public function testMissingEndingLineBreak() { 86 | $this->csv->heading = false; 87 | $this->csv->enclosure = '"'; 88 | $sInput = "86545235689,a\r\n34365587654,b\r\n13469874576,\"c\r\nd\""; 89 | $expected_data = [86545235689, 34365587654, 13469874576]; 90 | 91 | $actual_data = $this->invokeMethod($this->csv, '_parse_string', array($sInput)); 92 | $actual_column = array_map('reset', $actual_data); 93 | $this->assertEquals($expected_data, $actual_column); 94 | $this->assertEquals([ 95 | 'a', 96 | 'b', 97 | "c\r\nd", 98 | ], array_map('next', $actual_data)); 99 | } 100 | 101 | public function testSingleColumn() { 102 | $this->csv->auto(__DIR__ . '/../example_files/single_column.csv'); 103 | $expected = [ 104 | ['SMS' => '0444'], 105 | ['SMS' => '5555'], 106 | ['SMS' => '6606'], 107 | ['SMS' => '7777'], 108 | ]; 109 | $this->assertEquals($expected, $this->csv->data); 110 | } 111 | 112 | public function testMatomoData() { 113 | // Matomo (Piwik) export cannot be read with 114 | $this->csv->use_mb_convert_encoding = true; 115 | $this->csv->output_encoding = 'UTF-8'; 116 | $this->csv->auto(__DIR__ . '/../example_files/Piwik_API_download.csv'); 117 | $aAction27 = array_column($this->csv->data, 'url (actionDetails 27)'); 118 | $this->assertEquals([ 119 | 'http://application/_Main/_GraphicMeanSTD_MDI/btnConfBandOptions', 120 | '', 121 | '', 122 | ], $aAction27); 123 | 124 | $aCity = array_column($this->csv->data, 'city'); 125 | $this->assertEquals([ 126 | 'São Paulo', 127 | 'Johannesburg', 128 | '', 129 | ], $aCity); 130 | } 131 | 132 | /** 133 | * Tests if we can handle BOMs in string data, in contrast to loading files. 134 | */ 135 | public function testStringWithLeadingBOM() { 136 | $string_with_bom = strtr( 137 | file_get_contents(__DIR__ . '/../example_files/UTF-8_with_BOM_and_sep_row.csv'), 138 | ["sep=;\n" => '']); 139 | 140 | // Is the BOM still there? 141 | self::assertSame(0xEF, ord($string_with_bom)); 142 | 143 | $this->csv->output_encoding = 'UTF-8'; 144 | $this->csv->delimiter = ';'; 145 | self::assertTrue($this->csv->load_data($string_with_bom)); 146 | self::assertTrue($this->csv->parse($this->csv->file_data)); 147 | 148 | // This also tests if ::load_data removed the BOM from the data; 149 | // otherwise the 'title' column would have 3 extra bytes. 150 | $this->assertEquals([ 151 | 'title', 152 | 'isbn', 153 | 'publishedAt', 154 | ], array_keys(reset($this->csv->data))); 155 | 156 | $titles = array_column($this->csv->data, 'title'); 157 | $this->assertEquals([ 158 | 'Красивая кулинария', 159 | 'The Wine Connoisseurs', 160 | 'Weißwein', 161 | ], $titles); 162 | } 163 | 164 | public function testWithMultipleNewlines() { 165 | $this->csv->auto(__DIR__ . '/../example_files/multiple_empty_lines.csv'); 166 | $aElse9 = array_column($this->csv->data, 'else9'); 167 | 168 | /** @noinspection SpellCheckingInspection */ 169 | $this->assertEquals([ 170 | 'Abweichung', 171 | 'Abweichung', 172 | 'Abweichung', 173 | 'Alt', 174 | 'Fehlt', 175 | 'Neu', 176 | 'OK', 177 | 'Fehlt', 178 | 'Fehlt', 179 | 'Fehlt', 180 | ], $aElse9); 181 | } 182 | 183 | /** 184 | * @depends testSepRowAutoDetection 185 | */ 186 | public function testGetColumnDatatypes() { 187 | $this->csv->auto(__DIR__ . '/fixtures/datatype.csv'); 188 | $this->csv->getDatatypes(); 189 | $expected = [ 190 | 'title' => 'string', 191 | 'isbn' => 'string', 192 | 'publishedAt' => 'date', 193 | 'published' => 'boolean', 194 | 'count' => 'integer', 195 | 'price' => 'float', 196 | ]; 197 | 198 | $this->assertEquals($expected, $this->csv->data_types); 199 | } 200 | 201 | /** 202 | * @depends testSepRowAutoDetection 203 | */ 204 | public function testAutoDetectFileHasHeading() { 205 | $this->csv->auto(__DIR__ . '/fixtures/datatype.csv'); 206 | $this->assertTrue($this->csv->autoDetectFileHasHeading()); 207 | 208 | $this->csv->heading = false; 209 | $this->csv->auto(__DIR__ . '/fixtures/datatype.csv'); 210 | $this->assertTrue($this->csv->autoDetectFileHasHeading()); 211 | 212 | $this->csv->heading = false; 213 | $sInput = "86545235689\r\n34365587654\r\n13469874576"; 214 | $this->csv->auto($sInput); 215 | $this->assertFalse($this->csv->autoDetectFileHasHeading()); 216 | 217 | $this->csv->heading = true; 218 | $sInput = "86545235689\r\n34365587654\r\n13469874576"; 219 | $this->csv->auto($sInput); 220 | $this->assertFalse($this->csv->autoDetectFileHasHeading()); 221 | } 222 | 223 | public function testVeryLongNonExistingFile() { 224 | $this->csv->parse(str_repeat('long_string', PHP_MAXPATHLEN)); 225 | $this->csv->auto(str_repeat('long_string', PHP_MAXPATHLEN)); 226 | } 227 | 228 | protected function _get_magazines_data() { 229 | return [ 230 | [ 231 | 'title' => 'Красивая кулинария', 232 | 'isbn' => '5454-5587-3210', 233 | 'publishedAt' => '21.05.2011', 234 | ], 235 | [ 236 | 'title' => 'The Wine Connoisseurs', 237 | 'isbn' => '2547-8548-2541', 238 | 'publishedAt' => '12.12.2011', 239 | ], 240 | [ 241 | 'title' => 'Weißwein', 242 | 'isbn' => '1313-4545-8875', 243 | 'publishedAt' => '23.02.2012', 244 | ], 245 | ]; 246 | } 247 | 248 | public function autoQuotesDataProvider() { 249 | return array( 250 | array('auto-double-enclosure.csv', '"'), 251 | array('auto-single-enclosure.csv', "'"), 252 | ); 253 | } 254 | 255 | /** 256 | * @depends testSepRowAutoDetection 257 | * 258 | * @dataProvider autoQuotesDataProvider 259 | * 260 | * @param string $file 261 | * @param string $enclosure 262 | */ 263 | public function testAutoQuotes($file, $enclosure) { 264 | $csv = new Csv(); 265 | $csv->auto(__DIR__ . '/fixtures/' . $file, true, null, null, $enclosure); 266 | $this->assertArrayHasKey('column1', $csv->data[0], 'Data parsed incorrectly with enclosure ' . $enclosure); 267 | $this->assertEquals('value1', $csv->data[0]['column1'], 'Data parsed incorrectly with enclosure ' . $enclosure); 268 | } 269 | 270 | /** 271 | * Call protected/private method of a class. 272 | * 273 | * @param object &$object Instantiated object that we will run method on. 274 | * @param string $methodName Method name to call 275 | * @param array $parameters Array of parameters to pass into method. 276 | * 277 | * @return mixed Method return. 278 | */ 279 | private function invokeMethod(&$object, $methodName, array $parameters = array()) { 280 | $reflection = new \ReflectionClass(get_class($object)); 281 | $method = $reflection->getMethod($methodName); 282 | $method->setAccessible(true); 283 | 284 | return $method->invokeArgs($object, $parameters); 285 | } 286 | 287 | public function testWaiverFieldSeparator() { 288 | $this->assertSame(false, $this->csv->auto(__DIR__ . '/../example_files/waiver_field_separator.csv')); 289 | $expected = [ 290 | 'liability waiver', 291 | 'release of liability form', 292 | 'release of liability', 293 | 'sample waiver', 294 | 'sample waiver form', 295 | ]; 296 | $actual = array_column($this->csv->data, 'keyword'); 297 | $this->assertSame($expected, $actual); 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /parsecsv-for-php/tests/methods/SaveTest.php: -------------------------------------------------------------------------------- 1 | csv = new Csv(); 20 | $this->csv->auto(__DIR__ . '/../example_files/single_column.csv'); 21 | 22 | // Remove last 2 lines to simplify comparison 23 | unset($this->csv->data[2], $this->csv->data[3]); 24 | 25 | $temp_dir = str_replace("\\", '/', sys_get_temp_dir()); 26 | if (substr($temp_dir, -1) != '/') { 27 | // From the PHP.net documentation: 28 | // This function does not always add trailing slash. This behaviour 29 | // is inconsistent across systems. 30 | $temp_dir .= '/'; 31 | } 32 | $this->temp_filename = $temp_dir . 'parsecsv_test_file.csv'; 33 | } 34 | 35 | public function testSaveWithDefaultSettings() { 36 | $expected = "SMS\r0444\r5555\r"; 37 | $this->saveAndCompare($expected); 38 | } 39 | 40 | public function testSaveWithDosLineEnding() { 41 | $this->csv->linefeed = "\r\n"; 42 | $expected = "SMS\r\n0444\r\n5555\r\n"; 43 | $this->saveAndCompare($expected); 44 | } 45 | 46 | public function testSaveWithUnixLineEnding() { 47 | $this->csv->linefeed = "\n"; 48 | $expected = "SMS\n0444\n5555\n"; 49 | $this->saveAndCompare($expected); 50 | } 51 | 52 | public function testSaveWithNewHeader() { 53 | $this->csv->linefeed = "\n"; 54 | $this->csv->titles = array("NewTitle"); 55 | $expected = "NewTitle\n0444\n5555\n"; 56 | $this->saveAndCompare($expected); 57 | } 58 | 59 | public function testSaveWithoutHeader() { 60 | $this->csv->linefeed = "\n"; 61 | $this->csv->heading = false; 62 | $expected = "0444\n5555\n"; 63 | $this->saveAndCompare($expected); 64 | } 65 | 66 | public function testAllQuotes() { 67 | $this->csv->enclose_all = true; 68 | $expected = "\"SMS\"\r\"0444\"\r\"5555\"\r"; 69 | $this->saveAndCompare($expected); 70 | } 71 | 72 | private function saveAndCompare($expected) { 73 | $this->csv->save($this->temp_filename); 74 | $content = file_get_contents($this->temp_filename); 75 | $this->assertEquals($expected, $content); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /parsecsv-for-php/tests/methods/StreamTest.php: -------------------------------------------------------------------------------- 1 | auto(file_get_contents($filename))); 36 | self::assertCount(4, $csv->data); 37 | self::assertCount(6, reset($csv->data)); 38 | } 39 | 40 | public function testWriteStream() { 41 | $csv = new Csv(); 42 | $csv->linefeed = "\n"; 43 | $many_dots = str_repeat('.', 1000 * 1000); 44 | $csv->data = [ 45 | [ 46 | 'Name' => 'Rudolf', 47 | 'Question' => 'Which color is his nose?', 48 | ], 49 | [ 50 | 'Name' => 'Sponge Bob', 51 | 'Question' => 'Which shape are his pants?', 52 | ], 53 | [ 54 | 'Name' => $many_dots, 55 | 'Question' => 'Can you count one million dots?', 56 | ], 57 | ]; 58 | 59 | // Just export the first column, but with a new name 60 | $csv->titles = ['Name' => 'Character']; 61 | 62 | // Write data to our stream: 63 | $filename = 'example://data'; 64 | copy(__DIR__ . '/fixtures/datatype.csv', $filename); 65 | 66 | self::assertSame(true, $csv->save($filename)); 67 | $expected = "Character\nRudolf\nSponge Bob\n"; 68 | $expected .= $many_dots . "\n"; 69 | self::assertSame($expected, file_get_contents($filename)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /parsecsv-for-php/tests/methods/UnparseTest.php: -------------------------------------------------------------------------------- 1 | csv = new Csv(); 19 | $this->csv->auto(__DIR__ . '/fixtures/auto-double-enclosure.csv'); 20 | } 21 | 22 | public function testUnparseWithParameters() { 23 | $fields = array('a' => 'AA', 'b' => 'BB'); 24 | $data = [['a' => 'value1', 'b' => 'value2']]; 25 | $csv_object = new Csv(); 26 | $csv_string = $csv_object->unparse($data, $fields); 27 | $this->assertEquals("AA,BB\rvalue1,value2\r", $csv_string); 28 | 29 | $csv_object = new Csv(); 30 | $csv_object->linefeed = "\n"; 31 | $csv_string = $csv_object->unparse([[55, 66]]); 32 | $this->assertEquals("55,66\n", $csv_string); 33 | 34 | $csv_object = new Csv(); 35 | $data2 = [['a' => "multi\rline", 'b' => 'value2']]; 36 | $csv_object->enclosure = "'"; 37 | $csv_string = $csv_object->unparse($data2, $fields); 38 | $this->assertEquals("AA,BB\r'multi\rline',value2\r", $csv_string); 39 | } 40 | 41 | public function testUnparseDefault() { 42 | $expected = "column1,column2\rvalue1,value2\rvalue3,value4\r"; 43 | $this->unparseAndCompare($expected); 44 | } 45 | 46 | public function testUnparseDefaultWithoutHeading() { 47 | $this->csv->heading = false; 48 | $this->csv->auto(__DIR__ . '/fixtures/auto-double-enclosure.csv'); 49 | $expected = "column1,column2\rvalue1,value2\rvalue3,value4\r"; 50 | $this->unparseAndCompare($expected); 51 | 52 | } 53 | 54 | public function testUnparseRenameFields() { 55 | $expected = "C1,C2\rvalue1,value2\rvalue3,value4\r"; 56 | $this->unparseAndCompare($expected, array("C1", "C2")); 57 | } 58 | 59 | public function testReorderFields() { 60 | $expected = "column2,column1\rvalue2,value1\rvalue4,value3\r"; 61 | $this->unparseAndCompare($expected, array("column2", "column1")); 62 | } 63 | 64 | public function testSubsetFields() { 65 | $expected = "column1\rvalue1\rvalue3\r"; 66 | $this->unparseAndCompare($expected, array("column1")); 67 | } 68 | 69 | public function testReorderAndRenameFields() { 70 | $fields = array( 71 | 'column2' => 'C2', 72 | 'column1' => 'C1', 73 | ); 74 | $expected = "C2,C1\rvalue2,value1\rvalue4,value3\r"; 75 | $this->unparseAndCompare($expected, $fields); 76 | } 77 | 78 | public function testUnparseDefaultFirstRowMissing() { 79 | unset($this->csv->data[0]); 80 | $expected = "column1,column2\rvalue3,value4\r"; 81 | $this->unparseAndCompare($expected); 82 | } 83 | 84 | public function testUnparseDefaultWithoutData() { 85 | unset($this->csv->data[0]); 86 | unset($this->csv->data[1]); 87 | $expected = "column1,column2\r"; 88 | $this->unparseAndCompare($expected); 89 | } 90 | 91 | public function testObjectCells() { 92 | $this->csv->data = [ 93 | [ 94 | 'column1' => new ObjectThatHasToStringMethod(), 95 | 'column2' => 'boring', 96 | ], 97 | ]; 98 | $this->csv->linefeed = "\n"; 99 | $expected = "column1,column2\nsome value,boring\n"; 100 | $this->unparseAndCompare($expected); 101 | } 102 | 103 | private function unparseAndCompare($expected, $fields = array()) { 104 | $str = $this->csv->unparse($this->csv->data, $fields); 105 | $this->assertEquals($expected, $str); 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /parsecsv-for-php/tests/methods/fixtures/auto-double-enclosure.csv: -------------------------------------------------------------------------------- 1 | "column1","column2" 2 | "value1","value2" 3 | "value3","value4" -------------------------------------------------------------------------------- /parsecsv-for-php/tests/methods/fixtures/auto-single-enclosure.csv: -------------------------------------------------------------------------------- 1 | 'column1','column2' 2 | 'value1','value2' 3 | 'value3','value4' -------------------------------------------------------------------------------- /parsecsv-for-php/tests/methods/fixtures/datatype.csv: -------------------------------------------------------------------------------- 1 | sep=; 2 | title;isbn;publishedAt;published;count;price 3 | Красивая кулинария;5454-5587-3210;21.05.2011;true;1;10.99 4 | The Wine Connoisseurs;2547-8548-2541;12.12.2011;TRUE;;20.33 5 | Weißwein;1313-4545-8875;23.02.2012;false;10;10 6 | -------------------------------------------------------------------------------- /parsecsv-for-php/tests/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | properties/ 15 | methods/ 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /parsecsv-for-php/tests/properties/BaseClass.php: -------------------------------------------------------------------------------- 1 | csv = new Csv(); 24 | } 25 | 26 | protected function _compareWithExpected($expected) { 27 | $this->csv->auto(__DIR__ . '/../../examples/_books.csv'); 28 | $actual = array_map(function ($row) { 29 | return $row['title']; 30 | }, $this->csv->data); 31 | $this->assertEquals($expected, array_values($actual)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /parsecsv-for-php/tests/properties/ConditionsTest.php: -------------------------------------------------------------------------------- 1 | csv->conditions = 'author does not contain dan brown'; 9 | 10 | $this->_compareWithExpected([ 11 | 'The Killing Kind', 12 | 'The Third Secret', 13 | 'The Last Templar', 14 | 'The Traveller', 15 | 'Crisis Four', 16 | 'Prey', 17 | 'The Broker (Paperback)', 18 | 'Without Blood (Paperback)', 19 | 'State of Fear (Paperback)', 20 | 'The Rule of Four (Paperback)', 21 | ]); 22 | } 23 | 24 | public function testRatingEquals() { 25 | $rating_of_3 = [ 26 | 'The Last Templar', 27 | 'The Broker (Paperback)', 28 | 'Without Blood (Paperback)', 29 | ]; 30 | $this->csv->conditions = 'rating = 3'; 31 | $this->_compareWithExpected($rating_of_3); 32 | $this->csv->conditions = 'rating is 3'; 33 | $this->_compareWithExpected($rating_of_3); 34 | $this->csv->conditions = 'rating equals 3'; 35 | $this->_compareWithExpected($rating_of_3); 36 | } 37 | 38 | public function testRatingNotEquals() { 39 | $rating_not_4 = [ 40 | 'The Killing Kind', 41 | 'The Third Secret', 42 | 'The Last Templar', 43 | 'The Traveller', 44 | 'Prey', 45 | 'The Broker (Paperback)', 46 | 'Without Blood (Paperback)', 47 | 'State of Fear (Paperback)', 48 | 'Digital Fortress : A Thriller (Mass Market Paperback)', 49 | 'Angels & Demons (Mass Market Paperback)', 50 | ]; 51 | // $this->csv->conditions = 'rating != 4'; 52 | // $this->_compareWithExpected($rating_not_4); 53 | $this->csv->conditions = 'rating is not 4'; 54 | $this->_compareWithExpected($rating_not_4); 55 | // $this->csv->conditions = 'rating does not contain 4'; 56 | // $this->_compareWithExpected($rating_not_4); 57 | } 58 | 59 | public function testRatingLessThan() { 60 | $less_than_1 = [ 61 | 'The Killing Kind', 62 | 'The Third Secret', 63 | ]; 64 | $this->csv->conditions = 'rating < 1'; 65 | $this->_compareWithExpected($less_than_1); 66 | $this->csv->conditions = 'rating is less than 1'; 67 | $this->_compareWithExpected($less_than_1); 68 | } 69 | 70 | public function testRatingLessOrEquals() { 71 | $less_or_equals_1 = [ 72 | 'The Killing Kind', 73 | 'The Third Secret', 74 | ]; 75 | $this->csv->conditions = 'rating <= 1'; 76 | $this->_compareWithExpected($less_or_equals_1); 77 | $this->csv->conditions = 'rating is less than or equals 1'; 78 | $this->_compareWithExpected($less_or_equals_1); 79 | } 80 | 81 | public function testRatingGreaterThan() { 82 | $greater_4 = [ 83 | 'The Traveller', 84 | 'Prey', 85 | 'State of Fear (Paperback)', 86 | 'Digital Fortress : A Thriller (Mass Market Paperback)', 87 | 'Angels & Demons (Mass Market Paperback)', 88 | ]; 89 | $this->csv->conditions = 'rating > 4'; 90 | $this->_compareWithExpected($greater_4); 91 | $this->csv->conditions = 'rating is greater than 4'; 92 | $this->_compareWithExpected($greater_4); 93 | } 94 | 95 | public function testRatingGreaterOrEquals() { 96 | $greater_or_equal_4 = [ 97 | 'The Traveller', 98 | 'Crisis Four', 99 | 'Prey', 100 | 'State of Fear (Paperback)', 101 | 'The Rule of Four (Paperback)', 102 | 'Deception Point (Paperback)', 103 | 'Digital Fortress : A Thriller (Mass Market Paperback)', 104 | 'Angels & Demons (Mass Market Paperback)', 105 | 'The Da Vinci Code (Hardcover)', 106 | ]; 107 | $this->csv->conditions = 'rating >= 4'; 108 | $this->_compareWithExpected($greater_or_equal_4); 109 | $this->csv->conditions = 'rating is greater than or equals 4'; 110 | $this->_compareWithExpected($greater_or_equal_4); 111 | } 112 | 113 | public function testTitleContainsSecretOrCode() { 114 | $this->csv->conditions = 'title contains code OR title contains SECRET'; 115 | 116 | $this->_compareWithExpected([ 117 | 'The Third Secret', 118 | 'The Da Vinci Code (Hardcover)', 119 | ]); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /parsecsv-for-php/tests/properties/DefaultValuesPropertiesTest.php: -------------------------------------------------------------------------------- 1 | csv = new Csv(); 28 | } 29 | 30 | /** 31 | * Tear down 32 | * Tear down our test environment objects 33 | * 34 | * @access public 35 | */ 36 | public function tearDown() { 37 | $this->csv = null; 38 | } 39 | 40 | public function test_heading_default() { 41 | $this->assertTrue(is_bool($this->csv->heading)); 42 | $this->assertTrue($this->csv->heading); 43 | } 44 | 45 | public function test_fields_default() { 46 | $this->assertTrue(is_array($this->csv->fields)); 47 | $this->assertCount(0, $this->csv->fields); 48 | } 49 | 50 | public function test_sort_by_default() { 51 | $this->assertNull($this->csv->sort_by); 52 | } 53 | 54 | public function test_sort_reverse_default() { 55 | $this->assertTrue(is_bool($this->csv->sort_reverse)); 56 | $this->assertFalse($this->csv->sort_reverse); 57 | } 58 | 59 | public function test_sort_type_default() { 60 | $this->assertEquals('regular', $this->csv->sort_type); 61 | } 62 | 63 | public function test_delimiter_default() { 64 | $this->assertTrue(is_string($this->csv->delimiter)); 65 | $this->assertEquals(',', $this->csv->delimiter); 66 | } 67 | 68 | public function test_enclosure_default() { 69 | $this->assertTrue(is_string($this->csv->enclosure)); 70 | $this->assertEquals('"', $this->csv->enclosure); 71 | } 72 | 73 | public function test_enclose_all_default() { 74 | $this->assertTrue(is_bool($this->csv->enclose_all)); 75 | $this->assertFalse($this->csv->enclose_all); 76 | } 77 | 78 | public function test_conditions_default() { 79 | $this->assertNull($this->csv->conditions); 80 | } 81 | 82 | public function test_offset_default() { 83 | $this->assertNull($this->csv->offset); 84 | } 85 | 86 | public function test_limit_default() { 87 | $this->assertNull($this->csv->limit); 88 | } 89 | 90 | public function test_auto_depth_default() { 91 | $this->assertTrue(is_numeric($this->csv->auto_depth)); 92 | $this->assertEquals(15, $this->csv->auto_depth); 93 | } 94 | 95 | public function test_auto_non_chars_default() { 96 | $this->assertTrue(is_string($this->csv->auto_non_chars)); 97 | $this->assertEquals("a-zA-Z0-9\n\r", $this->csv->auto_non_chars); 98 | } 99 | 100 | public function test_auto_preferred_default() { 101 | $this->assertTrue(is_string($this->csv->auto_preferred)); 102 | $this->assertEquals(",;\t.:|", $this->csv->auto_preferred); 103 | } 104 | 105 | public function test_convert_encoding_default() { 106 | $this->assertTrue(is_bool($this->csv->convert_encoding)); 107 | $this->assertFalse($this->csv->convert_encoding); 108 | } 109 | 110 | public function test_input_encoding_default() { 111 | $this->assertTrue(is_string($this->csv->input_encoding)); 112 | $this->assertEquals('ISO-8859-1', $this->csv->input_encoding); 113 | } 114 | 115 | public function test_output_encoding_default() { 116 | $this->assertTrue(is_string($this->csv->output_encoding)); 117 | $this->assertEquals('ISO-8859-1', $this->csv->output_encoding); 118 | } 119 | 120 | public function test_linefeed_default() { 121 | $this->assertTrue(is_string($this->csv->linefeed)); 122 | $this->assertEquals("\r", $this->csv->linefeed); 123 | } 124 | 125 | public function test_output_delimiter_default() { 126 | $this->assertTrue(is_string($this->csv->output_delimiter)); 127 | $this->assertEquals(',', $this->csv->output_delimiter); 128 | } 129 | 130 | public function test_output_filename_default() { 131 | $this->assertTrue(is_string($this->csv->output_filename)); 132 | $this->assertEquals('data.csv', $this->csv->output_filename); 133 | } 134 | 135 | public function test_keep_file_data_default() { 136 | $this->assertTrue(is_bool($this->csv->keep_file_data)); 137 | $this->assertFalse($this->csv->keep_file_data); 138 | } 139 | 140 | public function test_file_default() { 141 | $this->assertNull($this->csv->file); 142 | } 143 | 144 | public function test_file_data_default() { 145 | $this->assertNull($this->csv->file_data); 146 | } 147 | 148 | public function test_error_default() { 149 | $this->assertTrue(is_numeric($this->csv->error)); 150 | $this->assertEquals(0, $this->csv->error); 151 | } 152 | 153 | public function test_error_info_default() { 154 | $this->assertTrue(is_array($this->csv->error_info)); 155 | $this->assertCount(0, $this->csv->error_info); 156 | } 157 | 158 | public function test_titles_default() { 159 | $this->assertTrue(is_array($this->csv->titles)); 160 | $this->assertCount(0, $this->csv->titles); 161 | } 162 | 163 | public function test_data_default() { 164 | $this->assertTrue(is_array($this->csv->data)); 165 | $this->assertCount(0, $this->csv->data); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /parsecsv-for-php/tests/properties/OffsetTest.php: -------------------------------------------------------------------------------- 1 | csv->offset = 1; 12 | $this->csv->auto(__DIR__ . '/../methods/fixtures/datatype.csv'); 13 | $this->assertCount(3, $this->csv->data); 14 | 15 | if (!function_exists('array_column')) { 16 | // function only available in PHP >= 5.5 17 | return; 18 | } 19 | $expected = [ 20 | 'Красивая кулинария', 21 | 'The Wine Connoisseurs', 22 | 'Weißwein', 23 | ]; 24 | $actual = array_column($this->csv->data, 'title'); 25 | $this->assertEquals($expected, $actual); 26 | } 27 | 28 | public function numberRangeZeroToFourProvider() { 29 | return array_map(function ($number) { 30 | return [$number]; 31 | }, range(0, 4)); 32 | } 33 | 34 | /** 35 | * @dataProvider numberRangeZeroToFourProvider 36 | * 37 | * @param integer $offset 38 | */ 39 | public function testOffsetOfOneNoHeader($offset) { 40 | $this->csv->offset = $offset; 41 | $this->csv->heading = false; 42 | $this->csv->auto(__DIR__ . '/../methods/fixtures/datatype.csv'); 43 | $this->assertCount(4 - $offset, $this->csv->data); 44 | } 45 | 46 | public function testDataArrayKeysWhenSettingOffsetWithHeading() { 47 | $this->csv->offset = 2; 48 | $this->csv->auto(__DIR__ . '/../methods/fixtures/datatype.csv'); 49 | $expected = [ 50 | [ 51 | 'title' => 'The Wine Connoisseurs', 52 | 'isbn' => '2547-8548-2541', 53 | 'publishedAt' => '12.12.2011', 54 | 'published' => 'TRUE', 55 | 'count' => '', 56 | 'price' => 20.33, 57 | ], 58 | [ 59 | 'title' => 'Weißwein', 60 | 'isbn' => '1313-4545-8875', 61 | 'publishedAt' => '23.02.2012', 62 | 'published' => 'false', 63 | 'count' => 10, 64 | 'price' => 10, 65 | ], 66 | ]; 67 | 68 | $this->assertEquals($expected, $this->csv->data); 69 | } 70 | 71 | public function testDataArrayKeysWhenSettingOffsetWithoutHeading() { 72 | $this->csv->heading = false; 73 | $this->csv->offset = 2; 74 | $this->csv->auto(__DIR__ . '/../methods/fixtures/datatype.csv'); 75 | $expected = range(0, 5, 1); 76 | 77 | $this->assertEquals($expected, array_keys($this->csv->data[0])); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /parsecsv-for-php/tests/properties/PublicPropertiesTest.php: -------------------------------------------------------------------------------- 1 | csv = new Csv(); 47 | 48 | //setup the reflection class 49 | $this->reflection = new \ReflectionClass($this->csv); 50 | 51 | //setup the reflected class properties 52 | $this->properties = $this->reflection->getProperties(); 53 | } 54 | 55 | /** 56 | * Tear down 57 | * Tear down our test environment objects 58 | * 59 | * @access public 60 | */ 61 | public function tearDown() { 62 | $this->csv = null; 63 | $this->reflection = null; 64 | $this->properties = null; 65 | } 66 | 67 | /** 68 | * test_propertiesCount 69 | * Counts the number of properties to make sure we didn't add or 70 | * subtract any without thinking 71 | * 72 | * @access public 73 | */ 74 | public function test_propertiesCount() { 75 | $this->assertCount(29, $this->properties); 76 | } 77 | 78 | /** 79 | * test_property_names 80 | * We have an expected set of properties that should exists 81 | * Make sure our expected number of properties matches the real 82 | * count of properties and also check to make sure our expected 83 | * properties exists within the class 84 | * 85 | * @access public 86 | */ 87 | public function test_property_names() { 88 | //set our expected properties name(s) 89 | $expected_names = array( 90 | 'heading', 91 | 'fields', 92 | 'sort_by', 93 | 'sort_reverse', 94 | 'sort_type', 95 | 'delimiter', 96 | 'enclosure', 97 | 'enclose_all', 98 | 'conditions', 99 | 'offset', 100 | 'limit', 101 | 'auto_depth', 102 | 'auto_non_chars', 103 | 'auto_preferred', 104 | 'convert_encoding', 105 | 'input_encoding', 106 | 'output_encoding', 107 | 'use_mb_convert_encoding', 108 | 'linefeed', 109 | 'output_delimiter', 110 | 'output_filename', 111 | 'keep_file_data', 112 | 'file', 113 | 'file_data', 114 | 'error', 115 | 'error_info', 116 | 'titles', 117 | 'data', 118 | 'data_types', 119 | ); 120 | 121 | // Find our real properties 122 | $real_properties = array_map(function (\ReflectionProperty $property) { 123 | return $property->getName(); 124 | }, $this->properties); 125 | 126 | // Lets make sure our expected matches the number of real properties 127 | $this->assertEquals($expected_names, $real_properties); 128 | } 129 | 130 | /** 131 | * test_count_public_properties 132 | * We at this point only have public properties so 133 | * lets verify all properties are public 134 | * 135 | * @access public 136 | */ 137 | public function test_count_public_properties() { 138 | $counter = 0; 139 | 140 | $propertiesCount = count($this->properties); 141 | for ($a = 0; $a < $propertiesCount; $a++) { 142 | if ($this->properties[$a]->isPublic() === true) { 143 | $counter++; 144 | } 145 | } 146 | 147 | $this->assertCount($counter, $this->properties); 148 | } 149 | 150 | public function testDefaultSortTypeIsRegular() { 151 | $this->assertEquals(SortEnum::SORT_TYPE_REGULAR, $this->csv->sort_type); 152 | } 153 | 154 | public function testSetSortType() { 155 | $this->csv->sort_type = 'numeric'; 156 | $this->assertEquals(SortEnum::SORT_TYPE_NUMERIC, $this->csv->sort_type); 157 | 158 | $this->csv->sort_type = 'string'; 159 | $this->assertEquals(SortEnum::SORT_TYPE_STRING, $this->csv->sort_type); 160 | } 161 | 162 | public function testGetSorting() { 163 | $this->csv->sort_type = 'numeric'; 164 | $sorting = SortEnum::getSorting($this->csv->sort_type); 165 | $this->assertEquals(SORT_NUMERIC, $sorting); 166 | 167 | $this->csv->sort_type = 'string'; 168 | $sorting = SortEnum::getSorting($this->csv->sort_type); 169 | $this->assertEquals(SORT_STRING, $sorting); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /parsecsv-for-php/tests/properties/SortByTest.php: -------------------------------------------------------------------------------- 1 | csv->sort_by = 'rating'; 9 | $this->csv->conditions = 'title does not contain Blood'; 10 | $this->_compareWithExpected([ 11 | // Rating 0 12 | 'The Killing Kind', 13 | 'The Third Secret', 14 | 15 | // Rating 3 16 | 'The Last Templar', 17 | 'The Broker (Paperback)', 18 | 19 | // Rating 4 20 | 'Deception Point (Paperback)', 21 | 'The Rule of Four (Paperback)', 22 | 'The Da Vinci Code (Hardcover)', 23 | 24 | // Rating 5 25 | 'State of Fear (Paperback)', 26 | 'Prey', 27 | 'Digital Fortress : A Thriller (Mass Market Paperback)', 28 | 'Angels & Demons (Mass Market Paperback)', 29 | ]); 30 | } 31 | 32 | public function testReverseSortByRating() { 33 | $this->csv->sort_by = 'rating'; 34 | $this->csv->conditions = 35 | 'title does not contain Prey AND ' . 36 | 'title does not contain Fortress AND ' . 37 | 'title does not contain Blood AND ' . 38 | 'title does not contain Fear'; 39 | $this->csv->sort_reverse = true; 40 | $this->_compareWithExpected([ 41 | 42 | // Rating 5 43 | 'Angels & Demons (Mass Market Paperback)', 44 | 'The Traveller', 45 | 46 | // Rating 4 47 | 'The Da Vinci Code (Hardcover)', 48 | 'The Rule of Four (Paperback)', 49 | 'Deception Point (Paperback)', 50 | 51 | // Rating 3 52 | 'The Broker (Paperback)', 53 | 'The Last Templar', 54 | 55 | // Rating 0 56 | 'The Third Secret', 57 | 'The Killing Kind', 58 | ]); 59 | } 60 | } 61 | --------------------------------------------------------------------------------