Custom stream wrapper support
Added support for custom stream wrappers, such as "fly" or "s3". Support is determined per reader.
This commit is contained in:
parent
0c90d102ef
commit
d2ac54c578
@ -280,6 +280,17 @@ class GlobalFunctionsHelper
|
||||
return mb_convert_encoding($string, $targetEncoding, $sourceEncoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function stream_get_wrappers()
|
||||
* @see stream_get_wrappers()
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function stream_get_wrappers()
|
||||
{
|
||||
return stream_get_wrappers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around global function stream_get_line()
|
||||
* @see stream_get_line()
|
||||
|
@ -19,6 +19,13 @@ abstract class AbstractReader implements ReaderInterface
|
||||
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
|
||||
protected $globalFunctionsHelper;
|
||||
|
||||
/**
|
||||
* Returns whether stream wrappers are supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
abstract protected function doesSupportStreamWrapper();
|
||||
|
||||
/**
|
||||
* Opens the file at the given file path to make it ready to be read
|
||||
*
|
||||
@ -62,6 +69,10 @@ abstract class AbstractReader implements ReaderInterface
|
||||
*/
|
||||
public function open($filePath)
|
||||
{
|
||||
if ($this->isStreamWrapper($filePath) && (!$this->doesSupportStreamWrapper() || !$this->isSupportedStreamWrapper($filePath))) {
|
||||
throw new IOException("Could not open $filePath for reading! Stream wrapper used is not supported for this type of file.");
|
||||
}
|
||||
|
||||
if (!$this->isPhpStream($filePath)) {
|
||||
// we skip the checks if the provided file path points to a PHP stream
|
||||
if (!$this->globalFunctionsHelper->file_exists($filePath)) {
|
||||
@ -72,8 +83,7 @@ abstract class AbstractReader implements ReaderInterface
|
||||
}
|
||||
|
||||
try {
|
||||
// Need to use realpath to fix "Can't open file" on some Windows setup
|
||||
$fileRealPath = realpath($filePath);
|
||||
$fileRealPath = $this->getFileRealPath($filePath);
|
||||
$this->openReader($fileRealPath);
|
||||
$this->isStreamOpened = true;
|
||||
} catch (\Exception $exception) {
|
||||
@ -81,6 +91,67 @@ abstract class AbstractReader implements ReaderInterface
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the real path of the given path.
|
||||
* If the given path is a valid stream wrapper, returns the path unchanged.
|
||||
*
|
||||
* @param string $filePath
|
||||
* @return string
|
||||
*/
|
||||
protected function getFileRealPath($filePath)
|
||||
{
|
||||
if ($this->isSupportedStreamWrapper($filePath)) {
|
||||
return $filePath;
|
||||
}
|
||||
|
||||
// Need to use realpath to fix "Can't open file" on some Windows setup
|
||||
return realpath($filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the scheme of the custom stream wrapper, if the path indicates a stream wrapper is used.
|
||||
* For example, php://temp => php, s3://path/to/file => s3...
|
||||
*
|
||||
* @param string $filePath Path of the file to be read
|
||||
* @return string|null The stream wrapper scheme or NULL if not a stream wrapper
|
||||
*/
|
||||
protected function getStreamWrapperScheme($filePath)
|
||||
{
|
||||
$streamScheme = null;
|
||||
if (preg_match('/^(\w+):\/\//', $filePath, $matches)) {
|
||||
$streamScheme = $matches[1];
|
||||
}
|
||||
return $streamScheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given path is an unsupported stream wrapper
|
||||
* (like local path, php://temp, mystream://foo/bar...).
|
||||
*
|
||||
* @param string $filePath Path of the file to be read
|
||||
* @return bool Whether the given path is an unsupported stream wrapper
|
||||
*/
|
||||
protected function isStreamWrapper($filePath)
|
||||
{
|
||||
return ($this->getStreamWrapperScheme($filePath) !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given path is an supported stream wrapper
|
||||
* (like php://temp, mystream://foo/bar...).
|
||||
* If the given path is a local path, returns true.
|
||||
*
|
||||
* @param string $filePath Path of the file to be read
|
||||
* @return bool Whether the given path is an supported stream wrapper
|
||||
*/
|
||||
protected function isSupportedStreamWrapper($filePath)
|
||||
{
|
||||
$streamScheme = $this->getStreamWrapperScheme($filePath);
|
||||
return ($streamScheme !== null) ?
|
||||
in_array($streamScheme, $this->globalFunctionsHelper->stream_get_wrappers()) :
|
||||
true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a path is a PHP stream (like php://output, php://memory, ...)
|
||||
*
|
||||
@ -89,7 +160,8 @@ abstract class AbstractReader implements ReaderInterface
|
||||
*/
|
||||
protected function isPhpStream($filePath)
|
||||
{
|
||||
return (strpos($filePath, 'php://') === 0);
|
||||
$streamScheme = $this->getStreamWrapperScheme($filePath);
|
||||
return ($streamScheme === 'php');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -30,7 +30,7 @@ class Reader extends AbstractReader
|
||||
protected $encoding = EncodingHelper::ENCODING_UTF8;
|
||||
|
||||
/** @var string Defines the End of line */
|
||||
protected $endOfLineCharacter = "\n";
|
||||
protected $endOfLineCharacter = "\n";
|
||||
|
||||
/**
|
||||
* Sets the field delimiter for the CSV.
|
||||
@ -82,7 +82,17 @@ class Reader extends AbstractReader
|
||||
{
|
||||
$this->endOfLineCharacter = $endOfLineCharacter;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether stream wrappers are supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function doesSupportStreamWrapper()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the file at the given path to make it ready to be read.
|
||||
|
@ -19,6 +19,16 @@ class Reader extends AbstractReader
|
||||
/** @var SheetIterator To iterator over the ODS sheets */
|
||||
protected $sheetIterator;
|
||||
|
||||
/**
|
||||
* Returns whether stream wrappers are supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function doesSupportStreamWrapper()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the file at the given file path to make it ready to be read.
|
||||
*
|
||||
|
@ -37,6 +37,16 @@ class Reader extends AbstractReader
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether stream wrappers are supported
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function doesSupportStreamWrapper()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the file at the given file path to make it ready to be read.
|
||||
* It also parses the sharedStrings.xml file to get all the shared strings available in memory
|
||||
|
@ -423,4 +423,50 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testReadCustomStreamWrapper()
|
||||
{
|
||||
$allRows = [];
|
||||
$resourcePath = 'spout://csv_standard';
|
||||
|
||||
// register stream wrapper
|
||||
stream_wrapper_register('spout', SpoutTestStream::CLASS_NAME);
|
||||
|
||||
/** @var \Box\Spout\Reader\CSV\Reader $reader */
|
||||
$reader = ReaderFactory::create(Type::CSV);
|
||||
$reader->open($resourcePath);
|
||||
|
||||
foreach ($reader->getSheetIterator() as $sheet) {
|
||||
foreach ($sheet->getRowIterator() as $row) {
|
||||
$allRows[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
$reader->close();
|
||||
|
||||
$expectedRows = [
|
||||
['csv--11', 'csv--12', 'csv--13'],
|
||||
['csv--21', 'csv--22', 'csv--23'],
|
||||
['csv--31', 'csv--32', 'csv--33'],
|
||||
];
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
|
||||
// cleanup
|
||||
stream_wrapper_unregister('spout');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Box\Spout\Common\Exception\IOException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testReadWithUnsupportedCustomStreamWrapper()
|
||||
{
|
||||
/** @var \Box\Spout\Reader\CSV\Reader $reader */
|
||||
$reader = ReaderFactory::create(Type::CSV);
|
||||
$reader->open('unsupported://foobar');
|
||||
}
|
||||
|
||||
}
|
||||
|
120
tests/Spout/Reader/CSV/SpoutTestStream.php
Normal file
120
tests/Spout/Reader/CSV/SpoutTestStream.php
Normal file
@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace Box\Spout\Reader\CSV;
|
||||
|
||||
/**
|
||||
* Class SpoutTestStream
|
||||
* Custom stream that reads CSV files located in the tests/resources/csv folder.
|
||||
* For example: spout://foobar will point to tests/resources/csv/foobar.csv
|
||||
*
|
||||
* @package Box\Spout\Reader\CSV
|
||||
*/
|
||||
class SpoutTestStream
|
||||
{
|
||||
const CLASS_NAME = __CLASS__;
|
||||
|
||||
const PATH_TO_CSV_RESOURCES = 'tests/resources/csv/';
|
||||
const CSV_EXTENSION = '.csv';
|
||||
|
||||
/** @var int $position */
|
||||
private $position;
|
||||
|
||||
/** @var resource $fileHandle */
|
||||
private $fileHandle;
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @param int $flag
|
||||
* @return array
|
||||
*/
|
||||
public function url_stat($path, $flag)
|
||||
{
|
||||
$filePath = $this->getFilePathFromStreamPath($path);
|
||||
return stat($filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $streamPath
|
||||
* @return string
|
||||
*/
|
||||
private function getFilePathFromStreamPath($streamPath)
|
||||
{
|
||||
$fileName = parse_url($streamPath, PHP_URL_HOST);
|
||||
return self::PATH_TO_CSV_RESOURCES . $fileName . self::CSV_EXTENSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @param string $mode
|
||||
* @param int $options
|
||||
* @param string $opened_path
|
||||
* @return bool
|
||||
*/
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
$this->position = 0;
|
||||
|
||||
// the path is like "spout://csv_name" so the actual file name correspond the name of the host.
|
||||
$filePath = $this->getFilePathFromStreamPath($path);
|
||||
$this->fileHandle = fopen($filePath, $mode);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $numBytes
|
||||
* @return string
|
||||
*/
|
||||
public function stream_read($numBytes)
|
||||
{
|
||||
$this->position += $numBytes;
|
||||
return fread($this->fileHandle, $numBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $offset
|
||||
* @param int|void $whence
|
||||
* @return bool
|
||||
*/
|
||||
public function stream_seek($offset, $whence = SEEK_SET)
|
||||
{
|
||||
$result = fseek($this->fileHandle, $offset, $whence);
|
||||
if ($result === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($whence === SEEK_SET) {
|
||||
$this->position = $offset;
|
||||
} else if ($whence === SEEK_CUR) {
|
||||
$this->position += $offset;
|
||||
} else {
|
||||
// not implemented
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function stream_close()
|
||||
{
|
||||
return fclose($this->fileHandle);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->fileHandle);
|
||||
}
|
||||
}
|
@ -363,6 +363,30 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Box\Spout\Common\Exception\IOException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testReadWithUnsupportedCustomStreamWrapper()
|
||||
{
|
||||
/** @var \Box\Spout\Reader\ODS\Reader $reader */
|
||||
$reader = ReaderFactory::create(Type::ODS);
|
||||
$reader->open('unsupported://foobar');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Box\Spout\Common\Exception\IOException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testReadWithSupportedCustomStreamWrapper()
|
||||
{
|
||||
/** @var \Box\Spout\Reader\ODS\Reader $reader */
|
||||
$reader = ReaderFactory::create(Type::ODS);
|
||||
$reader->open('php://memory');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fileName
|
||||
* @return array All the read rows the given file
|
||||
|
@ -404,6 +404,30 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expectedRows, $allRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Box\Spout\Common\Exception\IOException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testReadWithUnsupportedCustomStreamWrapper()
|
||||
{
|
||||
/** @var \Box\Spout\Reader\XLSX\Reader $reader */
|
||||
$reader = ReaderFactory::create(Type::XLSX);
|
||||
$reader->open('unsupported://foobar');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Box\Spout\Common\Exception\IOException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function testReadWithSupportedCustomStreamWrapper()
|
||||
{
|
||||
/** @var \Box\Spout\Reader\XLSX\Reader $reader */
|
||||
$reader = ReaderFactory::create(Type::XLSX);
|
||||
$reader->open('php://memory');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fileName
|
||||
* @return array All the read rows the given file
|
||||
|
@ -3,6 +3,7 @@ require_once(dirname(__DIR__) . '/vendor/autoload.php');
|
||||
|
||||
require_once(dirname(__DIR__) . '/tests/Spout/TestUsingResource.php');
|
||||
require_once(dirname(__DIR__) . '/tests/Spout/ReflectionHelper.php');
|
||||
require_once(dirname(__DIR__) . '/tests/Spout/Reader/CSV/SpoutTestStream.php');
|
||||
|
||||
// Make sure a timezone is set to be able to work with dates
|
||||
date_default_timezone_set('UTC');
|
||||
|
Loading…
x
Reference in New Issue
Block a user