cleanup context handling for wrappers

This commit is contained in:
Robin Appelman 2019-03-11 16:52:13 +01:00
commit 2c8ae56d02
16 changed files with 182 additions and 258 deletions

View file

@ -53,30 +53,28 @@ class CallbackWrapper extends Wrapper {
* Wraps a stream with the provided callbacks
*
* @param resource $source
* @param callable $read (optional)
* @param callable $write (optional)
* @param callable $close (optional)
* @param callable $readDir (optional)
* @param callable|null $read (optional)
* @param callable|null $write (optional)
* @param callable|null $close (optional)
* @param callable|null $readDir (optional)
* @param callable|null $preClose (optional)
* @return resource
*
* @throws \BadMethodCallException
*/
public static function wrap($source, $read = null, $write = null, $close = null, $readDir = null, $preClose = null) {
$context = stream_context_create(array(
'callback' => array(
$context = [
'source' => $source,
'read' => $read,
'write' => $write,
'close' => $close,
'readDir' => $readDir,
'preClose' => $preClose,
)
));
];
return self::wrapSource($source, $context);
}
protected function open() {
$context = $this->loadContext('callback');
$context = $this->loadContext();
$this->readCallback = $context['read'];
$this->writeCallback = $context['write'];
@ -112,7 +110,7 @@ class CallbackWrapper extends Wrapper {
public function stream_close() {
if (is_callable($this->preCloseCallback)) {
call_user_func($this->preCloseCallback, $this->loadContext('callback')['source']);
call_user_func($this->preCloseCallback, $this->source);
// prevent further calls by potential PHP 7 GC ghosts
$this->preCloseCallback = null;
}

View file

@ -63,17 +63,14 @@ class CountWrapper extends Wrapper {
if (!is_callable($callback)) {
throw new \InvalidArgumentException('Invalid or missing callback');
}
$context = stream_context_create(array(
'count' => array(
return self::wrapSource($source, [
'source' => $source,
'callback' => $callback
)
));
return self::wrapSource($source, $context);
]);
}
protected function open() {
$context = $this->loadContext('count');
$context = $this->loadContext();
$this->callback = $context['callback'];
return true;
}

View file

@ -25,7 +25,7 @@ class DirectoryFilter extends DirectoryWrapper {
* @return bool
*/
public function dir_opendir($path, $options) {
$context = $this->loadContext('filter');
$context = $this->loadContext();
$this->filter = $context['filter'];
return true;
}
@ -36,7 +36,7 @@ class DirectoryFilter extends DirectoryWrapper {
public function dir_readdir() {
$file = readdir($this->source);
$filter = $this->filter;
// keep reading untill we have an accepted entry or we're at the end of the folder
// keep reading until we have an accepted entry or we're at the end of the folder
while ($file !== false && $filter($file) === false) {
$file = readdir($this->source);
}
@ -49,12 +49,9 @@ class DirectoryFilter extends DirectoryWrapper {
* @return resource
*/
public static function wrap($source, callable $filter) {
$options = array(
'filter' => array(
return self::wrapSource($source, [
'source' => $source,
'filter' => $filter
)
);
return self::wrapWithOptions($options);
]);
}
}

View file

@ -7,37 +7,9 @@
namespace Icewind\Streams;
class DirectoryWrapper implements Directory {
/**
* @var resource
*/
public $context;
/**
* @var resource
*/
protected $source;
/**
* Load the source from the stream context and return the context options
*
* @param string $name
* @return array
* @throws \Exception
*/
protected function loadContext($name) {
$context = stream_context_get_options($this->context);
if (isset($context[$name])) {
$context = $context[$name];
} else {
throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set');
}
if (isset($context['source']) and is_resource($context['source'])) {
$this->source = $context['source'];
} else {
throw new \BadMethodCallException('Invalid context, source not set');
}
return $context;
class DirectoryWrapper extends Wrapper implements Directory {
public function stream_open($path, $mode, $options, &$opened_path) {
return false;
}
/**
@ -46,7 +18,7 @@ class DirectoryWrapper implements Directory {
* @return bool
*/
public function dir_opendir($path, $options) {
$this->loadContext('dir');
$this->loadContext();
return true;
}
@ -72,18 +44,4 @@ class DirectoryWrapper implements Directory {
rewinddir($this->source);
return true;
}
/**
* @param array $options the options for the context to wrap the stream with
* @param null $class deprecated, class is now automatically generated
* @return resource
*/
protected static function wrapWithOptions($options, $class = null) {
$class = static::class;
$context = stream_context_create($options);
stream_wrapper_register('dirwrapper', $class);
$wrapped = opendir('dirwrapper://', $context);
stream_wrapper_unregister('dirwrapper');
return $wrapped;
}
}

View file

@ -20,7 +20,7 @@ namespace Icewind\Streams;
*
* Either 'array' or 'iterator' need to be set, if both are set, 'iterator' takes preference
*/
class IteratorDirectory implements Directory {
class IteratorDirectory extends WrapperHandler implements Directory {
/**
* @var resource
*/
@ -36,15 +36,10 @@ class IteratorDirectory implements Directory {
*
* @param string $name
* @return array
* @throws \Exception
* @throws \BadMethodCallException
*/
protected function loadContext($name) {
$context = stream_context_get_options($this->context);
if (isset($context[$name])) {
$context = $context[$name];
} else {
throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set');
}
protected function loadContext($name = null) {
$context = parent::loadContext($name);
if (isset($context['iterator'])) {
$this->iterator = $context['iterator'];
} else if (isset($context['array'])) {
@ -61,7 +56,7 @@ class IteratorDirectory implements Directory {
* @return bool
*/
public function dir_opendir($path, $options) {
$this->loadContext('dir');
$this->loadContext();
return true;
}
@ -103,21 +98,16 @@ class IteratorDirectory implements Directory {
*/
public static function wrap($source) {
if ($source instanceof \Iterator) {
$context = stream_context_create(array(
'dir' => array(
'iterator' => $source)
));
$options = [
'iterator' => $source
];
} else if (is_array($source)) {
$context = stream_context_create(array(
'dir' => array(
'array' => $source)
));
$options = [
'array' => $source
];
} else {
throw new \BadMethodCallException('$source should be an Iterator or array');
}
stream_wrapper_register('iterator', static::class);
$wrapped = opendir('iterator://', $context);
stream_wrapper_unregister('iterator');
return $wrapped;
return self::wrapSource(self::NO_SOURCE_DIR, $options);
}
}

View file

@ -11,29 +11,17 @@ namespace Icewind\Streams;
* Stream wrapper that does nothing, used for tests
*/
class NullWrapper extends Wrapper {
/**
* Wraps a stream with the provided callbacks
*
* @param resource $source
* @return resource
*
* @throws \BadMethodCallException
*/
public static function wrap($source) {
$context = stream_context_create(array(
'null' => array(
'source' => $source)
));
return self::wrapSource($source, $context);
return self::wrapSource($source);
}
public function stream_open($path, $mode, $options, &$opened_path) {
$this->loadContext('null');
$this->loadContext();
return true;
}
public function dir_opendir($path, $options) {
$this->loadContext('null');
$this->loadContext();
return true;
}
}

View file

@ -38,7 +38,7 @@ class Path {
* @param string $class
* @param array $contextOptions
*/
public function __construct($class, $contextOptions = array()) {
public function __construct($class, $contextOptions = []) {
$this->class = $class;
$this->contextOptions = $contextOptions;
}
@ -75,7 +75,7 @@ class Path {
*/
protected function appendDefaultContent($values) {
if (!is_array(current($values))) {
$values = array($this->getProtocol() => $values);
$values = [$this->getProtocol() => $values];
}
$context = stream_context_get_default();
$defaults = stream_context_get_options($context);

View file

@ -16,10 +16,8 @@ class PathWrapper extends NullWrapper {
* @return Path|string
*/
public static function getPath($source) {
return new Path(__CLASS__, [
'null' => [
'source' => $source
]
return new Path(NullWrapper::class, [
NullWrapper::getProtocol() => ['source' => $source]
]);
}
}

View file

@ -11,25 +11,8 @@ namespace Icewind\Streams;
* Wrapper that retries reads/writes to remote streams that dont deliver/recieve all requested data at once
*/
class RetryWrapper extends Wrapper {
/**
* Wraps a stream with the provided callbacks
*
* @param resource $source
* @return resource
*/
public static function wrap($source) {
$context = stream_context_create(array(
'retry' => array(
'source' => $source
)
));
return self::wrapSource($source, $context);
}
protected function open() {
$this->loadContext('retry');
return true;
return self::wrapSource($source);
}
public function dir_opendir($path, $options) {
@ -37,7 +20,8 @@ class RetryWrapper extends Wrapper {
}
public function stream_open($path, $mode, $options, &$opened_path) {
return $this->open();
$this->loadContext();
return true;
}
public function stream_read($count) {

View file

@ -25,21 +25,8 @@ class SeekableWrapper extends Wrapper {
*/
protected $cache;
/**
* Wraps a stream to make it seekable
*
* @param resource $source
* @return resource
*
* @throws \BadMethodCallException
*/
public static function wrap($source) {
$context = stream_context_create(array(
'callback' => array(
'source' => $source
)
));
return self::wrapSource($source, $context);
return self::wrapSource($source);
}
public function dir_opendir($path, $options) {
@ -47,7 +34,7 @@ class SeekableWrapper extends Wrapper {
}
public function stream_open($path, $mode, $options, &$opened_path) {
$this->loadContext('callback');
$this->loadContext();
$this->cache = fopen('php://temp', 'w+');
return true;
}

View file

@ -51,7 +51,7 @@ class UrlCallback extends Wrapper implements Url {
*/
public static function wrap($source, $fopen = null, $opendir = null, $mkdir = null, $rename = null, $rmdir = null,
$unlink = null, $stat = null) {
$options = array(
return new Path(static::class, [
'source' => $source,
'fopen' => $fopen,
'opendir' => $opendir,
@ -60,11 +60,10 @@ class UrlCallback extends Wrapper implements Url {
'rmdir' => $rmdir,
'unlink' => $unlink,
'stat' => $stat
);
return new Path(static::class, $options);
]);
}
protected function loadContext($url) {
protected function loadUrlContext($url) {
list($protocol) = explode('://', $url);
$options = stream_context_get_options($this->context);
return $options[$protocol];
@ -77,40 +76,40 @@ class UrlCallback extends Wrapper implements Url {
}
public function stream_open($path, $mode, $options, &$opened_path) {
$context = $this->loadContext($path);
$context = $this->loadUrlContext($path);
$this->callCallBack($context, 'fopen');
$this->setSourceStream(fopen($context['source'], $mode));
return true;
}
public function dir_opendir($path, $options) {
$context = $this->loadContext($path);
$context = $this->loadUrlContext($path);
$this->callCallBack($context, 'opendir');
$this->setSourceStream(opendir($context['source']));
return true;
}
public function mkdir($path, $mode, $options) {
$context = $this->loadContext($path);
$context = $this->loadUrlContext($path);
$this->callCallBack($context, 'mkdir');
return mkdir($context['source'], $mode, $options & STREAM_MKDIR_RECURSIVE);
}
public function rmdir($path, $options) {
$context = $this->loadContext($path);
$context = $this->loadUrlContext($path);
$this->callCallBack($context, 'rmdir');
return rmdir($context['source']);
}
public function rename($source, $target) {
$context = $this->loadContext($source);
$context = $this->loadUrlContext($source);
$this->callCallBack($context, 'rename');
list(, $target) = explode('://', $target);
return rename($context['source'], $target);
}
public function unlink($path) {
$context = $this->loadContext($path);
$context = $this->loadUrlContext($path);
$this->callCallBack($context, 'unlink');
return unlink($context['source']);
}

View file

@ -12,7 +12,7 @@ namespace Icewind\Streams;
*
* This wrapper itself doesn't implement any functionality but is just a base class for other wrappers to extend
*/
abstract class Wrapper implements File, Directory {
abstract class Wrapper extends WrapperHandler implements File, Directory {
/**
* @var resource
*/
@ -26,55 +26,14 @@ abstract class Wrapper implements File, Directory {
protected $source;
/**
* @param $source
* @param $context
* @param null $protocol deprecated, protocol is now automatically generated
* @param null $class deprecated, class is now automatically generated
* @return bool|resource
* @param resource $source
*/
protected static function wrapSource($source, $context, $protocol = null, $class = null) {
if ($class === null) {
$class = static::class;
}
$parts = explode('\\', $class);
$protocol = strtolower(array_pop($parts));
if (!is_resource($source)) {
throw new \BadMethodCallException();
}
try {
stream_wrapper_register($protocol, $class);
if (self::isDirectoryHandle($source)) {
$wrapped = opendir($protocol . '://', $context);
} else {
$wrapped = fopen($protocol . '://', 'r+', false, $context);
}
} catch (\BadMethodCallException $e) {
stream_wrapper_unregister($protocol);
throw $e;
}
stream_wrapper_unregister($protocol);
return $wrapped;
protected function setSourceStream($source) {
$this->source = $source;
}
protected static function isDirectoryHandle($resource) {
$meta = stream_get_meta_data($resource);
return $meta['stream_type'] == 'dir';
}
/**
* Load the source from the stream context and return the context options
*
* @param string $name
* @return array
* @throws \Exception
*/
protected function loadContext($name) {
$context = stream_context_get_options($this->context);
if (isset($context[$name])) {
$context = $context[$name];
} else {
throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set');
}
protected function loadContext($name = null) {
$context = parent::loadContext($name);
if (isset($context['source']) and is_resource($context['source'])) {
$this->setSourceStream($context['source']);
} else {
@ -83,13 +42,6 @@ abstract class Wrapper implements File, Directory {
return $context;
}
/**
* @param resource $source
*/
protected function setSourceStream($source) {
$this->source = $source;
}
public function stream_seek($offset, $whence = SEEK_SET) {
$result = fseek($this->source, $offset, $whence);
return $result == 0 ? true : false;

108
src/WrapperHandler.php Normal file
View file

@ -0,0 +1,108 @@
<?php
/**
* @copyright Copyright (c) 2019 Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace Icewind\Streams;
class WrapperHandler {
const NO_SOURCE_DIR = 1;
/**
* get the protocol name that is generated for the class
* @param string|null $class
* @return string
*/
public static function getProtocol($class = null) {
if ($class === null) {
$class = static::class;
}
$parts = explode('\\', $class);
return strtolower(array_pop($parts));
}
/**
* @param resource|int $source
* @param resource|array $context
* @param null $protocol deprecated, protocol is now automatically generated
* @param null $class deprecated, class is now automatically generated
* @return bool|resource
*/
protected static function wrapSource($source, $context = [], $protocol = null, $class = null) {
if ($class === null) {
$class = static::class;
}
$protocol = self::getProtocol($class);
if (is_array($context)) {
$context['source'] = $source;
$context = stream_context_create([$protocol => $context]);
}
if ($source !== self::NO_SOURCE_DIR && !is_resource($source)) {
throw new \BadMethodCallException();
}
try {
stream_wrapper_register($protocol, $class);
if (self::isDirectoryHandle($source)) {
$wrapped = opendir($protocol . '://', $context);
} else {
$wrapped = fopen($protocol . '://', 'r+', false, $context);
}
} catch (\BadMethodCallException $e) {
stream_wrapper_unregister($protocol);
throw $e;
}
stream_wrapper_unregister($protocol);
return $wrapped;
}
protected static function isDirectoryHandle($resource) {
if ($resource === self::NO_SOURCE_DIR) {
return true;
}
$meta = stream_get_meta_data($resource);
return $meta['stream_type'] === 'dir' || $meta['stream_type'] === 'user-space-dir';
}
/**
* Load the source from the stream context and return the context options
*
* @param string|null $name if not set, the generated protocol name is used
* @return array
* @throws \BadMethodCallException
*/
protected function loadContext($name = null) {
if ($name === null) {
$parts = explode('\\', static::class);
$name = strtolower(array_pop($parts));
}
$context = stream_context_get_options($this->context);
if (isset($context[$name])) {
$context = $context[$name];
} else {
throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set');
}
return $context;
}
}

View file

@ -9,21 +9,13 @@ namespace Icewind\Streams\Tests;
class DirectoryWrapperNull extends \Icewind\Streams\DirectoryWrapper {
public static function wrap($source) {
$options = array(
'dir' => array(
'source' => $source)
);
return self::wrapWithOptions($options);
return self::wrapSource($source);
}
}
class DirectoryWrapperDummy extends \Icewind\Streams\DirectoryWrapper {
public static function wrap($source) {
$options = array(
'dir' => array(
'source' => $source)
);
return self::wrapWithOptions($options);
return self::wrapSource($source);
}
public function dir_readdir() {

View file

@ -8,20 +8,8 @@
namespace Icewind\Streams\Tests;
class PartialWrapper extends \Icewind\Streams\NullWrapper {
/**
* Wraps a stream with the provided callbacks
*
* @param resource $source
* @return resource
*
* @throws \BadMethodCallException
*/
public static function wrap($source) {
$context = stream_context_create(array(
'null' => array(
'source' => $source)
));
return self::wrapSource($source, $context);
return self::wrapSource($source);
}
public function stream_read($count) {
@ -36,20 +24,8 @@ class PartialWrapper extends \Icewind\Streams\NullWrapper {
}
class FailWrapper extends \Icewind\Streams\NullWrapper {
/**
* Wraps a stream with the provided callbacks
*
* @param resource $source
* @return resource
*
* @throws \BadMethodCallException
*/
public static function wrap($source) {
$context = stream_context_create(array(
'null' => array(
'source' => $source)
));
return self::wrapSource($source, $context);
return self::wrapSource($source);
}
public function stream_read($count) {