<?php namespace Maatwebsite\Excel\Readers; use Cache; use Config; use Maatwebsite\Excel\Classes\PHPExcel; use PHPExcel_Cell; use PHPExcel_IOFactory; use PHPExcel_Cell_IValueBinder; use PHPExcel_Cell_DefaultValueBinder; use Illuminate\Filesystem\Filesystem; use Maatwebsite\Excel\Parsers\ExcelParser; use Maatwebsite\Excel\Classes\FormatIdentifier; use Maatwebsite\Excel\Exceptions\LaravelExcelException; /** * * LaravelExcel Excel reader * * @category Laravel Excel * @version 1.0.0 * @package maatwebsite/excel * @copyright Copyright (c) 2013 - 2014 Maatwebsite (http://www.maatwebsite.nl) * @author Maatwebsite <info@maatwebsite.nl> * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt LGPL */ class LaravelExcelReader { /** * Excel object * @var \PHPExcel */ public $excel; /** * Spreadsheet writer * @var object */ public $reader; /** * The file to read * @var string */ public $file; /** * Selected columns * @var array */ public $columns = array(); /** * Spreadsheet title * @var string */ public $title; /** * Default extension * @var string */ public $ext = 'xls'; /** * Encoding * @var boolean */ public $encoding = false; /** * Default format * @var stirng */ public $format; /** * The parsed file * @var SheetCollection|RowCollection */ public $parsed; /** * Calculate [true/false] * @var boolean */ public $calculate; /** * Limit data * @var boolean */ protected $limit = false; /** * Amount of rows to skip * @var integer */ protected $skip = 0; /** * Slug separator * @var string */ public $separator = false; /** * Ignore empty cells * @var boolean */ public $ignoreEmpty = false; /** * Format dates * @var boolean */ public $formatDates = true; /** * The date columns * @var array */ public $dateColumns = array(); /** * If the file has a heading or not * @var boolean */ public $noHeading = false; /** * Default date format * @var string */ public $dateFormat; /** * Whether the results are cached or not * @var boolean */ public $remembered = false; /** * Amount of minutes the results will remain cached * @var integer */ public $cacheMinutes = 10; /** * Selected sheets * @var array */ public $selectedSheets = array(); /** * Selected sheet indices * @var array */ public $selectedSheetIndices = array(); /** * Active filter * @var PHPExcel_Reader_IReadFilter */ protected $filter; /** * Filters * @var array */ public $filters = array( 'registered' => array() ); /** * @var LaravelExcelWorksheet */ protected $sheet; /** * @var LaravelExcelWriter */ protected $writer; /** * @var bool|string */ protected $delimiter; /** * @var bool|string */ protected $enclosure; /** * @var \PHPExcel */ protected $original; /** * Construct new reader * @param Filesystem $filesystem * @param FormatIdentifier $identifier */ public function __construct(Filesystem $filesystem, FormatIdentifier $identifier) { $this->filesystem = $filesystem; $this->identifier = $identifier; } /** * Load a file * @param string $file * @param string|boolean $encoding * @param bool $noBasePath * @return LaravelExcelReader */ public function load($file, $encoding = false, $noBasePath = false) { // init the loading $this->_init($file, $encoding, $noBasePath); // Only fetch selected sheets if necessary if ($this->sheetsSelected()) $this->reader->setLoadSheetsOnly($this->selectedSheets); // Load the file $this->excel = $this->reader->load($this->file); // Return itself return $this; } /** * @param integer|callable|string $sheetID * @param null $callback * @return $this * @throws \PHPExcel_Exception */ public function sheet($sheetID, $callback = null) { // Default $isCallable = false; // Init a new PHPExcel instance without any worksheets if(!$this->excel instanceof PHPExcel) { $this->original = $this->excel; $this->initClonedExcelObject($this->excel); // Clone all connected sheets foreach($this->original->getAllSheets() as $sheet) { $this->excel->createSheet()->cloneParent($sheet); } } // Copy the callback when needed if(is_callable($sheetID)) { $callback = $sheetID; $isCallable = true; } elseif(is_callable($callback)) { $isCallable = true; } // Clone the loaded excel instance $this->sheet = $this->getSheetByIdOrName($sheetID); // Do the callback if ($isCallable) call_user_func($callback, $this->sheet); // Return the sheet return $this->sheet; } /** * Set csv delimiter * @param $delimiter * @return $this */ public function setDelimiter($delimiter) { $this->delimiter = $delimiter; return $this; } /** * Set csv enclosure * @param $enclosure * @return $this */ public function setEnclosure($enclosure) { $this->enclosure = $enclosure; return $this; } /** * set selected sheets * @param array $sheets */ public function setSelectedSheets($sheets) { $this->selectedSheets = $sheets; } /** * Check if sheets were selected * @return integer */ public function sheetsSelected() { return count($this->selectedSheets) > 0; } /** * Check if the file was selected by index * @param $index * @return boolean */ public function isSelectedByIndex($index) { $selectedSheets = $this->getSelectedSheetIndices(); if (empty($selectedSheets)) return true; return in_array($index, $selectedSheets) ? true : false; } /** * Set the selected sheet indices * @param $sheets * @return $this */ public function setSelectedSheetIndices($sheets) { $this->selectedSheetIndices = $sheets; return $this; } /** * Return the selected sheets * @return array */ public function getSelectedSheetIndices() { return $this->selectedSheetIndices; } /** * Remember the results for x minutes * @param integer $minutes * @return LaravelExcelReader */ public function remember($minutes) { $this->remembered = true; $this->cacheMinutes = $minutes; return $this; } /** * Read the file through a config file * @param string $config * @param callback|null $callback * @return SheetCollection */ public function byConfig($config, $callback = null) { $config = new ConfigReader($this->excel, $config, $callback); return $config->getSheetCollection(); } /** * Take x rows * @param integer $amount * @return LaravelExcelReader */ public function take($amount) { // Set limit $this->limit = $amount; return $this; } /** * Skip x rows * @param integer $amount * @return LaravelExcelReader */ public function skip($amount) { // Set skip amount $this->skip = $amount; return $this; } /** * Limit the results by x * @param integer $take * @param integer $skip * @return LaravelExcelReader */ public function limit($take, $skip = 0) { // Skip x records $this->skip($skip); // Take x records $this->take($take); return $this; } /** * Select certain columns * @param array $columns * @return LaravelExcelReader */ public function select($columns = array()) { $this->columns = array_merge($this->columns, $columns); return $this; } /** * Return all sheets/rows * @param array $columns * @return LaravelExcelReader */ public function all($columns = array()) { return $this->get($columns); } /** * Get first row/sheet only * @param array $columns * @return SheetCollection|RowCollection */ public function first($columns = array()) { return $this->take(1)->get($columns)->first(); } /** * Get all sheets/rows * @param array $columns * @return SheetCollection|RowCollection */ public function get($columns = array()) { if ($this->remembered) { // Return cached results return Cache::remember(md5($this->file), $this->cacheMinutes, function () use (&$columns) { $this->_parseFile($columns); return $this->parsed; }); } else { // return parsed file $this->_parseFile($columns); return $this->parsed; } } /** * Parse the file in chunks * @param int $size * @param $callback * @throws \Exception * @return void */ public function chunk($size = 10, $callback = null) { // Check if the chunk filter has been enabled if(!in_array('chunk', $this->filters['enabled'])) throw new \Exception("The chunk filter is not enabled, do so with ->filter('chunk')"); // Get total rows $totalRows = $this->getTotalRowsOfFile(); // Only read $this->reader->setReadDataOnly(true); $break = false; // Start the chunking for ($startRow = 0; $startRow < $totalRows; $startRow += $chunkSize) { // Set start index $startIndex = ($startRow == 0) ? $startRow : $startRow - 1; $chunkSize = ($startRow == 0)? $size + 1 : $size; // Set the rows for the chunking $this->filter->setRows($startRow, $chunkSize); // Load file with chunk filter enabled $this->excel = $this->reader->load($this->file); // Slice the results $results = $this->get()->slice($startIndex, $chunkSize); // Do a callback if(is_callable($callback)) { $break = call_user_func($callback, $results); } $this->_reset(); unset($this->excel, $results); if ($break === true) { break; } } } /** * Each * @param callback $callback * @return SheetCollection|RowCollection */ public function each($callback) { return $this->get()->each($callback); } /** * Parse the file to an array. * @param array $columns * @return array */ public function toArray($columns = array()) { return (array) $this->get($columns)->toArray(); } /** * Parse the file to an object. * @param array $columns * @return SheetCollection|RowCollection */ public function toObject($columns = array()) { return $this->get($columns); } /** * Dump the parsed file to a readable array * @param array $columns * @param boolean $die * @return string */ public function dump($columns = array(), $die = false) { echo '<pre class="container" style="background: #f5f5f5; border: 1px solid #e3e3e3; padding:15px;">'; $die ? dd($this->get($columns)) : var_dump($this->get($columns)); echo '</pre>'; } /** * Die and dump * @param array $columns * @return string */ public function dd($columns = array()) { return $this->dump($columns, true); } /** * Init the loading * @param $file * @param bool $encoding * @param bool $noBasePath */ protected function _init($file, $encoding = false, $noBasePath = false) { // Set the extension $this->_setFile($file, $noBasePath) ->setExtension() ->setTitle() ->_setFormat() ->_setReader() ->_enableFilters() ->_setInputEncoding($encoding); } /** * Inject the excel object * @param PHPExcel $excel * @return void */ public function injectExcel($excel) { $this->excel = $excel; $this->_reset(); } /** * Set filters * @param array $filters * */ public function setFilters($filters = array()) { $this->filters = $filters; } /** * Enable filters * @return $this */ protected function _enableFilters() { // Loop through the registered filters foreach($this->filters['registered'] as $key => $class) { // Set the filter inside the reader when enabled and the class exists if(in_array($key, $this->filters['enabled']) && class_exists($class)) { // init new filter (and overrule the current) $this->filter = new $class; // Set default rows if(method_exists($this->filter, 'setRows')) $this->filter->setRows(0, 1); // Set the read filter $this->reader->setReadFilter($this->filter); } } return $this; } /** * Set the file * @param string $file * @param bool $noBasePath * @return $this */ protected function _setFile($file, $noBasePath = false) { // check if we have a correct path if (!$noBasePath && !realpath($file)) $file = base_path($file); $this->file = $file; return $this; } /** * Set the spreadsheet title * @param string|boolean $title * @return LaraveExcelReader */ public function setTitle($title = false) { $this->title = $title ? $title : basename($this->file, '.' . $this->ext); return $this; } /** * Set extension * @param string|boolean $ext * @return LaraveExcelReader */ public function setExtension($ext = false) { $this->ext = $ext ? $ext : $this->filesystem->extension($this->file); return $this; } /** * Set custom value binder * @param string|boolean $ext * @return void */ public function setValueBinder(PHPExcel_Cell_IValueBinder $binder) { PHPExcel_Cell::setValueBinder($binder); return $this; } /** * Reset the value binder back to default * @return void */ public function resetValueBinder() { PHPExcel_Cell::setValueBinder(new PHPExcel_Cell_DefaultValueBinder); return $this; } /** * Set the date format * @param bool|string $format The date format * @return LaraveExcelReader */ public function setDateFormat($format = false) { $this->formatDates = $format ? true : false; $this->dateFormat = $format; return $this; } /** * Enable/disable date formating * @param boolean $boolean True/false * @param boolean $format * @return LaraveExcelReader */ public function formatDates($boolean = true, $format = false) { $this->formatDates = $boolean; $this->setDateFormat($format); return $this; } /** * Set the date columns * @return LaraveExcelReader */ public function setDateColumns() { $this->formatDates = true; $columns = func_get_args(); $this->dateColumns = array_merge($this->dateColumns, array_flatten($columns)); return $this; } /** * If the file has a table heading or not * @param boolean $boolean * @return LaraveExcelReader */ public function noHeading($boolean = true) { $this->noHeading = $boolean; return $this; } /** * Set the cell name word separator * @param string $separator * @return LaraveExcelReader */ public function setSeparator($separator) { $this->separator = $separator; return $this; } /** * Spelling mistake backwards compatibility * @param $separator * @return \Maatwebsite\Excel\Readers\LaraveExcelReader */ public function setSeperator($separator) { return $this->setSeparator($separator); } /** * Set default calculate * @param bool $boolean Calculate yes or no * @return LaraveExcelReader */ public function calculate($boolean = true) { $this->calculate = $boolean; return $this; } /** * Ignore empty cells * @param boolean $boolean * @return LaraveExcelReader */ public function ignoreEmpty($boolean = true) { $this->ignoreEmpty = $boolean; return $this; } /** * Check if the file has een heading * @return boolean */ public function hasHeading() { if (!$this->noHeading) { $config = Config::get('excel.import.heading', true); return $config !== false && $config !== 'numeric'; } return $this->noHeading ? false : true; } /** * Get the separator * @return string */ public function getSeparator() { if ($this->separator) return $this->separator; return Config::get('excel.import.separator', Config::get('excel.import.seperator', '_')); } /** * Get the dateFormat * @return string */ public function getDateFormat() { return $this->dateFormat; } /** * Get the date columns * @return array */ public function getDateColumns() { return $this->dateColumns; } /** * Check if we need to calculate the formula inside the cell * @return boolean */ public function needsCalculation() { return $this->calculate; } /** * Check if we need to ignore the empty cells * @return boolean */ public function needsIgnoreEmpty() { return $this->ignoreEmpty; } /** * Check if we need to format the dates * @return boolean */ public function needsDateFormatting() { return $this->formatDates ? true : false; } /** * Return the amount of rows to skip * @return integer */ public function getSkip() { return $this->skip; } /** * Return the amount of rows to take * @return integer */ public function getLimit() { return $this->limit; } /** * Get total rows of file * @return integer */ public function getTotalRowsOfFile() { // Get worksheet info $spreadsheetInfo = $this->getSheetInfoForActive(); // return total rows return $spreadsheetInfo['totalRows']; } /** * Get sheet info for active sheet * @return mixed */ public function getSheetInfoForActive() { $spreadsheetInfo = $this->reader->listWorksheetInfo($this->file); // Loop through the info foreach($spreadsheetInfo as $key => $value) { // When we hit the right worksheet if($value['worksheetName'] == $this->getActiveSheet()->getTitle()) $index = $key; } // return total rows return $spreadsheetInfo[$index]; } /** * @param $clone */ protected function initClonedExcelObject($clone) { $this->excel = new PHPExcel(); $this->excel->cloneParent(clone $clone); $this->excel->disconnectWorksheets(); } /** * Get the sheet by id or name, else get the active sheet * @param callable|integer|string $sheetID * @return \PHPExcel_Worksheet */ protected function getSheetByIdOrName($sheetID) { // If is a string, return the sheet by name if(is_string($sheetID)) return $this->excel->getSheetByName($sheetID); // Else it should be the sheet index return $this->excel->getSheet($sheetID); } /** * Get the file title * @return string */ public function getTitle() { return $this->excel->getProperties()->getTitle(); } /** * Get the current filename * @return mixed */ public function getFileName() { return pathinfo($this->file, PATHINFO_FILENAME); } /** * Check if the writer has the called method * @param $method * @return bool */ protected function writerHasMethod($method) { $this->initNewWriterWhenNeeded(); return method_exists($this->writer, $method) ? true : false; } /** * Init a new writer instance when it doesn't exist yet */ protected function initNewWriterWhenNeeded() { if(!$this->writer) { $this->writer = app('excel.writer'); $this->writer->injectExcel($this->excel, false); $this->writer->setFileName($this->getFileName()); $this->writer->setTitle($this->getTitle()); } } /** * Set the write format * @return LaraveExcelReader */ protected function _setFormat() { $this->format = $this->identifier->getFormatByFile($this->file); return $this; } /** * Parse the file * @param array $columns * @return void */ protected function _parseFile($columns = array()) { // Merge the selected columns $columns = array_merge($this->columns, $columns); // Parse the file $parser = new ExcelParser($this); $this->parsed = $parser->parseFile($columns); } /** * Set the writer * @return LaraveExcelReader */ protected function _setReader() { // Init the reader $this->reader = PHPExcel_IOFactory::createReader($this->format); $this->_setReaderDefaults(); return $this; } /** * Set the input encoding * @param boolean $encoding * @return LaraveExcelReader */ protected function _setInputEncoding($encoding = false) { if ($this->format == 'CSV') { // If no encoding was given, use the config value $encoding = $encoding ? $encoding : Config::get('excel.import.encoding.input', 'UTF-8'); $this->reader->setInputEncoding($encoding); } return $this; } /** * Set reader defaults * @return void */ protected function _setReaderDefaults() { // Set CSV delimiter if ($this->format == 'CSV') { // If no delimiter was given, take from config if(!$this->delimiter) $this->reader->setDelimiter(Config::get('excel.csv.delimiter', ',')); else $this->reader->setDelimiter($this->delimiter); if(!$this->enclosure) $this->reader->setEnclosure(Config::get('excel.csv.enclosure', '"')); else $this->reader->setEnclosure($this->enclosure); } // Set default calculate $this->calculate = Config::get('excel.import.calculate', true); // Set default for ignoring empty cells $this->ignoreEmpty = Config::get('excel.import.ignoreEmpty', true); // Set default date format $this->dateFormat = Config::get('excel.import.dates.format', 'Y-m-d'); // Date formatting disabled/enabled $this->formatDates = Config::get('excel.import.dates.enabled', true); // Set default date columns $this->dateColumns = Config::get('excel.import.dates.columns', array()); // Set default include charts $this->reader->setIncludeCharts(Config::get('excel.import.includeCharts', false)); } /** * Reset the writer * @return void */ protected function _reset() { $this->excel->disconnectWorksheets(); $this->resetValueBinder(); unset($this->parsed); } /** * Get excel object * @return PHPExcel */ public function getExcel() { return $this->excel; } /** * Dynamically call methods * @param string $method * @param array $params * @throws LaravelExcelException */ public function __call($method, $params) { // Call a php excel method if (method_exists($this->excel, $method)) { // Call the method from the excel object with the given params return call_user_func_array(array($this->excel, $method), $params); } // If it's a reader method elseif (method_exists($this->reader, $method)) { // Call the method from the excel object with the given params return call_user_func_array(array($this->reader, $method), $params); } elseif($this->writerHasMethod($method)) { return call_user_func_array(array($this->writer, $method), $params); } throw new LaravelExcelException('[ERROR] Reader method [' . $method . '] does not exist.'); } }