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

@ -14,7 +14,7 @@ it wraps an existing stream and can thus be used for any stream in php
The callbacks are passed in the stream context along with the source stream
and can be any valid [php callable](http://php.net/manual/en/language.types.callable.php)
###Example###
### Example ###
```php
<?php

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) {