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);
|
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()
|
* Wrapper around global function stream_get_line()
|
||||||
* @see 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 */
|
/** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper Helper to work with global functions */
|
||||||
protected $globalFunctionsHelper;
|
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
|
* 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)
|
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)) {
|
if (!$this->isPhpStream($filePath)) {
|
||||||
// we skip the checks if the provided file path points to a PHP stream
|
// we skip the checks if the provided file path points to a PHP stream
|
||||||
if (!$this->globalFunctionsHelper->file_exists($filePath)) {
|
if (!$this->globalFunctionsHelper->file_exists($filePath)) {
|
||||||
@ -72,8 +83,7 @@ abstract class AbstractReader implements ReaderInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Need to use realpath to fix "Can't open file" on some Windows setup
|
$fileRealPath = $this->getFileRealPath($filePath);
|
||||||
$fileRealPath = realpath($filePath);
|
|
||||||
$this->openReader($fileRealPath);
|
$this->openReader($fileRealPath);
|
||||||
$this->isStreamOpened = true;
|
$this->isStreamOpened = true;
|
||||||
} catch (\Exception $exception) {
|
} 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, ...)
|
* 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)
|
protected function isPhpStream($filePath)
|
||||||
{
|
{
|
||||||
return (strpos($filePath, 'php://') === 0);
|
$streamScheme = $this->getStreamWrapperScheme($filePath);
|
||||||
|
return ($streamScheme === 'php');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -84,6 +84,16 @@ class Reader extends AbstractReader
|
|||||||
return $this;
|
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.
|
* Opens the file at the given path to make it ready to be read.
|
||||||
* If setEncoding() was not called, it assumes that the file is encoded in UTF-8.
|
* If setEncoding() was not called, it assumes that the file is encoded in UTF-8.
|
||||||
|
@ -19,6 +19,16 @@ class Reader extends AbstractReader
|
|||||||
/** @var SheetIterator To iterator over the ODS sheets */
|
/** @var SheetIterator To iterator over the ODS sheets */
|
||||||
protected $sheetIterator;
|
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.
|
* 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;
|
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.
|
* 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
|
* 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);
|
$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);
|
$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
|
* @param string $fileName
|
||||||
* @return array All the read rows the given file
|
* @return array All the read rows the given file
|
||||||
|
@ -404,6 +404,30 @@ class ReaderTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertEquals($expectedRows, $allRows);
|
$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
|
* @param string $fileName
|
||||||
* @return array All the read rows the given file
|
* @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/TestUsingResource.php');
|
||||||
require_once(dirname(__DIR__) . '/tests/Spout/ReflectionHelper.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
|
// Make sure a timezone is set to be able to work with dates
|
||||||
date_default_timezone_set('UTC');
|
date_default_timezone_set('UTC');
|
||||||
|
Loading…
x
Reference in New Issue
Block a user