classes
3 days ago
export
3 days ago
forms
3 days ago
samples
3 days ago
column.php
3 days ago
exportable.php
3 days ago
factory.php
3 days ago
index.html
3 days ago
object.php
3 days ago
object.php
884 lines
| 1 | <?php |
| 2 | /** |
| 3 | * @package VikAppointments |
| 4 | * @subpackage core |
| 5 | * @author E4J s.r.l. |
| 6 | * @copyright Copyright (C) 2021 E4J s.r.l. All Rights Reserved. |
| 7 | * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL |
| 8 | * @link https://vikwp.com |
| 9 | */ |
| 10 | |
| 11 | // No direct access |
| 12 | defined('ABSPATH') or die('No script kiddies please!'); |
| 13 | |
| 14 | VAPLoader::import('libraries.import.column'); |
| 15 | |
| 16 | /** |
| 17 | * Class used to handle a generic import event. |
| 18 | * This class is able to import a list of records starting |
| 19 | * from a CSV file. |
| 20 | * |
| 21 | * The CSV must start with a valid heading, otherwise the first row |
| 22 | * will be skipped. |
| 23 | * |
| 24 | * @since 1.6 |
| 25 | */ |
| 26 | class ImportObject |
| 27 | { |
| 28 | /** |
| 29 | * The XML instructions object. |
| 30 | * |
| 31 | * @var SimpleXMLElement |
| 32 | */ |
| 33 | protected $xml; |
| 34 | |
| 35 | /** |
| 36 | * The import entity type. |
| 37 | * |
| 38 | * @var string |
| 39 | */ |
| 40 | protected $type; |
| 41 | |
| 42 | /** |
| 43 | * Property used to load the table columns only once. |
| 44 | * |
| 45 | * @var array |
| 46 | * @since 1.7 |
| 47 | */ |
| 48 | protected $columns = null; |
| 49 | |
| 50 | /** |
| 51 | * The filter input handler. |
| 52 | * |
| 53 | * @var mixed |
| 54 | */ |
| 55 | protected $filter; |
| 56 | |
| 57 | /** |
| 58 | * The path of the file to import. |
| 59 | * |
| 60 | * @var string |
| 61 | */ |
| 62 | protected $file = null; |
| 63 | |
| 64 | /** |
| 65 | * The total number of records fetched. |
| 66 | * Used by both the import and export methods. |
| 67 | * |
| 68 | * @var integer |
| 69 | */ |
| 70 | protected $total = 0; |
| 71 | |
| 72 | /** |
| 73 | * A list of errors. |
| 74 | * |
| 75 | * @var array |
| 76 | */ |
| 77 | protected $errors = array(); |
| 78 | |
| 79 | /** |
| 80 | * Class constructor. |
| 81 | * |
| 82 | * @param object $xml The XML object. |
| 83 | * @param string $type The entity type to import. |
| 84 | * |
| 85 | */ |
| 86 | public function __construct($xml, $type) |
| 87 | { |
| 88 | $this->xml = $xml; |
| 89 | $this->type = $type; |
| 90 | $this->filter = JFilterInput::getInstance(); |
| 91 | } |
| 92 | |
| 93 | /** |
| 94 | * Returns the path of the file to import. |
| 95 | * The path found is always cached to avoid retrieving |
| 96 | * it during the next accesses. |
| 97 | * |
| 98 | * @return mixed The file path if exists, otherwise false. |
| 99 | */ |
| 100 | public function getFile() |
| 101 | { |
| 102 | if ($this->file === null) |
| 103 | { |
| 104 | $folder = VAPADMIN . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR; |
| 105 | |
| 106 | $file = glob($folder . $this->type . '_*.csv'); |
| 107 | |
| 108 | if (count($file)) |
| 109 | { |
| 110 | $this->file = array_pop($file); |
| 111 | } |
| 112 | else |
| 113 | { |
| 114 | $this->file = false; |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | return $this->file; |
| 119 | } |
| 120 | |
| 121 | /** |
| 122 | * Checks if the file is ready to be imported. |
| 123 | * |
| 124 | * @return boolean True if the file exists, otherwise false. |
| 125 | * |
| 126 | * @uses getFile() |
| 127 | */ |
| 128 | public function hasFile() |
| 129 | { |
| 130 | return (bool) $this->getFile(); |
| 131 | } |
| 132 | |
| 133 | /** |
| 134 | * Returns the database table to use while importing the records. |
| 135 | * |
| 136 | * @param boolean True to return a database table instance. |
| 137 | * Otherwise the database table name will be returned. |
| 138 | * |
| 139 | * @return mixed The DB table name or a JTable instance. |
| 140 | */ |
| 141 | public function getTable($object = false) |
| 142 | { |
| 143 | /** |
| 144 | * Check whether we should return a JTable instance. |
| 145 | * |
| 146 | * @since 1.7 |
| 147 | */ |
| 148 | if ($object) |
| 149 | { |
| 150 | // create table instance |
| 151 | return JTableVAP::getInstance($this->xml->table->attributes()->id, 'VAPTable'); |
| 152 | } |
| 153 | |
| 154 | // return table name |
| 155 | return (string) $this->xml->table->attributes()->name; |
| 156 | } |
| 157 | |
| 158 | /** |
| 159 | * Returns the primary key of the database table. |
| 160 | * |
| 161 | * @param string $def The default primary key to use |
| 162 | * if not specified. |
| 163 | * |
| 164 | * @return string The primary key. |
| 165 | */ |
| 166 | public function getPrimaryKey($def = 'id') |
| 167 | { |
| 168 | $pk = (string) $this->xml->table->attributes()->pk; |
| 169 | |
| 170 | if (empty($pk)) |
| 171 | { |
| 172 | $pk = $def; |
| 173 | } |
| 174 | |
| 175 | return $pk; |
| 176 | } |
| 177 | |
| 178 | /** |
| 179 | * Returns all the available columns that can be assigned |
| 180 | * to the values listed in the CSV file. |
| 181 | * @since 1.7 columns are always translated and loaded once. |
| 182 | * |
| 183 | * @return array The list of available columns. |
| 184 | */ |
| 185 | public function getColumns() |
| 186 | { |
| 187 | if (is_null($this->columns)) |
| 188 | { |
| 189 | $this->columns = array(); |
| 190 | |
| 191 | foreach ($this->xml->table->column as $column) |
| 192 | { |
| 193 | /** |
| 194 | * Use an apposite instance to hold the column attributes. |
| 195 | * For backward compatibility, the instance will act as |
| 196 | * a plain object. |
| 197 | * |
| 198 | * @since 1.7 |
| 199 | */ |
| 200 | $obj = ImportColumn::getInstance($column); |
| 201 | |
| 202 | // skip in case the column didn't specify a name |
| 203 | if ($obj->name) |
| 204 | { |
| 205 | $this->columns[$obj->name] = $obj; |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | $dispatcher = VAPFactory::getEventDispatcher(); |
| 210 | |
| 211 | /** |
| 212 | * Trigger event to let external plugins be able to include |
| 213 | * additional columns that are not mentioned within the default |
| 214 | * XML file of this import/export type. |
| 215 | * |
| 216 | * Any attached element should support the following parameters: |
| 217 | * - name string the column name equals to array key (mandatory); |
| 218 | * - label string the column readable label (optional); |
| 219 | * - required bool true if required (optional); |
| 220 | * - default mixed the default value if missing (optional); |
| 221 | * - filter string the filter type (optional); |
| 222 | * - type string the type of column (optional); |
| 223 | * - options array an array of placeholders (optional); |
| 224 | * |
| 225 | * @param string $type The current import/export type. |
| 226 | * |
| 227 | * @return array A list of columns to append. |
| 228 | * |
| 229 | * @since 1.7 |
| 230 | */ |
| 231 | $results = $dispatcher->trigger('onLoadImportExportColumns', array($this->type)); |
| 232 | |
| 233 | // iterate all returned results |
| 234 | foreach ($results as $arr) |
| 235 | { |
| 236 | foreach ($arr as $column) |
| 237 | { |
| 238 | // instantiate column with returned properties |
| 239 | $obj = ImportColumn::getInstance($column); |
| 240 | |
| 241 | // skip in case the column didn't specify a name |
| 242 | if ($obj->name) |
| 243 | { |
| 244 | $this->columns[$obj->name] = $obj; |
| 245 | } |
| 246 | } |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | return $this->columns; |
| 251 | } |
| 252 | |
| 253 | /** |
| 254 | * Returns the cancellation task, if any. |
| 255 | * |
| 256 | * @return mixed The cancel task if specified, otherwise false. |
| 257 | */ |
| 258 | public function getCancelTask() |
| 259 | { |
| 260 | $task = (string) $this->xml->cancel->attributes()->task; |
| 261 | |
| 262 | if (empty($task)) |
| 263 | { |
| 264 | return false; |
| 265 | } |
| 266 | |
| 267 | return $task; |
| 268 | } |
| 269 | |
| 270 | /** |
| 271 | * Returns the file containing the sample data to import this type of object. |
| 272 | * |
| 273 | * @return mixed The file path if exists, otherwise false. |
| 274 | */ |
| 275 | public function getSampleFile() |
| 276 | { |
| 277 | $sample = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'samples' . DIRECTORY_SEPARATOR . $this->type . '.csv'; |
| 278 | |
| 279 | if (file_exists($sample)) |
| 280 | { |
| 281 | return $sample; |
| 282 | } |
| 283 | |
| 284 | return false; |
| 285 | } |
| 286 | |
| 287 | /** |
| 288 | * Checks if this object owns any sample data file. |
| 289 | * |
| 290 | * @return boolean True if exists, otherwise false. |
| 291 | * |
| 292 | * @uses getSampleFile() |
| 293 | */ |
| 294 | public function hasSampleFile() |
| 295 | { |
| 296 | return $this->getSampleFile() !== false; |
| 297 | } |
| 298 | |
| 299 | /** |
| 300 | * Returns a preview of the records contained in the file. |
| 301 | * |
| 302 | * @param integer $lim The maximum number of records to obtain. |
| 303 | * |
| 304 | * @return array The records list. |
| 305 | * |
| 306 | * @uses getFile() |
| 307 | */ |
| 308 | public function getRecords($lim = 10) |
| 309 | { |
| 310 | $rows = array(); |
| 311 | |
| 312 | $file = $this->getFile(); |
| 313 | |
| 314 | $handle = fopen($file, 'r'); |
| 315 | |
| 316 | /** |
| 317 | * Auto-detect the best separator and enclosure to use. |
| 318 | * |
| 319 | * @since 1.7.8 |
| 320 | */ |
| 321 | list($separator, $enclosure) = $this->detectCsvFormat($handle); |
| 322 | |
| 323 | $count = 0; |
| 324 | |
| 325 | while (($buffer = fgetcsv($handle, $length = 0, $separator, $enclosure, $escape = '')) && $count <= $lim) |
| 326 | { |
| 327 | $rows[] = $buffer; |
| 328 | $count++; |
| 329 | } |
| 330 | |
| 331 | fclose($handle); |
| 332 | |
| 333 | return $rows; |
| 334 | } |
| 335 | |
| 336 | /** |
| 337 | * Returns the total number of records fetched. |
| 338 | * |
| 339 | * @return integer The total count. |
| 340 | */ |
| 341 | public function getTotalCount() |
| 342 | { |
| 343 | return $this->total; |
| 344 | } |
| 345 | |
| 346 | /** |
| 347 | * Pushes a new error in the list. |
| 348 | * |
| 349 | * @param object $data The record failed. |
| 350 | * @param string $err The error message. |
| 351 | * |
| 352 | * @return self This object to support chaining. |
| 353 | */ |
| 354 | protected function setError($data, $err = '') |
| 355 | { |
| 356 | if (empty($err)) |
| 357 | { |
| 358 | $err = JText::translate('VAPIMPORTINSERTERR'); |
| 359 | } |
| 360 | |
| 361 | $str = '<b>' . $err . '</b><br />'; |
| 362 | |
| 363 | $data = (array) $data; |
| 364 | |
| 365 | if ($data) |
| 366 | { |
| 367 | $str .= '<pre>' . implode(', ', $data) . '</pre>'; |
| 368 | } |
| 369 | |
| 370 | $this->errors[] = $str; |
| 371 | |
| 372 | return $this; |
| 373 | } |
| 374 | |
| 375 | /** |
| 376 | * Returns a list of errors raised. |
| 377 | * |
| 378 | * @return array An errors list. |
| 379 | */ |
| 380 | public function getErrors() |
| 381 | { |
| 382 | return $this->errors; |
| 383 | } |
| 384 | |
| 385 | /** |
| 386 | * Processes the event to import all the records |
| 387 | * contained in the CSV file. |
| 388 | * |
| 389 | * @param array $assoc Associative array used to match |
| 390 | * the columns of the table with the columns |
| 391 | * of the CSV records. |
| 392 | * @param array $args Associative list of additional parameters. |
| 393 | * |
| 394 | * @return integer The number of imported records. |
| 395 | * |
| 396 | * @uses getColumns() |
| 397 | * @uses getFile() |
| 398 | * @uses getTable() |
| 399 | * @uses getPrimaryKey() |
| 400 | * @uses bind() |
| 401 | */ |
| 402 | public function save(array $assoc, array $args = array()) |
| 403 | { |
| 404 | $dbo = JFactory::getDbo(); |
| 405 | |
| 406 | $cols = $this->getColumns(); |
| 407 | $file = $this->getFile(); |
| 408 | $table = $this->getTable($object = true); |
| 409 | $pk = $this->getPrimaryKey(); |
| 410 | |
| 411 | // make sure this object supports a database table instance |
| 412 | if (!$table) |
| 413 | { |
| 414 | // nope, lets load the database table name |
| 415 | $table = $this->getTable(); |
| 416 | } |
| 417 | |
| 418 | $handle = fopen($file, 'r'); |
| 419 | |
| 420 | $count = $this->total = 0; |
| 421 | |
| 422 | // reset errors list |
| 423 | $this->errors = array(); |
| 424 | |
| 425 | $head = null; |
| 426 | |
| 427 | /** |
| 428 | * Auto-detect the best separator and enclosure to use. |
| 429 | * |
| 430 | * @since 1.7.8 |
| 431 | */ |
| 432 | list($separator, $enclosure) = $this->detectCsvFormat($handle); |
| 433 | |
| 434 | while (($buffer = fgetcsv($handle, $length = 0, $separator, $enclosure, $escape = ''))) |
| 435 | { |
| 436 | if ($head === null) |
| 437 | { |
| 438 | $head = $buffer; |
| 439 | } |
| 440 | else |
| 441 | { |
| 442 | $record = new stdClass; |
| 443 | $valid = true; |
| 444 | |
| 445 | foreach ($buffer as $k => $v) |
| 446 | { |
| 447 | if (!empty($assoc[$head[$k]])) |
| 448 | { |
| 449 | // get the column related to the specified CSV head |
| 450 | $column = $assoc[$head[$k]]; |
| 451 | |
| 452 | // if the value is empty, try to use the default column value |
| 453 | if (strlen($v) == 0 && !empty($cols[$column]->default)) |
| 454 | { |
| 455 | $v = $cols[$column]->default; |
| 456 | } |
| 457 | else |
| 458 | { |
| 459 | /** |
| 460 | * Check whether this column requires some adjustments. |
| 461 | * |
| 462 | * @since 1.7 |
| 463 | */ |
| 464 | $v = $cols[$column]->onImport($v); |
| 465 | } |
| 466 | |
| 467 | // try to filter the specified value |
| 468 | if (!empty($cols[$column]->filter)) |
| 469 | { |
| 470 | $v = $this->filter->clean($v, $cols[$column]->filter); |
| 471 | } |
| 472 | |
| 473 | // check if the value MUST NOT be empty |
| 474 | if (empty($v) && $cols[$column]->required) |
| 475 | { |
| 476 | // empty required value, the object |
| 477 | // should not be imported |
| 478 | $valid = false; |
| 479 | } |
| 480 | |
| 481 | $record->{$assoc[$head[$k]]} = $v; |
| 482 | } |
| 483 | } |
| 484 | |
| 485 | if ($valid && $this->bind($record, $args)) |
| 486 | { |
| 487 | $msg = null; |
| 488 | |
| 489 | if ($table instanceof JTable) |
| 490 | { |
| 491 | // always reset table before creating a new record |
| 492 | $table->reset(); |
| 493 | // unset primary key |
| 494 | $record->{$table->getKeyName()} = 0; |
| 495 | |
| 496 | // attempt to save by using the table instance |
| 497 | $res = $table->save($record); |
| 498 | |
| 499 | if (!$res) |
| 500 | { |
| 501 | // get registered error message |
| 502 | $msg = $table->getError(); |
| 503 | } |
| 504 | } |
| 505 | else |
| 506 | { |
| 507 | try |
| 508 | { |
| 509 | // table not supported, use direct DB insert |
| 510 | $res = $dbo->insertObject($table, $record, $pk) && $record->{$pk}; |
| 511 | } |
| 512 | catch (Exception $e) |
| 513 | { |
| 514 | $res = false; |
| 515 | $msg = $e->getMessage(); |
| 516 | } |
| 517 | } |
| 518 | |
| 519 | if ($res) |
| 520 | { |
| 521 | // imported |
| 522 | $count++; |
| 523 | } |
| 524 | else |
| 525 | { |
| 526 | // an error occurred |
| 527 | $this->setError($record, $msg); |
| 528 | } |
| 529 | } |
| 530 | |
| 531 | $this->total++; |
| 532 | } |
| 533 | } |
| 534 | |
| 535 | fclose($handle); |
| 536 | |
| 537 | return $count; |
| 538 | } |
| 539 | |
| 540 | /** |
| 541 | * Method used to bind the provided object. By returning |
| 542 | * false the system won't proceed importing the current record. |
| 543 | * |
| 544 | * A record won't be imported if it doesn't own any property. |
| 545 | * |
| 546 | * @param object &$data The object of the record to import. |
| 547 | * @param array $args Associative list of additional parameters. |
| 548 | * |
| 549 | * @return boolean True if the record should be imported, otherwise false. |
| 550 | */ |
| 551 | protected function bind(&$data, array $args = array()) |
| 552 | { |
| 553 | $vars = get_object_vars($data); |
| 554 | |
| 555 | return !is_null($vars) && count(array_keys($vars)); |
| 556 | } |
| 557 | |
| 558 | /** |
| 559 | * Returns a list of the records to export. |
| 560 | * @since 1.7 $full argument has been replaced by an array of options, |
| 561 | * which now includes it as attribute. |
| 562 | * |
| 563 | * @param array $options An array of export options. |
| 564 | * - full bool true to use a query limit; |
| 565 | * - raw bool true to format the records values; |
| 566 | * - columns array a list of columns to export. |
| 567 | * |
| 568 | * @return array The records to export. |
| 569 | * |
| 570 | * @uses buildExportQuery() |
| 571 | * @uses formatRecords() |
| 572 | */ |
| 573 | public function getExportableRows($options = array()) |
| 574 | { |
| 575 | $app = JFactory::getApplication(); |
| 576 | $dbo = JFactory::getDbo(); |
| 577 | |
| 578 | // check whether the options is a boolean for BC |
| 579 | if (is_bool($options)) |
| 580 | { |
| 581 | // define options with given full value |
| 582 | $options = array('full' => $options); |
| 583 | } |
| 584 | |
| 585 | // include application and database within options array |
| 586 | $options['app'] = $app; |
| 587 | $options['dbo'] = $dbo; |
| 588 | |
| 589 | // Create a registry for ease of use. |
| 590 | // Use JObject in place of JRegistry because this |
| 591 | // one accepts only standard elements. |
| 592 | $options = new JObject($options); |
| 593 | |
| 594 | $q = $this->buildExportQuery($options); |
| 595 | |
| 596 | if ($options->get('full', false)) |
| 597 | { |
| 598 | // unset limit |
| 599 | $lim0 = $lim = null; |
| 600 | } |
| 601 | else |
| 602 | { |
| 603 | $lim0 = 0; |
| 604 | $lim = 10; |
| 605 | } |
| 606 | |
| 607 | $dbo->setQuery($q, $lim0, $lim); |
| 608 | $rows = $dbo->loadAssocList(); |
| 609 | |
| 610 | if ($rows) |
| 611 | { |
| 612 | /** |
| 613 | * Check whether we should format the record |
| 614 | * while exporting them. |
| 615 | * |
| 616 | * @since 1.7 |
| 617 | */ |
| 618 | if (!$options->get('raw')) |
| 619 | { |
| 620 | $this->formatRecords($rows); |
| 621 | } |
| 622 | |
| 623 | // get the total number of rows |
| 624 | $dbo->setQuery('SELECT FOUND_ROWS();'); |
| 625 | $this->total = $dbo->loadResult(); |
| 626 | |
| 627 | return $rows; |
| 628 | } |
| 629 | |
| 630 | return array(); |
| 631 | } |
| 632 | |
| 633 | /** |
| 634 | * Builds the base query to export all the records. |
| 635 | * @since 1.7 $app and $dbo are now included within the $options argument. |
| 636 | * |
| 637 | * @param JObject $options A registry of export options. |
| 638 | * @param string $alias The table alias. |
| 639 | * |
| 640 | * @return mixed The query builder object. |
| 641 | * |
| 642 | * @uses getColumns() |
| 643 | * @uses getTable() |
| 644 | * @uses getPrimaryKey() |
| 645 | */ |
| 646 | protected function buildExportQuery($options, $alias = '') |
| 647 | { |
| 648 | $app = $options->get('app'); |
| 649 | $dbo = $options->get('dbo'); |
| 650 | |
| 651 | $columns = $this->getColumns(); |
| 652 | $table = $this->getTable(); |
| 653 | $pk = $this->getPrimaryKey(); |
| 654 | |
| 655 | $q = $dbo->getQuery(true); |
| 656 | |
| 657 | /** |
| 658 | * Implemented table alias to support joins with |
| 659 | * external tables in children classes. |
| 660 | * |
| 661 | * @since 1.7 |
| 662 | */ |
| 663 | $alias = $alias ? $alias : 't'; |
| 664 | |
| 665 | // get list of columns to introduce within the query |
| 666 | $queryColumns = array_keys($columns); |
| 667 | |
| 668 | /** |
| 669 | * Check whether we need to exclude some columns from the query. |
| 670 | * |
| 671 | * @since 1.7 |
| 672 | */ |
| 673 | if ($selectedColumns = $options->get('columns', array())) |
| 674 | { |
| 675 | // take only the columns included within the list |
| 676 | $queryColumns = array_filter($queryColumns, function($col) use ($selectedColumns) |
| 677 | { |
| 678 | return in_array($col, $selectedColumns); |
| 679 | }); |
| 680 | } |
| 681 | |
| 682 | // Calculate the total number of records fetched. |
| 683 | // Pop the first column to concat SQL_CALC_FOUND_ROWS. |
| 684 | $q->select('SQL_CALC_FOUND_ROWS ' . $dbo->qn($alias . '.' . array_shift($queryColumns))); |
| 685 | |
| 686 | // map the columns to select |
| 687 | foreach ($queryColumns as $col) |
| 688 | { |
| 689 | $q->select($dbo->qn($alias . '.' . $col)); |
| 690 | } |
| 691 | |
| 692 | // define the table to access |
| 693 | $q->from($dbo->qn($table, $alias)); |
| 694 | |
| 695 | $ids = $app->input->get('cid', array(), 'string'); |
| 696 | |
| 697 | if (count($ids)) |
| 698 | { |
| 699 | // map the array to quote each element |
| 700 | $ids = array_map(array($dbo, 'q'), $ids); |
| 701 | // build IN statement |
| 702 | $q->where($dbo->qn($alias . '.' . $pk) . ' IN (' . implode(', ', $ids) . ')'); |
| 703 | } |
| 704 | |
| 705 | // create hook for query manipulation |
| 706 | $event = 'onBeforeListQuery' . ucfirst($this->type); |
| 707 | |
| 708 | // Create a dummy object to replicate the behavior of a view. |
| 709 | // Not the best solution but does its job, at least until |
| 710 | // List models will be implemented... |
| 711 | $view = new stdClass; |
| 712 | $view->filters = array(); |
| 713 | $view->ordering = 'id'; |
| 714 | $view->orderingDir = 'asc'; |
| 715 | |
| 716 | /** |
| 717 | * Replicate same hook used by the view in order to keep the |
| 718 | * custom filters also while exporting the records. |
| 719 | * |
| 720 | * @since 1.7 |
| 721 | */ |
| 722 | VAPFactory::getEventDispatcher()->trigger($event, array(&$q, $view)); |
| 723 | |
| 724 | return $q; |
| 725 | } |
| 726 | |
| 727 | /** |
| 728 | * Formats the records according to the type of the columns |
| 729 | * specified with the XML manifest. |
| 730 | * |
| 731 | * @param array &$rows The rows to export. |
| 732 | * |
| 733 | * @return void |
| 734 | * |
| 735 | * @since 1.7 |
| 736 | * |
| 737 | * @uses getColumns() |
| 738 | */ |
| 739 | protected function formatRecords(&$rows) |
| 740 | { |
| 741 | // get support columns |
| 742 | $columns = $this->getColumns(); |
| 743 | |
| 744 | // iterate exported records |
| 745 | foreach ($rows as $i => $row) |
| 746 | { |
| 747 | // iterate columns of current record |
| 748 | foreach ($row as $k => $v) |
| 749 | { |
| 750 | // make sure the column exists because the query of the import handler |
| 751 | // might have manually selected the columns of another table |
| 752 | if (isset($columns[$k])) |
| 753 | { |
| 754 | // attempt to format the value |
| 755 | $rows[$i][$k] = $columns[$k]->format($v); |
| 756 | } |
| 757 | } |
| 758 | } |
| 759 | } |
| 760 | |
| 761 | /** |
| 762 | * Exports the records using the given handler. |
| 763 | * |
| 764 | * @param Exportable $handler The export handler. |
| 765 | * @param string $name The file name. |
| 766 | * @param array $options An array of export options. |
| 767 | * |
| 768 | * @return void |
| 769 | * |
| 770 | * @uses getExportableRows() |
| 771 | */ |
| 772 | public function export($handler, $name) |
| 773 | { |
| 774 | /** |
| 775 | * Extract export options from handler. |
| 776 | * |
| 777 | * @since 1.7 |
| 778 | */ |
| 779 | $options = $handler->getOptions(); |
| 780 | |
| 781 | // ignore query limits |
| 782 | $options['full'] = true; |
| 783 | |
| 784 | // get rows to export |
| 785 | $rows = $this->getExportableRows($options); |
| 786 | |
| 787 | if (!$name) |
| 788 | { |
| 789 | $name = strtolower($this->type); |
| 790 | } |
| 791 | |
| 792 | // export the records |
| 793 | $handler->download($name, $rows, $this); |
| 794 | } |
| 795 | |
| 796 | /** |
| 797 | * Detects the separator and enclosure used by the specified CSV file. |
| 798 | * |
| 799 | * @param object $file The file pointer of the CSV to scan. |
| 800 | * @param int $checkLines The number of lines to read and test. |
| 801 | * |
| 802 | * @return string[] A 2-elements array holding the separator and the enclosure. |
| 803 | * |
| 804 | * @since 1.7.8 |
| 805 | */ |
| 806 | protected function detectCsvFormat($file, int $checkLines = 5) { |
| 807 | // define all the supported separators and enclosures |
| 808 | $separators = [",", ";", "\t", "|"]; |
| 809 | $enclosures = ['"', "'"]; |
| 810 | |
| 811 | $lines = []; |
| 812 | |
| 813 | // read the first N lines to detect the separator and the enclosure |
| 814 | while (($line = fgets($file)) !== false && count($lines) < $checkLines) |
| 815 | { |
| 816 | $lines[] = trim($line); |
| 817 | } |
| 818 | |
| 819 | // move the cursor at the beginning of the file |
| 820 | rewind($file); |
| 821 | |
| 822 | // detect the separator used in the CSV |
| 823 | $bestSeparator = ','; |
| 824 | $maxFields = 0; |
| 825 | |
| 826 | // test all the supported separators |
| 827 | foreach ($separators as $sep) |
| 828 | { |
| 829 | $fieldCounts = []; |
| 830 | |
| 831 | // for each line, count the number of columns extracted by using the current separator |
| 832 | foreach ($lines as $line) |
| 833 | { |
| 834 | // save columns count line per line |
| 835 | $fieldCounts[] = count(str_getcsv($line, $sep)); |
| 836 | } |
| 837 | |
| 838 | // make sure all the lines share the same number of columns and make sure the new count |
| 839 | // is higher than the one registered for the previous separator |
| 840 | if (count(array_unique($fieldCounts)) === 1 && $fieldCounts[0] > $maxFields) |
| 841 | { |
| 842 | // a new optimal separator has been found |
| 843 | $bestSeparator = $sep; |
| 844 | $maxFields = $fieldCounts[0]; |
| 845 | } |
| 846 | } |
| 847 | |
| 848 | // detect the enclosure used in the CSV |
| 849 | $bestEnclosure = '"'; |
| 850 | $maxQuotedTotal = 0; |
| 851 | |
| 852 | // test all the supported enclosures |
| 853 | foreach ($enclosures as $enc) |
| 854 | { |
| 855 | $quotedTotal = 0; |
| 856 | |
| 857 | // count the total number of columns that properly use the current enclosure |
| 858 | foreach ($lines as $line) |
| 859 | { |
| 860 | // extract the columns for the current line |
| 861 | $fields = array_map('trim', explode($bestSeparator, $line)); |
| 862 | |
| 863 | // preserve only the columns that actually start and ends with the current enclosure |
| 864 | $quoted = array_filter($fields, function($f) use ($enc) { |
| 865 | return str_starts_with($f, $enc) && str_ends_with($f, $enc); |
| 866 | }); |
| 867 | |
| 868 | $quotedTotal += count($quoted); |
| 869 | } |
| 870 | |
| 871 | // make sure the total number of occurrences is higher than the one calculated |
| 872 | // for the previously tested enclosure |
| 873 | if ($quotedTotal > $maxQuotedTotal) |
| 874 | { |
| 875 | // a new optimal enclosure has been found |
| 876 | $bestEnclosure = $enc; |
| 877 | $maxQuotedTotal = $quotedTotal; |
| 878 | } |
| 879 | } |
| 880 | |
| 881 | return [$bestSeparator, $bestEnclosure]; |
| 882 | } |
| 883 | } |
| 884 |