This commit is contained in:
Enoch 2024-11-10 21:26:45 +08:00
parent a9c4408722
commit 6d56f09106
155 changed files with 1 additions and 15142 deletions

View File

@ -3,7 +3,7 @@
"description": "A package to use Websocket API of ping0.cc to monitor the ping latency of target host [TEST ONLY]", "description": "A package to use Websocket API of ping0.cc to monitor the ping latency of target host [TEST ONLY]",
"type": "library", "type": "library",
"require": { "require": {
"textalk/websocket": "dev-master" "textalk/websocket": ">=1.5"
}, },
"license": "AGPL", "license": "AGPL",
"autoload": { "autoload": {

25
vendor/autoload.php vendored
View File

@ -1,25 +0,0 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit7f9863daaab5193cdcf774ddaa946c8d::getLoader();

View File

@ -1,579 +0,0 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}

View File

@ -1,359 +0,0 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array()) {
$installed[] = self::$installed;
}
return $installed;
}
}

View File

@ -1,21 +0,0 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,10 +0,0 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
);

View File

@ -1,9 +0,0 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);

View File

@ -1,15 +0,0 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'WebSocket\\' => array($vendorDir . '/textalk/websocket/lib'),
'Psr\\Log\\' => array($vendorDir . '/psr/log/src'),
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
'Phrity\\Util\\' => array($vendorDir . '/phrity/util-errorhandler/src'),
'Phrity\\Net\\' => array($vendorDir . '/phrity/net-uri/src'),
'Laysense\\Monitor\\' => array($baseDir . '/src'),
);

View File

@ -1,38 +0,0 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit7f9863daaab5193cdcf774ddaa946c8d
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit7f9863daaab5193cdcf774ddaa946c8d', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit7f9863daaab5193cdcf774ddaa946c8d', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit7f9863daaab5193cdcf774ddaa946c8d::getInitializer($loader));
$loader->register(true);
return $loader;
}
}

View File

@ -1,68 +0,0 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit7f9863daaab5193cdcf774ddaa946c8d
{
public static $prefixLengthsPsr4 = array (
'W' =>
array (
'WebSocket\\' => 10,
),
'P' =>
array (
'Psr\\Log\\' => 8,
'Psr\\Http\\Message\\' => 17,
'Phrity\\Util\\' => 12,
'Phrity\\Net\\' => 11,
),
'L' =>
array (
'Laysense\\Monitor\\' => 17,
),
);
public static $prefixDirsPsr4 = array (
'WebSocket\\' =>
array (
0 => __DIR__ . '/..' . '/textalk/websocket/lib',
),
'Psr\\Log\\' =>
array (
0 => __DIR__ . '/..' . '/psr/log/src',
),
'Psr\\Http\\Message\\' =>
array (
0 => __DIR__ . '/..' . '/psr/http-factory/src',
1 => __DIR__ . '/..' . '/psr/http-message/src',
),
'Phrity\\Util\\' =>
array (
0 => __DIR__ . '/..' . '/phrity/util-errorhandler/src',
),
'Phrity\\Net\\' =>
array (
0 => __DIR__ . '/..' . '/phrity/net-uri/src',
),
'Laysense\\Monitor\\' =>
array (
0 => __DIR__ . '/../..' . '/src',
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit7f9863daaab5193cdcf774ddaa946c8d::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit7f9863daaab5193cdcf774ddaa946c8d::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInit7f9863daaab5193cdcf774ddaa946c8d::$classMap;
}, null, ClassLoader::class);
}
}

View File

@ -1,378 +0,0 @@
{
"packages": [
{
"name": "phrity/net-uri",
"version": "1.3.0",
"version_normalized": "1.3.0.0",
"source": {
"type": "git",
"url": "https://github.com/sirn-se/phrity-net-uri.git",
"reference": "3f458e0c4d1ddc0e218d7a5b9420127c63925f43"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sirn-se/phrity-net-uri/zipball/3f458e0c4d1ddc0e218d7a5b9420127c63925f43",
"reference": "3f458e0c4d1ddc0e218d7a5b9420127c63925f43",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": "^7.4 | ^8.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.0 | ^2.0"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.0",
"phpunit/phpunit": "^9.0 | ^10.0",
"squizlabs/php_codesniffer": "^3.0"
},
"time": "2023-08-21T10:33:06+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Phrity\\Net\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"description": "PSR-7 Uri and PSR-17 UriFactory implementation",
"homepage": "https://phrity.sirn.se/net-uri",
"keywords": [
"psr-17",
"psr-7",
"uri",
"uri factory"
],
"support": {
"issues": "https://github.com/sirn-se/phrity-net-uri/issues",
"source": "https://github.com/sirn-se/phrity-net-uri/tree/1.3.0"
},
"install-path": "../phrity/net-uri"
},
{
"name": "phrity/util-errorhandler",
"version": "1.1.1",
"version_normalized": "1.1.1.0",
"source": {
"type": "git",
"url": "https://github.com/sirn-se/phrity-util-errorhandler.git",
"reference": "483228156e06673963902b1cc1e6bd9541ab4d5e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sirn-se/phrity-util-errorhandler/zipball/483228156e06673963902b1cc1e6bd9541ab4d5e",
"reference": "483228156e06673963902b1cc1e6bd9541ab4d5e",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": "^7.4 | ^8.0"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.0",
"phpunit/phpunit": "^9.0 | ^10.0 | ^11.0",
"squizlabs/php_codesniffer": "^3.5"
},
"time": "2024-09-12T06:49:16+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Phrity\\Util\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"description": "Inline error handler; catch and resolve errors for code block.",
"homepage": "https://phrity.sirn.se/util-errorhandler",
"keywords": [
"error",
"warning"
],
"support": {
"issues": "https://github.com/sirn-se/phrity-util-errorhandler/issues",
"source": "https://github.com/sirn-se/phrity-util-errorhandler/tree/1.1.1"
},
"install-path": "../phrity/util-errorhandler"
},
{
"name": "psr/http-factory",
"version": "dev-master",
"version_normalized": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-factory.git",
"reference": "7037f4b0950474e9d1350e8df89b15f1842085f6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-factory/zipball/7037f4b0950474e9d1350e8df89b15f1842085f6",
"reference": "7037f4b0950474e9d1350e8df89b15f1842085f6",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": ">=7.0.0",
"psr/http-message": "^1.0 || ^2.0"
},
"time": "2023-09-22T11:16:44+00:00",
"default-branch": true,
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
"keywords": [
"factory",
"http",
"message",
"psr",
"psr-17",
"psr-7",
"request",
"response"
],
"support": {
"source": "https://github.com/php-fig/http-factory"
},
"install-path": "../psr/http-factory"
},
{
"name": "psr/http-message",
"version": "1.1",
"version_normalized": "1.1.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
"reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": "^7.2 || ^8.0"
},
"time": "2023-04-04T09:50:52+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
"homepage": "https://github.com/php-fig/http-message",
"keywords": [
"http",
"http-message",
"psr",
"psr-7",
"request",
"response"
],
"support": {
"source": "https://github.com/php-fig/http-message/tree/1.1"
},
"install-path": "../psr/http-message"
},
{
"name": "psr/log",
"version": "dev-master",
"version_normalized": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": ">=8.0.0"
},
"time": "2024-09-11T13:17:53+00:00",
"default-branch": true,
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Psr\\Log\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"homepage": "https://github.com/php-fig/log",
"keywords": [
"log",
"psr",
"psr-3"
],
"support": {
"source": "https://github.com/php-fig/log/tree/3.0.2"
},
"install-path": "../psr/log"
},
{
"name": "textalk/websocket",
"version": "dev-master",
"version_normalized": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/Textalk/websocket-php.git",
"reference": "34b2f0efa2e1c071b046e2b98848178fddf21552"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Textalk/websocket-php/zipball/34b2f0efa2e1c071b046e2b98848178fddf21552",
"reference": "34b2f0efa2e1c071b046e2b98848178fddf21552",
"shasum": "",
"mirrors": [
{
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
"preferred": true
}
]
},
"require": {
"php": "^7.4 | ^8.0",
"phrity/net-uri": "^1.0",
"phrity/util-errorhandler": "^1.0",
"psr/http-message": "^1.0",
"psr/log": "^1.0 | ^2.0 | ^3.0"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.0",
"phpunit/phpunit": "^9.0",
"squizlabs/php_codesniffer": "^3.5"
},
"time": "2023-12-16T14:43:30+00:00",
"default-branch": true,
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"WebSocket\\": "lib"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"ISC"
],
"authors": [
{
"name": "Fredrik Liljegren"
},
{
"name": "Sören Jensen"
}
],
"description": "WebSocket client and server",
"support": {
"issues": "https://github.com/Textalk/websocket-php/issues",
"source": "https://github.com/Textalk/websocket-php/tree/master"
},
"install-path": "../textalk/websocket"
}
],
"dev": true,
"dev-package-names": []
}

View File

@ -1,83 +0,0 @@
<?php return array(
'root' => array(
'name' => 'laysense/monitor',
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => null,
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'laysense/monitor' => array(
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => null,
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'phrity/net-uri' => array(
'pretty_version' => '1.3.0',
'version' => '1.3.0.0',
'reference' => '3f458e0c4d1ddc0e218d7a5b9420127c63925f43',
'type' => 'library',
'install_path' => __DIR__ . '/../phrity/net-uri',
'aliases' => array(),
'dev_requirement' => false,
),
'phrity/util-errorhandler' => array(
'pretty_version' => '1.1.1',
'version' => '1.1.1.0',
'reference' => '483228156e06673963902b1cc1e6bd9541ab4d5e',
'type' => 'library',
'install_path' => __DIR__ . '/../phrity/util-errorhandler',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/http-factory' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '7037f4b0950474e9d1350e8df89b15f1842085f6',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/http-factory',
'aliases' => array(
0 => '1.0.x-dev',
),
'dev_requirement' => false,
),
'psr/http-message' => array(
'pretty_version' => '1.1',
'version' => '1.1.0.0',
'reference' => 'cb6ce4845ce34a8ad9e68117c10ee90a29919eba',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/http-message',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/log' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => 'f16e1d5863e37f8d8c2a01719f5b34baa2b714d3',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/log',
'aliases' => array(
0 => '3.x-dev',
),
'dev_requirement' => false,
),
'textalk/websocket' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => '34b2f0efa2e1c071b046e2b98848178fddf21552',
'type' => 'library',
'install_path' => __DIR__ . '/../textalk/websocket',
'aliases' => array(
0 => '9999999-dev',
),
'dev_requirement' => false,
),
),
);

View File

@ -1,26 +0,0 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 80000)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.0.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}

View File

@ -1,30 +0,0 @@
{
"name": "phrity/net-uri",
"type": "library",
"description": "PSR-7 Uri and PSR-17 UriFactory implementation",
"homepage": "https://phrity.sirn.se/net-uri",
"keywords": ["uri", "uri factory", "PSR-7", "PSR-17"],
"license": "MIT",
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"autoload": {
"psr-4": {
"Phrity\\Net\\": "src/"
}
},
"require": {
"php": "^7.4 | ^8.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.0 | ^2.0"
},
"require-dev": {
"phpunit/phpunit": "^9.0 | ^10.0",
"php-coveralls/php-coveralls": "^2.0",
"squizlabs/php_codesniffer": "^3.0"
}
}

View File

@ -1,486 +0,0 @@
<?php
/**
* File for Net\Uri class.
* @package Phrity > Net > Uri
* @see https://www.rfc-editor.org/rfc/rfc3986
* @see https://www.php-fig.org/psr/psr-7/#35-psrhttpmessageuriinterface
*/
namespace Phrity\Net;
use InvalidArgumentException;
use Psr\Http\Message\UriInterface;
/**
* Net\Uri class.
*/
class Uri implements UriInterface
{
public const REQUIRE_PORT = 1; // Always include port, explicit or default
public const ABSOLUTE_PATH = 2; // Enforce absolute path
public const NORMALIZE_PATH = 4; // Normalize path
public const IDNA = 8; // IDNA-convert host
private const RE_MAIN = '!^(?P<schemec>(?P<scheme>[^:/?#]+):)?(?P<authorityc>//(?P<authority>[^/?#]*))?'
. '(?P<path>[^?#]*)(?P<queryc>\?(?P<query>[^#]*))?(?P<fragmentc>#(?P<fragment>.*))?$!';
private const RE_AUTH = '!^(?P<userinfoc>(?P<user>[^:/?#]+)(?P<passc>:(?P<pass>[^:/?#]+))?@)?'
. '(?P<host>[^:/?#]*|\[[^/?#]*\])(?P<portc>:(?P<port>[0-9]*))?$!';
private static $port_defaults = [
'acap' => 674,
'afp' => 548,
'dict' => 2628,
'dns' => 53,
'ftp' => 21,
'git' => 9418,
'gopher' => 70,
'http' => 80,
'https' => 443,
'imap' => 143,
'ipp' => 631,
'ipps' => 631,
'irc' => 194,
'ircs' => 6697,
'ldap' => 389,
'ldaps' => 636,
'mms' => 1755,
'msrp' => 2855,
'mtqp' => 1038,
'nfs' => 111,
'nntp' => 119,
'nntps' => 563,
'pop' => 110,
'prospero' => 1525,
'redis' => 6379,
'rsync' => 873,
'rtsp' => 554,
'rtsps' => 322,
'rtspu' => 5005,
'sftp' => 22,
'smb' => 445,
'snmp' => 161,
'ssh' => 22,
'svn' => 3690,
'telnet' => 23,
'ventrilo' => 3784,
'vnc' => 5900,
'wais' => 210,
'ws' => 80,
'wss' => 443,
];
private $scheme;
private $authority;
private $host;
private $port;
private $user;
private $pass;
private $path;
private $query;
private $fragment;
/**
* Create new URI instance using a string
* @param string $uri_string URI as string
* @throws \InvalidArgumentException If the given URI cannot be parsed
*/
public function __construct(string $uri_string = '', int $flags = 0)
{
$this->parse($uri_string);
}
// ---------- PSR-7 getters ---------------------------------------------------------------------------------------
/**
* Retrieve the scheme component of the URI.
* @return string The URI scheme
*/
public function getScheme(int $flags = 0): string
{
return $this->getComponent('scheme') ?? '';
}
/**
* Retrieve the authority component of the URI.
* @return string The URI authority, in "[user-info@]host[:port]" format
*/
public function getAuthority(int $flags = 0): string
{
$host = $this->formatComponent($this->getHost($flags));
if ($this->isEmpty($host)) {
return '';
}
$userinfo = $this->formatComponent($this->getUserInfo(), '', '@');
$port = $this->formatComponent($this->getPort($flags), ':');
return "{$userinfo}{$host}{$port}";
}
/**
* Retrieve the user information component of the URI.
* @return string The URI user information, in "username[:password]" format
*/
public function getUserInfo(int $flags = 0): string
{
$user = $this->formatComponent($this->getComponent('user'));
$pass = $this->formatComponent($this->getComponent('pass'), ':');
return $this->isEmpty($user) ? '' : "{$user}{$pass}";
}
/**
* Retrieve the host component of the URI.
* @return string The URI host
*/
public function getHost(int $flags = 0): string
{
$host = $this->getComponent('host') ?? '';
if ($flags & self::IDNA) {
$host = $this->idna($host);
}
return $host;
}
/**
* Retrieve the port component of the URI.
* @return null|int The URI port
*/
public function getPort(int $flags = 0): ?int
{
$port = $this->getComponent('port');
$scheme = $this->getComponent('scheme');
$default = isset(self::$port_defaults[$scheme]) ? self::$port_defaults[$scheme] : null;
if ($flags & self::REQUIRE_PORT) {
return !$this->isEmpty($port) ? $port : $default;
}
return $this->isEmpty($port) || $port === $default ? null : $port;
}
/**
* Retrieve the path component of the URI.
* @return string The URI path
*/
public function getPath(int $flags = 0): string
{
$path = $this->getComponent('path') ?? '';
if ($flags & self::NORMALIZE_PATH) {
$path = $this->normalizePath($path);
}
if ($flags & self::ABSOLUTE_PATH && substr($path, 0, 1) !== '/') {
$path = "/{$path}";
}
return $path;
}
/**
* Retrieve the query string of the URI.
* @return string The URI query string
*/
public function getQuery(int $flags = 0): string
{
return $this->getComponent('query') ?? '';
}
/**
* Retrieve the fragment component of the URI.
* @return string The URI fragment
*/
public function getFragment(int $flags = 0): string
{
return $this->getComponent('fragment') ?? '';
}
// ---------- PSR-7 setters ---------------------------------------------------------------------------------------
/**
* Return an instance with the specified scheme.
* @param string $scheme The scheme to use with the new instance
* @return static A new instance with the specified scheme
* @throws \InvalidArgumentException for invalid schemes
* @throws \InvalidArgumentException for unsupported schemes
*/
public function withScheme($scheme, int $flags = 0): UriInterface
{
$clone = clone $this;
if ($flags & self::REQUIRE_PORT) {
$clone->setComponent('port', $this->getPort(self::REQUIRE_PORT));
$default = isset(self::$port_defaults[$scheme]) ? self::$port_defaults[$scheme] : null;
}
$clone->setComponent('scheme', $scheme);
return $clone;
}
/**
* Return an instance with the specified user information.
* @param string $user The user name to use for authority
* @param null|string $password The password associated with $user
* @return static A new instance with the specified user information
*/
public function withUserInfo($user, $password = null, int $flags = 0): UriInterface
{
$clone = clone $this;
$clone->setComponent('user', $user);
$clone->setComponent('pass', $password);
return $clone;
}
/**
* Return an instance with the specified host.
* @param string $host The hostname to use with the new instance
* @return static A new instance with the specified host
* @throws \InvalidArgumentException for invalid hostnames
*/
public function withHost($host, int $flags = 0): UriInterface
{
$clone = clone $this;
if ($flags & self::IDNA) {
$host = $this->idna($host);
}
$clone->setComponent('host', $host);
return $clone;
}
/**
* Return an instance with the specified port.
* @param null|int $port The port to use with the new instance
* @return static A new instance with the specified port
* @throws \InvalidArgumentException for invalid ports
*/
public function withPort($port, int $flags = 0): UriInterface
{
$clone = clone $this;
$clone->setComponent('port', $port);
return $clone;
}
/**
* Return an instance with the specified path.
* @param string $path The path to use with the new instance
* @return static A new instance with the specified path
* @throws \InvalidArgumentException for invalid paths
*/
public function withPath($path, int $flags = 0): UriInterface
{
$clone = clone $this;
if ($flags & self::NORMALIZE_PATH) {
$path = $this->normalizePath($path);
}
if ($flags & self::ABSOLUTE_PATH && substr($path, 0, 1) !== '/') {
$path = "/{$path}";
}
$clone->setComponent('path', $path);
return $clone;
}
/**
* Return an instance with the specified query string.
* @param string $query The query string to use with the new instance
* @return static A new instance with the specified query string
* @throws \InvalidArgumentException for invalid query strings
*/
public function withQuery($query, int $flags = 0): UriInterface
{
$clone = clone $this;
$clone->setComponent('query', $query);
return $clone;
}
/**
* Return an instance with the specified URI fragment.
* @param string $fragment The fragment to use with the new instance
* @return static A new instance with the specified fragment
*/
public function withFragment($fragment, int $flags = 0): UriInterface
{
$clone = clone $this;
$clone->setComponent('fragment', $fragment);
return $clone;
}
// ---------- PSR-7 string ----------------------------------------------------------------------------------------
/**
* Return the string representation as a URI reference.
* @return string
*/
public function __toString(): string
{
return $this->toString();
}
// ---------- Extensions ------------------------------------------------------------------------------------------
/**
* Return the string representation as a URI reference.
* @return string
*/
public function toString(int $flags = 0): string
{
$scheme = $this->formatComponent($this->getComponent('scheme'), '', ':');
$authority = $this->authority ? "//{$this->formatComponent($this->getAuthority($flags))}" : '';
$path_flags = ($this->authority && $this->path ? self::ABSOLUTE_PATH : 0) | $flags;
$path = $this->formatComponent($this->getPath($path_flags));
$query = $this->formatComponent($this->getComponent('query'), '?');
$fragment = $this->formatComponent($this->getComponent('fragment'), '#');
return "{$scheme}{$authority}{$path}{$query}{$fragment}";
}
// ---------- Private helper methods ------------------------------------------------------------------------------
private function parse(string $uri_string = ''): void
{
if ($uri_string === '') {
return;
}
preg_match(self::RE_MAIN, $uri_string, $main);
$this->authority = !empty($main['authorityc']);
$this->setComponent('scheme', isset($main['schemec']) ? $main['scheme'] : '');
$this->setComponent('path', isset($main['path']) ? $main['path'] : '');
$this->setComponent('query', isset($main['queryc']) ? $main['query'] : '');
$this->setComponent('fragment', isset($main['fragmentc']) ? $main['fragment'] : '');
if ($this->authority) {
preg_match(self::RE_AUTH, $main['authority'], $auth);
if (empty($auth) && $main['authority'] !== '') {
throw new InvalidArgumentException("Invalid 'authority'.");
}
if ($this->isEmpty($auth['host']) && !$this->isEmpty($auth['user'])) {
throw new InvalidArgumentException("Invalid 'authority'.");
}
$this->setComponent('user', isset($auth['user']) ? $auth['user'] : '');
$this->setComponent('pass', isset($auth['passc']) ? $auth['pass'] : '');
$this->setComponent('host', isset($auth['host']) ? $auth['host'] : '');
$this->setComponent('port', isset($auth['portc']) ? $auth['port'] : '');
}
}
private function encode(string $source, string $keep = ''): string
{
$exclude = "[^%\/:=&!\$'()*+,;@{$keep}]+";
$exp = "/(%{$exclude})|({$exclude})/";
return preg_replace_callback($exp, function ($matches) {
if ($e = preg_match('/^(%[0-9a-fA-F]{2})/', $matches[0], $m)) {
return substr($matches[0], 0, 3) . rawurlencode(substr($matches[0], 3));
} else {
return rawurlencode($matches[0]);
}
}, $source);
}
private function setComponent(string $component, $value): void
{
$value = $this->parseCompontent($component, $value);
$this->$component = $value;
}
private function parseCompontent(string $component, $value)
{
if ($this->isEmpty($value)) {
return null;
}
switch ($component) {
case 'scheme':
$this->assertString($component, $value);
$this->assertpattern($component, $value, '/^[a-z][a-z0-9-+.]*$/i');
return mb_strtolower($value);
case 'host': // IP-literal / IPv4address / reg-name
$this->assertString($component, $value);
$this->authority = $this->authority || !$this->isEmpty($value);
return mb_strtolower($value);
case 'port':
$this->assertInteger($component, $value);
if ($value < 0 || $value > 65535) {
throw new InvalidArgumentException("Invalid port number");
}
return (int)$value;
case 'path':
$this->assertString($component, $value);
$value = $this->encode($value);
return $value;
case 'user':
case 'pass':
case 'query':
case 'fragment':
$this->assertString($component, $value);
$value = $this->encode($value, '?');
return $value;
}
}
private function getComponent(string $component)
{
return isset($this->$component) ? $this->$component : null;
}
private function formatComponent($value, string $before = '', string $after = ''): string
{
return $this->isEmpty($value) ? '' : "{$before}{$value}{$after}";
}
private function isEmpty($value): bool
{
return is_null($value) || $value === '';
}
private function assertString(string $component, $value): void
{
if (!is_string($value)) {
throw new InvalidArgumentException("Invalid '{$component}': Should be a string");
}
}
private function assertInteger(string $component, $value): void
{
if (!is_numeric($value) || intval($value) != $value) {
throw new InvalidArgumentException("Invalid '{$component}': Should be an integer");
}
}
private function assertPattern(string $component, string $value, string $pattern): void
{
if (preg_match($pattern, $value) == 0) {
throw new InvalidArgumentException("Invalid '{$component}': Should match {$pattern}");
}
}
private function normalizePath(string $path): string
{
$result = [];
preg_match_all('!([^/]*/|[^/]*$)!', $path, $items);
foreach ($items[0] as $item) {
switch ($item) {
case '':
case './':
case '.':
break; // just skip
case '/':
if (empty($result)) {
array_push($result, $item); // add
}
break;
case '..':
case '../':
if (empty($result) || end($result) == '../') {
array_push($result, $item); // add
} else {
array_pop($result); // remove previous
}
break;
default:
array_push($result, $item); // add
}
}
return implode('', $result);
}
private function idna(string $value): string
{
if ($value === '' || !is_callable('idn_to_ascii')) {
return $value; // Can't convert, but don't cause exception
}
return idn_to_ascii($value, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
}
}

View File

@ -1,31 +0,0 @@
<?php
/**
* File for Net\UriFactory class.
* @package Phrity > Net > Uri
* @see https://www.rfc-editor.org/rfc/rfc3986
* @see https://www.php-fig.org/psr/psr-17/#26-urifactoryinterface
*/
namespace Phrity\Net;
use Psr\Http\Message\{
UriFactoryInterface,
UriInterface
};
/**
* Net\UriFactory class.
*/
class UriFactory implements UriFactoryInterface
{
/**
* Create a new URI.
* @param string $uri The URI to parse.
* @throws \InvalidArgumentException If the given URI cannot be parsed
*/
public function createUri(string $uri = ''): UriInterface
{
return new Uri($uri);
}
}

View File

@ -1,93 +0,0 @@
name: Acceptance
on: [push, pull_request]
jobs:
test:
strategy:
matrix:
php-versions: ["7.4", "8.0", "8.1", "8.2", "8.3", "8.4"]
runs-on: ubuntu-latest
name: Unit test
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
coverage: none
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --prefer-dist
- name: Test
run: vendor/bin/phpunit
cs-check:
runs-on: ubuntu-latest
name: Code standard
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
coverage: none
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --prefer-dist
- name: Code standard
run: vendor/bin/phpcs --standard=PSR1,PSR12 --encoding=UTF-8 --report=full --colors src tests
coverage:
runs-on: ubuntu-latest
name: Code coverage
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
coverage: xdebug
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --prefer-dist
- name: Code coverage build
run: XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-clover build/logs/clover.xml
- name: Code coverage upload
env:
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: vendor/bin/php-coveralls -v

View File

@ -1,6 +0,0 @@
.DS_Store
.phpunit.result.cache
build/
composer.lock
composer.phar
vendor/

View File

@ -1,41 +0,0 @@
# Default
all: deps-install
# DEPENDENCY MANAGEMENT
# Updates dependencies according to lock file
deps-install: composer.phar
./composer.phar --no-interaction install
# Updates dependencies according to json file
deps-update: composer.phar
./composer.phar self-update
./composer.phar --no-interaction update
# TESTS AND REPORTS
# Code standard check
cs-check: composer.lock
./vendor/bin/phpcs --standard=PSR1,PSR12 --encoding=UTF-8 --report=full --colors src tests
# Run tests
test: composer.lock
./vendor/bin/phpunit
# Run tests with clover coverage report
coverage: composer.lock
XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml
./vendor/bin/php-coveralls -v
# INITIAL INSTALL
# Ensures composer is installed
composer.phar:
curl -sS https://getcomposer.org/installer | php
# Ensures composer is installed and dependencies loaded
composer.lock: composer.phar
./composer.phar --no-interaction install

View File

@ -1,147 +0,0 @@
[![Build Status](https://github.com/sirn-se/phrity-util-errorhandler/actions/workflows/acceptance.yml/badge.svg)](https://github.com/sirn-se/phrity-util-errorhandler/actions)
[![Coverage Status](https://coveralls.io/repos/github/sirn-se/phrity-util-errorhandler/badge.svg?branch=main)](https://coveralls.io/github/sirn-se/phrity-util-errorhandler?branch=main)
# Error Handler utility
The PHP [error handling](https://www.php.net/manual/en/book.errorfunc.php) can be somewhat of a headache.
Typically an application uses a system level [error handler](https://www.php.net/manual/en/function.set-error-handler.php) and/or suppressing errors using the `@` prefix.
But those cases when your code need to act on triggered errors are more tricky.
This library provides two convenience methods to handle errors on code blocks, either by throwing exceptions or running callback code when an error occurs.
Current version supports PHP `^7.2|^8.0`.
## Installation
Install with [Composer](https://getcomposer.org/);
```
composer require phrity/util-errorhandler
```
## The Error Handler
The class provides two main methods; `with()` and `withAll()`.
The difference is that `with()` will act immediately on an error and abort further code execution, while `withAll()` will attempt to execute the entire code block before acting on errors that occurred.
### Throwing ErrorException
```php
use Phrity\Util\ErrorHandler;
$handler = new ErrorHandler();
$result = $handler->with(function () {
// Code to execute
return $success_result;
});
$result = $handler->withAll(function () {
// Code to execute
return $success_result;
});
```
The examples above will run the callback code, but if an error occurs it will throw an [ErrorException](https://www.php.net/manual/en/class.errorexception.php).
Error message and severity will be that of the triggering error.
* `with()` will throw immediately when occured
* `withAll()` will throw when code is complete; if more than one error occurred, the first will be thrown
### Throwing specified Throwable
```php
use Phrity\Util\ErrorHandler;
$handler = new ErrorHandler();
$result = $handler->with(function () {
// Code to execute
return $success_result;
}, new RuntimeException('A specified error'));
$result = $handler->withAll(function () {
// Code to execute
return $success_result;
}, new RuntimeException('A specified error'));
```
The examples above will run the callback code, but if an error occurs it will throw provided Throwable.
The thrown Throwable will have an [ErrorException](https://www.php.net/manual/en/class.errorexception.php) attached as `$previous`.
* `with()` will throw immediately when occured
* `withAll()` will throw when code is complete; if more than one error occurred, the first will be thrown
### Using callback
```php
use Phrity\Util\ErrorHandler;
$handler = new ErrorHandler();
$result = $handler->with(function () {
// Code to execute
return $success_result;
}, function (ErrorException $error) {
// Code to handle error
return $error_result;
});
$result = $handler->withAll(function () {
// Code to execute
return $success_result;
}, function (array $errors, $success_result) {
// Code to handle errors
return $error_result;
});
```
The examples above will run the callback code, but if an error occurs it will call the error callback as well.
* `with()` will run the error callback immediately when error occured; error callback expects an ErrorException instance
* `withAll()` will run the error callback when code is complete; error callback expects an array of ErrorException and the returned result of code callback
### Filtering error types
Both `with()` and `withAll()` accepts error level(s) as last parameter.
```php
use Phrity\Util\ErrorHandler;
$handler = new ErrorHandler();
$result = $handler->with(function () {
// Code to execute
return $success_result;
}, null, E_USER_ERROR);
$result = $handler->withAll(function () {
// Code to execute
return $success_result;
}, null, E_USER_ERROR & E_USER_WARNING);
```
Any value or combination of values accepted by [set_error_handler](https://www.php.net/manual/en/function.set-error-handler.php) is usable.
Default is `E_ALL`. [List of constants](https://www.php.net/manual/en/errorfunc.constants.php).
### The global error handler
The class also has global `set()` and `restore()` methods.
```php
use Phrity\Util\ErrorHandler;
$handler = new ErrorHandler();
$handler->set(); // Throws ErrorException on error
$handler->set(new RuntimeException('A specified error')); // Throws provided Throwable on error
$handler->set(function (ErrorException $error) {
// Code to handle errors
return $error_result;
}); // Runs callback on error
$handler->restore(); // Restores error handler
```
### Class synopsis
```php
Phrity\Util\ErrorHandler {
/* Methods */
public __construct()
public with(callable $callback, mixed $handling = null, int $levels = E_ALL) : mixed
public withAll(callable $callback, mixed $handling = null, int $levels = E_ALL) : mixed
public set($handling = null, int $levels = E_ALL) : mixed
public restore() : bool
}
```
## Versions
| Version | PHP | |
| --- | --- | --- |
| `1.1` | `^7.4\|^8.0` | Some improvements |
| `1.0` | `^7.2\|^8.0` | Initial version |

View File

@ -1,33 +0,0 @@
{
"name": "phrity/util-errorhandler",
"type": "library",
"description": "Inline error handler; catch and resolve errors for code block.",
"homepage": "https://phrity.sirn.se/util-errorhandler",
"keywords": ["error", "warning"],
"license": "MIT",
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"autoload": {
"psr-4": {
"Phrity\\Util\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Phrity\\Util\\Tests\\": "tests/"
}
},
"require": {
"php": "^7.4 | ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^9.0 | ^10.0 | ^11.0",
"php-coveralls/php-coveralls": "^2.0",
"squizlabs/php_codesniffer": "^3.5"
}
}

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" colors="true" bootstrap="vendor/autoload.php" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.0/phpunit.xsd">
<testsuites>
<testsuite name="Phrity Util/Accessor tests">
<directory>./tests/</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">./src/</directory>
</include>
</source>
</phpunit>

View File

@ -1,121 +0,0 @@
<?php
/**
* File for ErrorHandler utility class.
* @package Phrity > Util > ErrorHandler
*/
namespace Phrity\Util;
use ErrorException;
use Throwable;
/**
* ErrorHandler utility class.
* Allows catching and resolving errors inline.
*/
class ErrorHandler
{
/* ----------------- Public methods ---------------------------------------------- */
/**
* Set error handler to run until removed.
* @param mixed $handling
* - If null, handler will throw ErrorException
* - If Throwable $t, throw $t with ErrorException attached as previous
* - If callable, will invoke callback with ErrorException as argument
* @param int $levels Error levels to catch, all errors by default
* @return mixed Previously registered error handler, if any
*/
public function set($handling = null, int $levels = E_ALL)
{
return set_error_handler($this->getHandler($handling), $levels);
}
/**
* Remove error handler.
* @return bool True if removed
*/
public function restore(): bool
{
return restore_error_handler();
}
/**
* Run code with error handling, breaks on first encountered error.
* @param callable $callback The code to run
* @param mixed $handling
* - If null, handler will throw ErrorException
* - If Throwable $t, throw $t with ErrorException attached as previous
* - If callable, will invoke callback with ErrorException as argument
* @param int $levels Error levels to catch, all errors by default
* @return mixed Return what $callback returns, or what $handling retuns on error
*/
public function with(callable $callback, $handling = null, int $levels = E_ALL)
{
$error = null;
$result = null;
try {
$this->set(null, $levels);
$result = $callback();
} catch (ErrorException $e) {
$error = $this->handle($handling, $e);
} finally {
$this->restore();
}
return $error ?? $result;
}
/**
* Run code with error handling, comletes code before handling errors
* @param callable $callback The code to run
* @param mixed $handling
* - If null, handler will throw ErrorException
* - If Throwable $t, throw $t with ErrorException attached as previous
* - If callable, will invoke callback with ErrorException as argument
* @param int $levels Error levels to catch, all errors by default
* @return mixed Return what $callback returns, or what $handling retuns on error
*/
public function withAll(callable $callback, $handling = null, int $levels = E_ALL)
{
$errors = [];
$this->set(function (ErrorException $e) use (&$errors) {
$errors[] = $e;
}, $levels);
$result = $callback();
$this->restore();
$error = empty($errors) ? null : $this->handle($handling, $errors, $result);
return $error ?? $result;
}
/* ----------------- Private helpers --------------------------------------------- */
// Get handler function
private function getHandler($handling)
{
return function ($severity, $message, $file, $line) use ($handling) {
$error = new ErrorException($message, 0, $severity, $file, $line);
$this->handle($handling, $error);
};
}
// Handle error according to $handlig type
private function handle($handling, $error, $result = null)
{
if (is_callable($handling)) {
return $handling($error, $result);
}
if (is_array($error)) {
$error = array_shift($error);
}
if ($handling instanceof Throwable) {
try {
throw $error;
} finally {
throw $handling;
}
}
throw $error;
}
}

View File

@ -1,303 +0,0 @@
<?php
/**
* File for ErrorHandler function tests.
* @package Phrity > Util > ErrorHandler
*/
declare(strict_types=1);
namespace Phrity\Util;
use ErrorException;
use RuntimeException;
use Phrity\Util\ErrorHandler;
use PHPUnit\Framework\TestCase;
/**
* ErrorHandler test class.
*/
class ErrorHandlerTest extends TestCase
{
/**
* Set up for all tests
*/
public function setUp(): void
{
error_reporting(-1);
}
public function testSetNull(): void
{
$handler = new ErrorHandler();
$handler->set();
// Verify exception
try {
trigger_error('An error');
} catch (ErrorException $e) {
$this->assertEquals('An error', $e->getMessage());
$this->assertEquals(0, $e->getCode());
$this->assertEquals(E_USER_NOTICE, $e->getSeverity());
$this->assertNull($e->getPrevious());
}
// Restore handler
$this->assertTrue($handler->restore());
}
public function testSetThrowable(): void
{
$handler = new ErrorHandler();
$handler->set(new RuntimeException('A provided exception', 23));
// Verify exception
try {
trigger_error('An error');
} catch (RuntimeException $e) {
$this->assertEquals('A provided exception', $e->getMessage());
$this->assertEquals(23, $e->getCode());
$this->assertNotNull($e->getPrevious());
$prev = $e->getPrevious();
$this->assertEquals('An error', $prev->getMessage());
$this->assertEquals(0, $prev->getCode());
$this->assertEquals(E_USER_NOTICE, $prev->getSeverity());
$this->assertNull($prev->getPrevious());
}
// Restore handler
$this->assertTrue($handler->restore());
}
public function testSetCallback(): void
{
$handler = new ErrorHandler();
$result = null;
$handler->set(function ($error) use (&$result) {
$result = [
'message' => $error->getMessage(),
'code' => $error->getCode(),
'severity' => $error->getSeverity(),
];
});
// Verify exception
trigger_error('An error');
$this->assertEquals([
'message' => 'An error',
'code' => 0,
'severity' => E_USER_NOTICE,
], $result);
// Restore handler
$this->assertTrue($handler->restore());
}
public function testWithNull(): void
{
$handler = new ErrorHandler();
$check = false;
// No exception
$result = $handler->with(function () {
return 'Code success';
});
$this->assertEquals('Code success', $result);
// Verify exception
try {
$result = $handler->with(function () use (&$check) {
trigger_error('An error');
$check = true;
return 'Code success';
});
} catch (ErrorException $e) {
$this->assertEquals('An error', $e->getMessage());
$this->assertEquals(0, $e->getCode());
$this->assertEquals(E_USER_NOTICE, $e->getSeverity());
$this->assertNull($e->getPrevious());
}
$this->assertFalse($check);
// Verify that exception is thrown
$this->expectException('ErrorException');
$result = $handler->with(function () {
trigger_error('An error');
return 'Code success';
});
}
public function testWithThrowable(): void
{
$handler = new ErrorHandler();
$check = false;
// No exception
$result = $handler->with(function () {
return 'Code success';
});
$this->assertEquals('Code success', $result);
// Verify exception
try {
$result = $handler->with(function () use (&$check) {
trigger_error('An error');
$check = true;
return 'Code success';
}, new RuntimeException('A provided exception', 23));
} catch (RuntimeException $e) {
$this->assertEquals('A provided exception', $e->getMessage());
$this->assertEquals(23, $e->getCode());
$this->assertNotNull($e->getPrevious());
$prev = $e->getPrevious();
$this->assertEquals('An error', $prev->getMessage());
$this->assertEquals(0, $prev->getCode());
$this->assertEquals(E_USER_NOTICE, $prev->getSeverity());
$this->assertNull($prev->getPrevious());
}
$this->assertFalse($check);
// Verify that exception is thrown
$this->expectException('RuntimeException');
$result = $handler->with(function () {
trigger_error('An error');
return 'Code success';
}, new RuntimeException('A provided exception', 23));
}
public function testWithCallback(): void
{
$handler = new ErrorHandler();
$check = false;
// No error invoked
$result = $handler->with(function () {
return 'Code success';
}, function ($error) {
return $error;
});
$this->assertEquals('Code success', $result);
// An error is invoked
$result = $handler->with(function () use (&$check) {
trigger_error('An error');
$check = true;
return 'Code success';
}, function ($error) {
return $error;
});
$this->assertFalse($check);
$this->assertEquals('An error', $result->getMessage());
$this->assertEquals(0, $result->getCode());
$this->assertEquals(E_USER_NOTICE, $result->getSeverity());
$this->assertNull($result->getPrevious());
}
public function testWithAllNull(): void
{
$handler = new ErrorHandler();
$check = false;
// No error invoked
$result = $handler->withAll(function () {
return 'Code success';
});
$this->assertEquals('Code success', $result);
// Verify exception
try {
$result = $handler->withAll(function () use (&$check) {
trigger_error('An error');
$check = true;
return 'Code success';
});
} catch (ErrorException $e) {
$this->assertEquals('An error', $e->getMessage());
$this->assertEquals(0, $e->getCode());
$this->assertEquals(E_USER_NOTICE, $e->getSeverity());
$this->assertNull($e->getPrevious());
}
$this->assertTrue($check);
// Verify that exception is thrown
$this->expectException('ErrorException');
$result = $handler->withAll(function () {
trigger_error('An error');
return 'Code success';
});
}
public function testWithAllThrowable(): void
{
$handler = new ErrorHandler();
$check = false;
// No exception
$result = $handler->withAll(function () {
return 'Code success';
});
$this->assertEquals('Code success', $result);
// Verify exception
try {
$result = $handler->withAll(function () use (&$check) {
trigger_error('An error');
$check = true;
return 'Code success';
}, new RuntimeException('A provided exception', 23));
} catch (RuntimeException $e) {
$this->assertEquals('A provided exception', $e->getMessage());
$this->assertEquals(23, $e->getCode());
$this->assertNotNull($e->getPrevious());
$prev = $e->getPrevious();
$this->assertEquals('An error', $prev->getMessage());
$this->assertEquals(0, $prev->getCode());
$this->assertEquals(E_USER_NOTICE, $prev->getSeverity());
$this->assertNull($prev->getPrevious());
}
$this->assertTrue($check);
// Verify that exception is thrown
$this->expectException('RuntimeException');
$result = $handler->withAll(function () {
trigger_error('An error');
return 'Code success';
}, new RuntimeException('A provided exception', 23));
}
public function testWithAllCallback(): void
{
$handler = new ErrorHandler();
$check = false;
// No error invoked
$result = $handler->withAll(function () {
return 'Code success';
}, function ($error, $result) {
return $error;
});
$this->assertEquals('Code success', $result);
// An error is invoked
$result = $handler->withAll(function () use (&$check) {
trigger_error('An error');
trigger_error('Another error', E_USER_WARNING);
$check = true;
return 'Code success';
}, function ($errors, $result) {
return ['errors' => $errors, 'result' => $result];
});
$this->assertTrue($check);
$this->assertEquals('Code success', $result['result']);
$this->assertEquals('An error', $result['errors'][0]->getMessage());
$this->assertEquals(0, $result['errors'][0]->getCode());
$this->assertEquals(E_USER_NOTICE, $result['errors'][0]->getSeverity());
$this->assertNull($result['errors'][0]->getPrevious());
$this->assertEquals('Another error', $result['errors'][1]->getMessage());
$this->assertEquals(0, $result['errors'][1]->getCode());
$this->assertEquals(E_USER_WARNING, $result['errors'][1]->getSeverity());
$this->assertNull($result['errors'][1]->getPrevious());
}
}

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2018 PHP-FIG
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,12 +0,0 @@
HTTP Factories
==============
This repository holds all interfaces related to [PSR-17 (HTTP Factories)][psr-url].
Note that this is not a HTTP Factory implementation of its own. It is merely interfaces that describe the components of a HTTP Factory.
The installable [package][package-url] and [implementations][implementation-url] are listed on Packagist.
[psr-url]: https://www.php-fig.org/psr/psr-17/
[package-url]: https://packagist.org/packages/psr/http-factory
[implementation-url]: https://packagist.org/providers/psr/http-factory-implementation

View File

@ -1,38 +0,0 @@
{
"name": "psr/http-factory",
"description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
"keywords": [
"psr",
"psr-7",
"psr-17",
"http",
"factory",
"message",
"request",
"response"
],
"license": "MIT",
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"support": {
"source": "https://github.com/php-fig/http-factory"
},
"require": {
"php": ">=7.0.0",
"psr/http-message": "^1.0 || ^2.0"
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
}
}

View File

@ -1,18 +0,0 @@
<?php
namespace Psr\Http\Message;
interface RequestFactoryInterface
{
/**
* Create a new request.
*
* @param string $method The HTTP method associated with the request.
* @param UriInterface|string $uri The URI associated with the request. If
* the value is a string, the factory MUST create a UriInterface
* instance based on it.
*
* @return RequestInterface
*/
public function createRequest(string $method, $uri): RequestInterface;
}

View File

@ -1,18 +0,0 @@
<?php
namespace Psr\Http\Message;
interface ResponseFactoryInterface
{
/**
* Create a new response.
*
* @param int $code HTTP status code; defaults to 200
* @param string $reasonPhrase Reason phrase to associate with status code
* in generated response; if none is provided implementations MAY use
* the defaults as suggested in the HTTP specification.
*
* @return ResponseInterface
*/
public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface;
}

View File

@ -1,24 +0,0 @@
<?php
namespace Psr\Http\Message;
interface ServerRequestFactoryInterface
{
/**
* Create a new server request.
*
* Note that server-params are taken precisely as given - no parsing/processing
* of the given values is performed, and, in particular, no attempt is made to
* determine the HTTP method or URI, which must be provided explicitly.
*
* @param string $method The HTTP method associated with the request.
* @param UriInterface|string $uri The URI associated with the request. If
* the value is a string, the factory MUST create a UriInterface
* instance based on it.
* @param array $serverParams Array of SAPI parameters with which to seed
* the generated request instance.
*
* @return ServerRequestInterface
*/
public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface;
}

View File

@ -1,45 +0,0 @@
<?php
namespace Psr\Http\Message;
interface StreamFactoryInterface
{
/**
* Create a new stream from a string.
*
* The stream SHOULD be created with a temporary resource.
*
* @param string $content String content with which to populate the stream.
*
* @return StreamInterface
*/
public function createStream(string $content = ''): StreamInterface;
/**
* Create a stream from an existing file.
*
* The file MUST be opened using the given mode, which may be any mode
* supported by the `fopen` function.
*
* The `$filename` MAY be any string supported by `fopen()`.
*
* @param string $filename Filename or stream URI to use as basis of stream.
* @param string $mode Mode with which to open the underlying filename/stream.
*
* @return StreamInterface
* @throws \RuntimeException If the file cannot be opened.
* @throws \InvalidArgumentException If the mode is invalid.
*/
public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface;
/**
* Create a new stream from an existing resource.
*
* The stream MUST be readable and may be writable.
*
* @param resource $resource PHP resource to use as basis of stream.
*
* @return StreamInterface
*/
public function createStreamFromResource($resource): StreamInterface;
}

View File

@ -1,34 +0,0 @@
<?php
namespace Psr\Http\Message;
interface UploadedFileFactoryInterface
{
/**
* Create a new uploaded file.
*
* If a size is not provided it will be determined by checking the size of
* the file.
*
* @see http://php.net/manual/features.file-upload.post-method.php
* @see http://php.net/manual/features.file-upload.errors.php
*
* @param StreamInterface $stream Underlying stream representing the
* uploaded file content.
* @param int $size in bytes
* @param int $error PHP file upload error
* @param string $clientFilename Filename as provided by the client, if any.
* @param string $clientMediaType Media type as provided by the client, if any.
*
* @return UploadedFileInterface
*
* @throws \InvalidArgumentException If the file resource is not readable.
*/
public function createUploadedFile(
StreamInterface $stream,
int $size = null,
int $error = \UPLOAD_ERR_OK,
string $clientFilename = null,
string $clientMediaType = null
): UploadedFileInterface;
}

View File

@ -1,17 +0,0 @@
<?php
namespace Psr\Http\Message;
interface UriFactoryInterface
{
/**
* Create a new URI.
*
* @param string $uri
*
* @return UriInterface
*
* @throws \InvalidArgumentException If the given URI cannot be parsed.
*/
public function createUri(string $uri = ''): UriInterface;
}

View File

@ -1,36 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file, in reverse chronological order by release.
## 1.0.1 - 2016-08-06
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Updated all `@return self` annotation references in interfaces to use
`@return static`, which more closelly follows the semantics of the
specification.
- Updated the `MessageInterface::getHeaders()` return annotation to use the
value `string[][]`, indicating the format is a nested array of strings.
- Updated the `@link` annotation for `RequestInterface::withRequestTarget()`
to point to the correct section of RFC 7230.
- Updated the `ServerRequestInterface::withUploadedFiles()` parameter annotation
to add the parameter name (`$uploadedFiles`).
- Updated a `@throws` annotation for the `UploadedFileInterface::moveTo()`
method to correctly reference the method parameter (it was referencing an
incorrect parameter name previously).
## 1.0.0 - 2016-05-18
Initial stable release; reflects accepted PSR-7 specification.

View File

@ -1,19 +0,0 @@
Copyright (c) 2014 PHP Framework Interoperability Group
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,16 +0,0 @@
PSR Http Message
================
This repository holds all interfaces/classes/traits related to
[PSR-7](http://www.php-fig.org/psr/psr-7/).
Note that this is not a HTTP message implementation of its own. It is merely an
interface that describes a HTTP message. See the specification for more details.
Usage
-----
Before reading the usage guide we recommend reading the PSR-7 interfaces method list:
* [`PSR-7 Interfaces Method List`](docs/PSR7-Interfaces.md)
* [`PSR-7 Usage Guide`](docs/PSR7-Usage.md)

View File

@ -1,26 +0,0 @@
{
"name": "psr/http-message",
"description": "Common interface for HTTP messages",
"keywords": ["psr", "psr-7", "http", "http-message", "request", "response"],
"homepage": "https://github.com/php-fig/http-message",
"license": "MIT",
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"require": {
"php": "^7.2 || ^8.0"
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
}
}

View File

@ -1,130 +0,0 @@
# Interfaces
The purpose of this list is to help in finding the methods when working with PSR-7. This can be considered as a cheatsheet for PSR-7 interfaces.
The interfaces defined in PSR-7 are the following:
| Class Name | Description |
|---|---|
| [Psr\Http\Message\MessageInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessagemessageinterface) | Representation of a HTTP message |
| [Psr\Http\Message\RequestInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessagerequestinterface) | Representation of an outgoing, client-side request. |
| [Psr\Http\Message\ServerRequestInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageserverrequestinterface) | Representation of an incoming, server-side HTTP request. |
| [Psr\Http\Message\ResponseInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageresponseinterface) | Representation of an outgoing, server-side response. |
| [Psr\Http\Message\StreamInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessagestreaminterface) | Describes a data stream |
| [Psr\Http\Message\UriInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageuriinterface) | Value object representing a URI. |
| [Psr\Http\Message\UploadedFileInterface](http://www.php-fig.org/psr/psr-7/#psrhttpmessageuploadedfileinterface) | Value object representing a file uploaded through an HTTP request. |
## `Psr\Http\Message\MessageInterface` Methods
| Method Name | Description | Notes |
|------------------------------------| ----------- | ----- |
| `getProtocolVersion()` | Retrieve HTTP protocol version | 1.0 or 1.1 |
| `withProtocolVersion($version)` | Returns new message instance with given HTTP protocol version | |
| `getHeaders()` | Retrieve all HTTP Headers | [Request Header List](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields), [Response Header List](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Response_fields) |
| `hasHeader($name)` | Checks if HTTP Header with given name exists | |
| `getHeader($name)` | Retrieves a array with the values for a single header | |
| `getHeaderLine($name)` | Retrieves a comma-separated string of the values for a single header | |
| `withHeader($name, $value)` | Returns new message instance with given HTTP Header | if the header existed in the original instance, replaces the header value from the original message with the value provided when creating the new instance. |
| `withAddedHeader($name, $value)` | Returns new message instance with appended value to given header | If header already exists value will be appended, if not a new header will be created |
| `withoutHeader($name)` | Removes HTTP Header with given name| |
| `getBody()` | Retrieves the HTTP Message Body | Returns object implementing `StreamInterface`|
| `withBody(StreamInterface $body)` | Returns new message instance with given HTTP Message Body | |
## `Psr\Http\Message\RequestInterface` Methods
Same methods as `Psr\Http\Message\MessageInterface` + the following methods:
| Method Name | Description | Notes |
|------------------------------------| ----------- | ----- |
| `getRequestTarget()` | Retrieves the message's request target | origin-form, absolute-form, authority-form, asterisk-form ([RFC7230](https://www.rfc-editor.org/rfc/rfc7230.txt)) |
| `withRequestTarget($requestTarget)` | Return a new message instance with the specific request-target | |
| `getMethod()` | Retrieves the HTTP method of the request. | GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE (defined in [RFC7231](https://tools.ietf.org/html/rfc7231)), PATCH (defined in [RFC5789](https://tools.ietf.org/html/rfc5789)) |
| `withMethod($method)` | Returns a new message instance with the provided HTTP method | |
| `getUri()` | Retrieves the URI instance | |
| `withUri(UriInterface $uri, $preserveHost = false)` | Returns a new message instance with the provided URI | |
## `Psr\Http\Message\ServerRequestInterface` Methods
Same methods as `Psr\Http\Message\RequestInterface` + the following methods:
| Method Name | Description | Notes |
|------------------------------------| ----------- | ----- |
| `getServerParams() ` | Retrieve server parameters | Typically derived from `$_SERVER` |
| `getCookieParams()` | Retrieves cookies sent by the client to the server. | Typically derived from `$_COOKIES` |
| `withCookieParams(array $cookies)` | Returns a new request instance with the specified cookies | |
| `withQueryParams(array $query)` | Returns a new request instance with the specified query string arguments | |
| `getUploadedFiles()` | Retrieve normalized file upload data | |
| `withUploadedFiles(array $uploadedFiles)` | Returns a new request instance with the specified uploaded files | |
| `getParsedBody()` | Retrieve any parameters provided in the request body | |
| `withParsedBody($data)` | Returns a new request instance with the specified body parameters | |
| `getAttributes()` | Retrieve attributes derived from the request | |
| `getAttribute($name, $default = null)` | Retrieve a single derived request attribute | |
| `withAttribute($name, $value)` | Returns a new request instance with the specified derived request attribute | |
| `withoutAttribute($name)` | Returns a new request instance that without the specified derived request attribute | |
## `Psr\Http\Message\ResponseInterface` Methods:
Same methods as `Psr\Http\Message\MessageInterface` + the following methods:
| Method Name | Description | Notes |
|------------------------------------| ----------- | ----- |
| `getStatusCode()` | Gets the response status code. | |
| `withStatus($code, $reasonPhrase = '')` | Returns a new response instance with the specified status code and, optionally, reason phrase. | |
| `getReasonPhrase()` | Gets the response reason phrase associated with the status code. | |
## `Psr\Http\Message\StreamInterface` Methods
| Method Name | Description | Notes |
|------------------------------------| ----------- | ----- |
| `__toString()` | Reads all data from the stream into a string, from the beginning to end. | |
| `close()` | Closes the stream and any underlying resources. | |
| `detach()` | Separates any underlying resources from the stream. | |
| `getSize()` | Get the size of the stream if known. | |
| `eof()` | Returns true if the stream is at the end of the stream.| |
| `isSeekable()` | Returns whether or not the stream is seekable. | |
| `seek($offset, $whence = SEEK_SET)` | Seek to a position in the stream. | |
| `rewind()` | Seek to the beginning of the stream. | |
| `isWritable()` | Returns whether or not the stream is writable. | |
| `write($string)` | Write data to the stream. | |
| `isReadable()` | Returns whether or not the stream is readable. | |
| `read($length)` | Read data from the stream. | |
| `getContents()` | Returns the remaining contents in a string | |
| `getMetadata($key = null)()` | Get stream metadata as an associative array or retrieve a specific key. | |
## `Psr\Http\Message\UriInterface` Methods
| Method Name | Description | Notes |
|------------------------------------| ----------- | ----- |
| `getScheme()` | Retrieve the scheme component of the URI. | |
| `getAuthority()` | Retrieve the authority component of the URI. | |
| `getUserInfo()` | Retrieve the user information component of the URI. | |
| `getHost()` | Retrieve the host component of the URI. | |
| `getPort()` | Retrieve the port component of the URI. | |
| `getPath()` | Retrieve the path component of the URI. | |
| `getQuery()` | Retrieve the query string of the URI. | |
| `getFragment()` | Retrieve the fragment component of the URI. | |
| `withScheme($scheme)` | Return an instance with the specified scheme. | |
| `withUserInfo($user, $password = null)` | Return an instance with the specified user information. | |
| `withHost($host)` | Return an instance with the specified host. | |
| `withPort($port)` | Return an instance with the specified port. | |
| `withPath($path)` | Return an instance with the specified path. | |
| `withQuery($query)` | Return an instance with the specified query string. | |
| `withFragment($fragment)` | Return an instance with the specified URI fragment. | |
| `__toString()` | Return the string representation as a URI reference. | |
## `Psr\Http\Message\UploadedFileInterface` Methods
| Method Name | Description | Notes |
|------------------------------------| ----------- | ----- |
| `getStream()` | Retrieve a stream representing the uploaded file. | |
| `moveTo($targetPath)` | Move the uploaded file to a new location. | |
| `getSize()` | Retrieve the file size. | |
| `getError()` | Retrieve the error associated with the uploaded file. | |
| `getClientFilename()` | Retrieve the filename sent by the client. | |
| `getClientMediaType()` | Retrieve the media type sent by the client. | |
> `RequestInterface`, `ServerRequestInterface`, `ResponseInterface` extend `MessageInterface` because the `Request` and the `Response` are `HTTP Messages`.
> When using `ServerRequestInterface`, both `RequestInterface` and `Psr\Http\Message\MessageInterface` methods are considered.

View File

@ -1,159 +0,0 @@
### PSR-7 Usage
All PSR-7 applications comply with these interfaces
They were created to establish a standard between middleware implementations.
> `RequestInterface`, `ServerRequestInterface`, `ResponseInterface` extend `MessageInterface` because the `Request` and the `Response` are `HTTP Messages`.
> When using `ServerRequestInterface`, both `RequestInterface` and `Psr\Http\Message\MessageInterface` methods are considered.
The following examples will illustrate how basic operations are done in PSR-7.
##### Examples
For this examples to work (at least) a PSR-7 implementation package is required. (eg: zendframework/zend-diactoros, guzzlehttp/psr7, slim/slim, etc)
All PSR-7 implementations should have the same behaviour.
The following will be assumed:
`$request` is an object of `Psr\Http\Message\RequestInterface` and
`$response` is an object implementing `Psr\Http\Message\RequestInterface`
### Working with HTTP Headers
#### Adding headers to response:
```php
$response->withHeader('My-Custom-Header', 'My Custom Message');
```
#### Appending values to headers
```php
$response->withAddedHeader('My-Custom-Header', 'The second message');
```
#### Checking if header exists:
```php
$request->hasHeader('My-Custom-Header'); // will return false
$response->hasHeader('My-Custom-Header'); // will return true
```
> Note: My-Custom-Header was only added in the Response
#### Getting comma-separated values from a header (also applies to request)
```php
// getting value from request headers
$request->getHeaderLine('Content-Type'); // will return: "text/html; charset=UTF-8"
// getting value from response headers
$response->getHeaderLine('My-Custom-Header'); // will return: "My Custom Message; The second message"
```
#### Getting array of value from a header (also applies to request)
```php
// getting value from request headers
$request->getHeader('Content-Type'); // will return: ["text/html", "charset=UTF-8"]
// getting value from response headers
$response->getHeader('My-Custom-Header'); // will return: ["My Custom Message", "The second message"]
```
#### Removing headers from HTTP Messages
```php
// removing a header from Request, removing deprecated "Content-MD5" header
$request->withoutHeader('Content-MD5');
// removing a header from Response
// effect: the browser won't know the size of the stream
// the browser will download the stream till it ends
$response->withoutHeader('Content-Length');
```
### Working with HTTP Message Body
When working with the PSR-7 there are two methods of implementation:
#### 1. Getting the body separately
> This method makes the body handling easier to understand and is useful when repeatedly calling body methods. (You only call `getBody()` once). Using this method mistakes like `$response->write()` are also prevented.
```php
$body = $response->getBody();
// operations on body, eg. read, write, seek
// ...
// replacing the old body
$response->withBody($body);
// this last statement is optional as we working with objects
// in this case the "new" body is same with the "old" one
// the $body variable has the same value as the one in $request, only the reference is passed
```
#### 2. Working directly on response
> This method is useful when only performing few operations as the `$request->getBody()` statement fragment is required
```php
$response->getBody()->write('hello');
```
### Getting the body contents
The following snippet gets the contents of a stream contents.
> Note: Streams must be rewinded, if content was written into streams, it will be ignored when calling `getContents()` because the stream pointer is set to the last character, which is `\0` - meaning end of stream.
```php
$body = $response->getBody();
$body->rewind(); // or $body->seek(0);
$bodyText = $body->getContents();
```
> Note: If `$body->seek(1)` is called before `$body->getContents()`, the first character will be ommited as the starting pointer is set to `1`, not `0`. This is why using `$body->rewind()` is recommended.
### Append to body
```php
$response->getBody()->write('Hello'); // writing directly
$body = $request->getBody(); // which is a `StreamInterface`
$body->write('xxxxx');
```
### Prepend to body
Prepending is different when it comes to streams. The content must be copied before writing the content to be prepended.
The following example will explain the behaviour of streams.
```php
// assuming our response is initially empty
$body = $repsonse->getBody();
// writing the string "abcd"
$body->write('abcd');
// seeking to start of stream
$body->seek(0);
// writing 'ef'
$body->write('ef'); // at this point the stream contains "efcd"
```
#### Prepending by rewriting separately
```php
// assuming our response body stream only contains: "abcd"
$body = $response->getBody();
$body->rewind();
$contents = $body->getContents(); // abcd
// seeking the stream to beginning
$body->rewind();
$body->write('ef'); // stream contains "efcd"
$body->write($contents); // stream contains "efabcd"
```
> Note: `getContents()` seeks the stream while reading it, therefore if the second `rewind()` method call was not present the stream would have resulted in `abcdefabcd` because the `write()` method appends to stream if not preceeded by `rewind()` or `seek(0)`.
#### Prepending by using contents as a string
```php
$body = $response->getBody();
$body->rewind();
$contents = $body->getContents(); // efabcd
$contents = 'ef'.$contents;
$body->rewind();
$body->write($contents);
```

View File

@ -1,189 +0,0 @@
<?php
declare(strict_types=1);
namespace Psr\Http\Message;
/**
* HTTP messages consist of requests from a client to a server and responses
* from a server to a client. This interface defines the methods common to
* each.
*
* Messages are considered immutable; all methods that might change state MUST
* be implemented such that they retain the internal state of the current
* message and return an instance that contains the changed state.
*
* @link http://www.ietf.org/rfc/rfc7230.txt
* @link http://www.ietf.org/rfc/rfc7231.txt
*/
interface MessageInterface
{
/**
* Retrieves the HTTP protocol version as a string.
*
* The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
*
* @return string HTTP protocol version.
*/
public function getProtocolVersion();
/**
* Return an instance with the specified HTTP protocol version.
*
* The version string MUST contain only the HTTP version number (e.g.,
* "1.1", "1.0").
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new protocol version.
*
* @param string $version HTTP protocol version
* @return static
*/
public function withProtocolVersion(string $version);
/**
* Retrieves all message header values.
*
* The keys represent the header name as it will be sent over the wire, and
* each value is an array of strings associated with the header.
*
* // Represent the headers as a string
* foreach ($message->getHeaders() as $name => $values) {
* echo $name . ": " . implode(", ", $values);
* }
*
* // Emit headers iteratively:
* foreach ($message->getHeaders() as $name => $values) {
* foreach ($values as $value) {
* header(sprintf('%s: %s', $name, $value), false);
* }
* }
*
* While header names are not case-sensitive, getHeaders() will preserve the
* exact case in which headers were originally specified.
*
* @return string[][] Returns an associative array of the message's headers. Each
* key MUST be a header name, and each value MUST be an array of strings
* for that header.
*/
public function getHeaders();
/**
* Checks if a header exists by the given case-insensitive name.
*
* @param string $name Case-insensitive header field name.
* @return bool Returns true if any header names match the given header
* name using a case-insensitive string comparison. Returns false if
* no matching header name is found in the message.
*/
public function hasHeader(string $name);
/**
* Retrieves a message header value by the given case-insensitive name.
*
* This method returns an array of all the header values of the given
* case-insensitive header name.
*
* If the header does not appear in the message, this method MUST return an
* empty array.
*
* @param string $name Case-insensitive header field name.
* @return string[] An array of string values as provided for the given
* header. If the header does not appear in the message, this method MUST
* return an empty array.
*/
public function getHeader(string $name);
/**
* Retrieves a comma-separated string of the values for a single header.
*
* This method returns all of the header values of the given
* case-insensitive header name as a string concatenated together using
* a comma.
*
* NOTE: Not all header values may be appropriately represented using
* comma concatenation. For such headers, use getHeader() instead
* and supply your own delimiter when concatenating.
*
* If the header does not appear in the message, this method MUST return
* an empty string.
*
* @param string $name Case-insensitive header field name.
* @return string A string of values as provided for the given header
* concatenated together using a comma. If the header does not appear in
* the message, this method MUST return an empty string.
*/
public function getHeaderLine(string $name);
/**
* Return an instance with the provided value replacing the specified header.
*
* While header names are case-insensitive, the casing of the header will
* be preserved by this function, and returned from getHeaders().
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new and/or updated header and value.
*
* @param string $name Case-insensitive header field name.
* @param string|string[] $value Header value(s).
* @return static
* @throws \InvalidArgumentException for invalid header names or values.
*/
public function withHeader(string $name, $value);
/**
* Return an instance with the specified header appended with the given value.
*
* Existing values for the specified header will be maintained. The new
* value(s) will be appended to the existing list. If the header did not
* exist previously, it will be added.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new header and/or value.
*
* @param string $name Case-insensitive header field name to add.
* @param string|string[] $value Header value(s).
* @return static
* @throws \InvalidArgumentException for invalid header names or values.
*/
public function withAddedHeader(string $name, $value);
/**
* Return an instance without the specified header.
*
* Header resolution MUST be done without case-sensitivity.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that removes
* the named header.
*
* @param string $name Case-insensitive header field name to remove.
* @return static
*/
public function withoutHeader(string $name);
/**
* Gets the body of the message.
*
* @return StreamInterface Returns the body as a stream.
*/
public function getBody();
/**
* Return an instance with the specified message body.
*
* The body MUST be a StreamInterface object.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return a new instance that has the
* new body stream.
*
* @param StreamInterface $body Body.
* @return static
* @throws \InvalidArgumentException When the body is not valid.
*/
public function withBody(StreamInterface $body);
}

View File

@ -1,131 +0,0 @@
<?php
declare(strict_types=1);
namespace Psr\Http\Message;
/**
* Representation of an outgoing, client-side request.
*
* Per the HTTP specification, this interface includes properties for
* each of the following:
*
* - Protocol version
* - HTTP method
* - URI
* - Headers
* - Message body
*
* During construction, implementations MUST attempt to set the Host header from
* a provided URI if no Host header is provided.
*
* Requests are considered immutable; all methods that might change state MUST
* be implemented such that they retain the internal state of the current
* message and return an instance that contains the changed state.
*/
interface RequestInterface extends MessageInterface
{
/**
* Retrieves the message's request target.
*
* Retrieves the message's request-target either as it will appear (for
* clients), as it appeared at request (for servers), or as it was
* specified for the instance (see withRequestTarget()).
*
* In most cases, this will be the origin-form of the composed URI,
* unless a value was provided to the concrete implementation (see
* withRequestTarget() below).
*
* If no URI is available, and no request-target has been specifically
* provided, this method MUST return the string "/".
*
* @return string
*/
public function getRequestTarget();
/**
* Return an instance with the specific request-target.
*
* If the request needs a non-origin-form request-target e.g., for
* specifying an absolute-form, authority-form, or asterisk-form
* this method may be used to create an instance with the specified
* request-target, verbatim.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* changed request target.
*
* @link http://tools.ietf.org/html/rfc7230#section-5.3 (for the various
* request-target forms allowed in request messages)
* @param string $requestTarget
* @return static
*/
public function withRequestTarget(string $requestTarget);
/**
* Retrieves the HTTP method of the request.
*
* @return string Returns the request method.
*/
public function getMethod();
/**
* Return an instance with the provided HTTP method.
*
* While HTTP method names are typically all uppercase characters, HTTP
* method names are case-sensitive and thus implementations SHOULD NOT
* modify the given string.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* changed request method.
*
* @param string $method Case-sensitive method.
* @return static
* @throws \InvalidArgumentException for invalid HTTP methods.
*/
public function withMethod(string $method);
/**
* Retrieves the URI instance.
*
* This method MUST return a UriInterface instance.
*
* @link http://tools.ietf.org/html/rfc3986#section-4.3
* @return UriInterface Returns a UriInterface instance
* representing the URI of the request.
*/
public function getUri();
/**
* Returns an instance with the provided URI.
*
* This method MUST update the Host header of the returned request by
* default if the URI contains a host component. If the URI does not
* contain a host component, any pre-existing Host header MUST be carried
* over to the returned request.
*
* You can opt-in to preserving the original state of the Host header by
* setting `$preserveHost` to `true`. When `$preserveHost` is set to
* `true`, this method interacts with the Host header in the following ways:
*
* - If the Host header is missing or empty, and the new URI contains
* a host component, this method MUST update the Host header in the returned
* request.
* - If the Host header is missing or empty, and the new URI does not contain a
* host component, this method MUST NOT update the Host header in the returned
* request.
* - If a Host header is present and non-empty, this method MUST NOT update
* the Host header in the returned request.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new UriInterface instance.
*
* @link http://tools.ietf.org/html/rfc3986#section-4.3
* @param UriInterface $uri New request URI to use.
* @param bool $preserveHost Preserve the original state of the Host header.
* @return static
*/
public function withUri(UriInterface $uri, bool $preserveHost = false);
}

View File

@ -1,70 +0,0 @@
<?php
declare(strict_types=1);
namespace Psr\Http\Message;
/**
* Representation of an outgoing, server-side response.
*
* Per the HTTP specification, this interface includes properties for
* each of the following:
*
* - Protocol version
* - Status code and reason phrase
* - Headers
* - Message body
*
* Responses are considered immutable; all methods that might change state MUST
* be implemented such that they retain the internal state of the current
* message and return an instance that contains the changed state.
*/
interface ResponseInterface extends MessageInterface
{
/**
* Gets the response status code.
*
* The status code is a 3-digit integer result code of the server's attempt
* to understand and satisfy the request.
*
* @return int Status code.
*/
public function getStatusCode();
/**
* Return an instance with the specified status code and, optionally, reason phrase.
*
* If no reason phrase is specified, implementations MAY choose to default
* to the RFC 7231 or IANA recommended reason phrase for the response's
* status code.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* updated status and reason phrase.
*
* @link http://tools.ietf.org/html/rfc7231#section-6
* @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
* @param int $code The 3-digit integer result code to set.
* @param string $reasonPhrase The reason phrase to use with the
* provided status code; if none is provided, implementations MAY
* use the defaults as suggested in the HTTP specification.
* @return static
* @throws \InvalidArgumentException For invalid status code arguments.
*/
public function withStatus(int $code, string $reasonPhrase = '');
/**
* Gets the response reason phrase associated with the status code.
*
* Because a reason phrase is not a required element in a response
* status line, the reason phrase value MAY be null. Implementations MAY
* choose to return the default RFC 7231 recommended reason phrase (or those
* listed in the IANA HTTP Status Code Registry) for the response's
* status code.
*
* @link http://tools.ietf.org/html/rfc7231#section-6
* @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
* @return string Reason phrase; must return an empty string if none present.
*/
public function getReasonPhrase();
}

View File

@ -1,263 +0,0 @@
<?php
declare(strict_types=1);
namespace Psr\Http\Message;
/**
* Representation of an incoming, server-side HTTP request.
*
* Per the HTTP specification, this interface includes properties for
* each of the following:
*
* - Protocol version
* - HTTP method
* - URI
* - Headers
* - Message body
*
* Additionally, it encapsulates all data as it has arrived to the
* application from the CGI and/or PHP environment, including:
*
* - The values represented in $_SERVER.
* - Any cookies provided (generally via $_COOKIE)
* - Query string arguments (generally via $_GET, or as parsed via parse_str())
* - Upload files, if any (as represented by $_FILES)
* - Deserialized body parameters (generally from $_POST)
*
* $_SERVER values MUST be treated as immutable, as they represent application
* state at the time of request; as such, no methods are provided to allow
* modification of those values. The other values provide such methods, as they
* can be restored from $_SERVER or the request body, and may need treatment
* during the application (e.g., body parameters may be deserialized based on
* content type).
*
* Additionally, this interface recognizes the utility of introspecting a
* request to derive and match additional parameters (e.g., via URI path
* matching, decrypting cookie values, deserializing non-form-encoded body
* content, matching authorization headers to users, etc). These parameters
* are stored in an "attributes" property.
*
* Requests are considered immutable; all methods that might change state MUST
* be implemented such that they retain the internal state of the current
* message and return an instance that contains the changed state.
*/
interface ServerRequestInterface extends RequestInterface
{
/**
* Retrieve server parameters.
*
* Retrieves data related to the incoming request environment,
* typically derived from PHP's $_SERVER superglobal. The data IS NOT
* REQUIRED to originate from $_SERVER.
*
* @return array
*/
public function getServerParams();
/**
* Retrieve cookies.
*
* Retrieves cookies sent by the client to the server.
*
* The data MUST be compatible with the structure of the $_COOKIE
* superglobal.
*
* @return array
*/
public function getCookieParams();
/**
* Return an instance with the specified cookies.
*
* The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST
* be compatible with the structure of $_COOKIE. Typically, this data will
* be injected at instantiation.
*
* This method MUST NOT update the related Cookie header of the request
* instance, nor related values in the server params.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* updated cookie values.
*
* @param array $cookies Array of key/value pairs representing cookies.
* @return static
*/
public function withCookieParams(array $cookies);
/**
* Retrieve query string arguments.
*
* Retrieves the deserialized query string arguments, if any.
*
* Note: the query params might not be in sync with the URI or server
* params. If you need to ensure you are only getting the original
* values, you may need to parse the query string from `getUri()->getQuery()`
* or from the `QUERY_STRING` server param.
*
* @return array
*/
public function getQueryParams();
/**
* Return an instance with the specified query string arguments.
*
* These values SHOULD remain immutable over the course of the incoming
* request. They MAY be injected during instantiation, such as from PHP's
* $_GET superglobal, or MAY be derived from some other value such as the
* URI. In cases where the arguments are parsed from the URI, the data
* MUST be compatible with what PHP's parse_str() would return for
* purposes of how duplicate query parameters are handled, and how nested
* sets are handled.
*
* Setting query string arguments MUST NOT change the URI stored by the
* request, nor the values in the server params.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* updated query string arguments.
*
* @param array $query Array of query string arguments, typically from
* $_GET.
* @return static
*/
public function withQueryParams(array $query);
/**
* Retrieve normalized file upload data.
*
* This method returns upload metadata in a normalized tree, with each leaf
* an instance of Psr\Http\Message\UploadedFileInterface.
*
* These values MAY be prepared from $_FILES or the message body during
* instantiation, or MAY be injected via withUploadedFiles().
*
* @return array An array tree of UploadedFileInterface instances; an empty
* array MUST be returned if no data is present.
*/
public function getUploadedFiles();
/**
* Create a new instance with the specified uploaded files.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* updated body parameters.
*
* @param array $uploadedFiles An array tree of UploadedFileInterface instances.
* @return static
* @throws \InvalidArgumentException if an invalid structure is provided.
*/
public function withUploadedFiles(array $uploadedFiles);
/**
* Retrieve any parameters provided in the request body.
*
* If the request Content-Type is either application/x-www-form-urlencoded
* or multipart/form-data, and the request method is POST, this method MUST
* return the contents of $_POST.
*
* Otherwise, this method may return any results of deserializing
* the request body content; as parsing returns structured content, the
* potential types MUST be arrays or objects only. A null value indicates
* the absence of body content.
*
* @return null|array|object The deserialized body parameters, if any.
* These will typically be an array or object.
*/
public function getParsedBody();
/**
* Return an instance with the specified body parameters.
*
* These MAY be injected during instantiation.
*
* If the request Content-Type is either application/x-www-form-urlencoded
* or multipart/form-data, and the request method is POST, use this method
* ONLY to inject the contents of $_POST.
*
* The data IS NOT REQUIRED to come from $_POST, but MUST be the results of
* deserializing the request body content. Deserialization/parsing returns
* structured data, and, as such, this method ONLY accepts arrays or objects,
* or a null value if nothing was available to parse.
*
* As an example, if content negotiation determines that the request data
* is a JSON payload, this method could be used to create a request
* instance with the deserialized parameters.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* updated body parameters.
*
* @param null|array|object $data The deserialized body data. This will
* typically be in an array or object.
* @return static
* @throws \InvalidArgumentException if an unsupported argument type is
* provided.
*/
public function withParsedBody($data);
/**
* Retrieve attributes derived from the request.
*
* The request "attributes" may be used to allow injection of any
* parameters derived from the request: e.g., the results of path
* match operations; the results of decrypting cookies; the results of
* deserializing non-form-encoded message bodies; etc. Attributes
* will be application and request specific, and CAN be mutable.
*
* @return array Attributes derived from the request.
*/
public function getAttributes();
/**
* Retrieve a single derived request attribute.
*
* Retrieves a single derived request attribute as described in
* getAttributes(). If the attribute has not been previously set, returns
* the default value as provided.
*
* This method obviates the need for a hasAttribute() method, as it allows
* specifying a default value to return if the attribute is not found.
*
* @see getAttributes()
* @param string $name The attribute name.
* @param mixed $default Default value to return if the attribute does not exist.
* @return mixed
*/
public function getAttribute(string $name, $default = null);
/**
* Return an instance with the specified derived request attribute.
*
* This method allows setting a single derived request attribute as
* described in getAttributes().
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* updated attribute.
*
* @see getAttributes()
* @param string $name The attribute name.
* @param mixed $value The value of the attribute.
* @return static
*/
public function withAttribute(string $name, $value);
/**
* Return an instance that removes the specified derived request attribute.
*
* This method allows removing a single derived request attribute as
* described in getAttributes().
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that removes
* the attribute.
*
* @see getAttributes()
* @param string $name The attribute name.
* @return static
*/
public function withoutAttribute(string $name);
}

View File

@ -1,160 +0,0 @@
<?php
declare(strict_types=1);
namespace Psr\Http\Message;
/**
* Describes a data stream.
*
* Typically, an instance will wrap a PHP stream; this interface provides
* a wrapper around the most common operations, including serialization of
* the entire stream to a string.
*/
interface StreamInterface
{
/**
* Reads all data from the stream into a string, from the beginning to end.
*
* This method MUST attempt to seek to the beginning of the stream before
* reading data and read the stream until the end is reached.
*
* Warning: This could attempt to load a large amount of data into memory.
*
* This method MUST NOT raise an exception in order to conform with PHP's
* string casting operations.
*
* @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
* @return string
*/
public function __toString();
/**
* Closes the stream and any underlying resources.
*
* @return void
*/
public function close();
/**
* Separates any underlying resources from the stream.
*
* After the stream has been detached, the stream is in an unusable state.
*
* @return resource|null Underlying PHP stream, if any
*/
public function detach();
/**
* Get the size of the stream if known.
*
* @return int|null Returns the size in bytes if known, or null if unknown.
*/
public function getSize();
/**
* Returns the current position of the file read/write pointer
*
* @return int Position of the file pointer
* @throws \RuntimeException on error.
*/
public function tell();
/**
* Returns true if the stream is at the end of the stream.
*
* @return bool
*/
public function eof();
/**
* Returns whether or not the stream is seekable.
*
* @return bool
*/
public function isSeekable();
/**
* Seek to a position in the stream.
*
* @link http://www.php.net/manual/en/function.fseek.php
* @param int $offset Stream offset
* @param int $whence Specifies how the cursor position will be calculated
* based on the seek offset. Valid values are identical to the built-in
* PHP $whence values for `fseek()`. SEEK_SET: Set position equal to
* offset bytes SEEK_CUR: Set position to current location plus offset
* SEEK_END: Set position to end-of-stream plus offset.
* @throws \RuntimeException on failure.
*/
public function seek(int $offset, int $whence = SEEK_SET);
/**
* Seek to the beginning of the stream.
*
* If the stream is not seekable, this method will raise an exception;
* otherwise, it will perform a seek(0).
*
* @see seek()
* @link http://www.php.net/manual/en/function.fseek.php
* @throws \RuntimeException on failure.
*/
public function rewind();
/**
* Returns whether or not the stream is writable.
*
* @return bool
*/
public function isWritable();
/**
* Write data to the stream.
*
* @param string $string The string that is to be written.
* @return int Returns the number of bytes written to the stream.
* @throws \RuntimeException on failure.
*/
public function write(string $string);
/**
* Returns whether or not the stream is readable.
*
* @return bool
*/
public function isReadable();
/**
* Read data from the stream.
*
* @param int $length Read up to $length bytes from the object and return
* them. Fewer than $length bytes may be returned if underlying stream
* call returns fewer bytes.
* @return string Returns the data read from the stream, or an empty string
* if no bytes are available.
* @throws \RuntimeException if an error occurs.
*/
public function read(int $length);
/**
* Returns the remaining contents in a string
*
* @return string
* @throws \RuntimeException if unable to read or an error occurs while
* reading.
*/
public function getContents();
/**
* Get stream metadata as an associative array or retrieve a specific key.
*
* The keys returned are identical to the keys returned from PHP's
* stream_get_meta_data() function.
*
* @link http://php.net/manual/en/function.stream-get-meta-data.php
* @param string|null $key Specific metadata to retrieve.
* @return array|mixed|null Returns an associative array if no key is
* provided. Returns a specific key value if a key is provided and the
* value is found, or null if the key is not found.
*/
public function getMetadata(?string $key = null);
}

View File

@ -1,125 +0,0 @@
<?php
declare(strict_types=1);
namespace Psr\Http\Message;
/**
* Value object representing a file uploaded through an HTTP request.
*
* Instances of this interface are considered immutable; all methods that
* might change state MUST be implemented such that they retain the internal
* state of the current instance and return an instance that contains the
* changed state.
*/
interface UploadedFileInterface
{
/**
* Retrieve a stream representing the uploaded file.
*
* This method MUST return a StreamInterface instance, representing the
* uploaded file. The purpose of this method is to allow utilizing native PHP
* stream functionality to manipulate the file upload, such as
* stream_copy_to_stream() (though the result will need to be decorated in a
* native PHP stream wrapper to work with such functions).
*
* If the moveTo() method has been called previously, this method MUST raise
* an exception.
*
* @return StreamInterface Stream representation of the uploaded file.
* @throws \RuntimeException in cases when no stream is available or can be
* created.
*/
public function getStream();
/**
* Move the uploaded file to a new location.
*
* Use this method as an alternative to move_uploaded_file(). This method is
* guaranteed to work in both SAPI and non-SAPI environments.
* Implementations must determine which environment they are in, and use the
* appropriate method (move_uploaded_file(), rename(), or a stream
* operation) to perform the operation.
*
* $targetPath may be an absolute path, or a relative path. If it is a
* relative path, resolution should be the same as used by PHP's rename()
* function.
*
* The original file or stream MUST be removed on completion.
*
* If this method is called more than once, any subsequent calls MUST raise
* an exception.
*
* When used in an SAPI environment where $_FILES is populated, when writing
* files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be
* used to ensure permissions and upload status are verified correctly.
*
* If you wish to move to a stream, use getStream(), as SAPI operations
* cannot guarantee writing to stream destinations.
*
* @see http://php.net/is_uploaded_file
* @see http://php.net/move_uploaded_file
* @param string $targetPath Path to which to move the uploaded file.
* @throws \InvalidArgumentException if the $targetPath specified is invalid.
* @throws \RuntimeException on any error during the move operation, or on
* the second or subsequent call to the method.
*/
public function moveTo(string $targetPath);
/**
* Retrieve the file size.
*
* Implementations SHOULD return the value stored in the "size" key of
* the file in the $_FILES array if available, as PHP calculates this based
* on the actual size transmitted.
*
* @return int|null The file size in bytes or null if unknown.
*/
public function getSize();
/**
* Retrieve the error associated with the uploaded file.
*
* The return value MUST be one of PHP's UPLOAD_ERR_XXX constants.
*
* If the file was uploaded successfully, this method MUST return
* UPLOAD_ERR_OK.
*
* Implementations SHOULD return the value stored in the "error" key of
* the file in the $_FILES array.
*
* @see http://php.net/manual/en/features.file-upload.errors.php
* @return int One of PHP's UPLOAD_ERR_XXX constants.
*/
public function getError();
/**
* Retrieve the filename sent by the client.
*
* Do not trust the value returned by this method. A client could send
* a malicious filename with the intention to corrupt or hack your
* application.
*
* Implementations SHOULD return the value stored in the "name" key of
* the file in the $_FILES array.
*
* @return string|null The filename sent by the client or null if none
* was provided.
*/
public function getClientFilename();
/**
* Retrieve the media type sent by the client.
*
* Do not trust the value returned by this method. A client could send
* a malicious media type with the intention to corrupt or hack your
* application.
*
* Implementations SHOULD return the value stored in the "type" key of
* the file in the $_FILES array.
*
* @return string|null The media type sent by the client or null if none
* was provided.
*/
public function getClientMediaType();
}

View File

@ -1,326 +0,0 @@
<?php
declare(strict_types=1);
namespace Psr\Http\Message;
/**
* Value object representing a URI.
*
* This interface is meant to represent URIs according to RFC 3986 and to
* provide methods for most common operations. Additional functionality for
* working with URIs can be provided on top of the interface or externally.
* Its primary use is for HTTP requests, but may also be used in other
* contexts.
*
* Instances of this interface are considered immutable; all methods that
* might change state MUST be implemented such that they retain the internal
* state of the current instance and return an instance that contains the
* changed state.
*
* Typically the Host header will be also be present in the request message.
* For server-side requests, the scheme will typically be discoverable in the
* server parameters.
*
* @link http://tools.ietf.org/html/rfc3986 (the URI specification)
*/
interface UriInterface
{
/**
* Retrieve the scheme component of the URI.
*
* If no scheme is present, this method MUST return an empty string.
*
* The value returned MUST be normalized to lowercase, per RFC 3986
* Section 3.1.
*
* The trailing ":" character is not part of the scheme and MUST NOT be
* added.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.1
* @return string The URI scheme.
*/
public function getScheme();
/**
* Retrieve the authority component of the URI.
*
* If no authority information is present, this method MUST return an empty
* string.
*
* The authority syntax of the URI is:
*
* <pre>
* [user-info@]host[:port]
* </pre>
*
* If the port component is not set or is the standard port for the current
* scheme, it SHOULD NOT be included.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.2
* @return string The URI authority, in "[user-info@]host[:port]" format.
*/
public function getAuthority();
/**
* Retrieve the user information component of the URI.
*
* If no user information is present, this method MUST return an empty
* string.
*
* If a user is present in the URI, this will return that value;
* additionally, if the password is also present, it will be appended to the
* user value, with a colon (":") separating the values.
*
* The trailing "@" character is not part of the user information and MUST
* NOT be added.
*
* @return string The URI user information, in "username[:password]" format.
*/
public function getUserInfo();
/**
* Retrieve the host component of the URI.
*
* If no host is present, this method MUST return an empty string.
*
* The value returned MUST be normalized to lowercase, per RFC 3986
* Section 3.2.2.
*
* @see http://tools.ietf.org/html/rfc3986#section-3.2.2
* @return string The URI host.
*/
public function getHost();
/**
* Retrieve the port component of the URI.
*
* If a port is present, and it is non-standard for the current scheme,
* this method MUST return it as an integer. If the port is the standard port
* used with the current scheme, this method SHOULD return null.
*
* If no port is present, and no scheme is present, this method MUST return
* a null value.
*
* If no port is present, but a scheme is present, this method MAY return
* the standard port for that scheme, but SHOULD return null.
*
* @return null|int The URI port.
*/
public function getPort();
/**
* Retrieve the path component of the URI.
*
* The path can either be empty or absolute (starting with a slash) or
* rootless (not starting with a slash). Implementations MUST support all
* three syntaxes.
*
* Normally, the empty path "" and absolute path "/" are considered equal as
* defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
* do this normalization because in contexts with a trimmed base path, e.g.
* the front controller, this difference becomes significant. It's the task
* of the user to handle both "" and "/".
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986, Sections 2 and 3.3.
*
* As an example, if the value should include a slash ("/") not intended as
* delimiter between path segments, that value MUST be passed in encoded
* form (e.g., "%2F") to the instance.
*
* @see https://tools.ietf.org/html/rfc3986#section-2
* @see https://tools.ietf.org/html/rfc3986#section-3.3
* @return string The URI path.
*/
public function getPath();
/**
* Retrieve the query string of the URI.
*
* If no query string is present, this method MUST return an empty string.
*
* The leading "?" character is not part of the query and MUST NOT be
* added.
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986, Sections 2 and 3.4.
*
* As an example, if a value in a key/value pair of the query string should
* include an ampersand ("&") not intended as a delimiter between values,
* that value MUST be passed in encoded form (e.g., "%26") to the instance.
*
* @see https://tools.ietf.org/html/rfc3986#section-2
* @see https://tools.ietf.org/html/rfc3986#section-3.4
* @return string The URI query string.
*/
public function getQuery();
/**
* Retrieve the fragment component of the URI.
*
* If no fragment is present, this method MUST return an empty string.
*
* The leading "#" character is not part of the fragment and MUST NOT be
* added.
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986, Sections 2 and 3.5.
*
* @see https://tools.ietf.org/html/rfc3986#section-2
* @see https://tools.ietf.org/html/rfc3986#section-3.5
* @return string The URI fragment.
*/
public function getFragment();
/**
* Return an instance with the specified scheme.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified scheme.
*
* Implementations MUST support the schemes "http" and "https" case
* insensitively, and MAY accommodate other schemes if required.
*
* An empty scheme is equivalent to removing the scheme.
*
* @param string $scheme The scheme to use with the new instance.
* @return static A new instance with the specified scheme.
* @throws \InvalidArgumentException for invalid or unsupported schemes.
*/
public function withScheme(string $scheme);
/**
* Return an instance with the specified user information.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified user information.
*
* Password is optional, but the user information MUST include the
* user; an empty string for the user is equivalent to removing user
* information.
*
* @param string $user The user name to use for authority.
* @param null|string $password The password associated with $user.
* @return static A new instance with the specified user information.
*/
public function withUserInfo(string $user, ?string $password = null);
/**
* Return an instance with the specified host.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified host.
*
* An empty host value is equivalent to removing the host.
*
* @param string $host The hostname to use with the new instance.
* @return static A new instance with the specified host.
* @throws \InvalidArgumentException for invalid hostnames.
*/
public function withHost(string $host);
/**
* Return an instance with the specified port.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified port.
*
* Implementations MUST raise an exception for ports outside the
* established TCP and UDP port ranges.
*
* A null value provided for the port is equivalent to removing the port
* information.
*
* @param null|int $port The port to use with the new instance; a null value
* removes the port information.
* @return static A new instance with the specified port.
* @throws \InvalidArgumentException for invalid ports.
*/
public function withPort(?int $port);
/**
* Return an instance with the specified path.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified path.
*
* The path can either be empty or absolute (starting with a slash) or
* rootless (not starting with a slash). Implementations MUST support all
* three syntaxes.
*
* If the path is intended to be domain-relative rather than path relative then
* it must begin with a slash ("/"). Paths not starting with a slash ("/")
* are assumed to be relative to some base path known to the application or
* consumer.
*
* Users can provide both encoded and decoded path characters.
* Implementations ensure the correct encoding as outlined in getPath().
*
* @param string $path The path to use with the new instance.
* @return static A new instance with the specified path.
* @throws \InvalidArgumentException for invalid paths.
*/
public function withPath(string $path);
/**
* Return an instance with the specified query string.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified query string.
*
* Users can provide both encoded and decoded query characters.
* Implementations ensure the correct encoding as outlined in getQuery().
*
* An empty query string value is equivalent to removing the query string.
*
* @param string $query The query string to use with the new instance.
* @return static A new instance with the specified query string.
* @throws \InvalidArgumentException for invalid query strings.
*/
public function withQuery(string $query);
/**
* Return an instance with the specified URI fragment.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified URI fragment.
*
* Users can provide both encoded and decoded fragment characters.
* Implementations ensure the correct encoding as outlined in getFragment().
*
* An empty fragment value is equivalent to removing the fragment.
*
* @param string $fragment The fragment to use with the new instance.
* @return static A new instance with the specified fragment.
*/
public function withFragment(string $fragment);
/**
* Return the string representation as a URI reference.
*
* Depending on which components of the URI are present, the resulting
* string is either a full URI or relative reference according to RFC 3986,
* Section 4.1. The method concatenates the various components of the URI,
* using the appropriate delimiters:
*
* - If a scheme is present, it MUST be suffixed by ":".
* - If an authority is present, it MUST be prefixed by "//".
* - The path can be concatenated without delimiters. But there are two
* cases where the path has to be adjusted to make the URI reference
* valid as PHP does not allow to throw an exception in __toString():
* - If the path is rootless and an authority is present, the path MUST
* be prefixed by "/".
* - If the path is starting with more than one "/" and no authority is
* present, the starting slashes MUST be reduced to one.
* - If a query is present, it MUST be prefixed by "?".
* - If a fragment is present, it MUST be prefixed by "#".
*
* @see http://tools.ietf.org/html/rfc3986#section-4.1
* @return string
*/
public function __toString();
}

View File

@ -1,19 +0,0 @@
Copyright (c) 2012 PHP Framework Interoperability Group
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,58 +0,0 @@
PSR Log
=======
This repository holds all interfaces/classes/traits related to
[PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md).
Note that this is not a logger of its own. It is merely an interface that
describes a logger. See the specification for more details.
Installation
------------
```bash
composer require psr/log
```
Usage
-----
If you need a logger, you can use the interface like this:
```php
<?php
use Psr\Log\LoggerInterface;
class Foo
{
private $logger;
public function __construct(LoggerInterface $logger = null)
{
$this->logger = $logger;
}
public function doSomething()
{
if ($this->logger) {
$this->logger->info('Doing work');
}
try {
$this->doSomethingElse();
} catch (Exception $exception) {
$this->logger->error('Oh no!', array('exception' => $exception));
}
// do something useful
}
}
```
You can then pick one of the implementations of the interface to get a logger.
If you want to implement the interface, you can require this package and
implement `Psr\Log\LoggerInterface` in your code. Please read the
[specification text](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md)
for details.

View File

@ -1,26 +0,0 @@
{
"name": "psr/log",
"description": "Common interface for logging libraries",
"keywords": ["psr", "psr-3", "log"],
"homepage": "https://github.com/php-fig/log",
"license": "MIT",
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"require": {
"php": ">=8.0.0"
},
"autoload": {
"psr-4": {
"Psr\\Log\\": "src"
}
},
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace Psr\Log;
/**
* This is a simple Logger implementation that other Loggers can inherit from.
*
* It simply delegates all log-level-specific methods to the `log` method to
* reduce boilerplate code that a simple Logger that does the same thing with
* messages regardless of the error level has to implement.
*/
abstract class AbstractLogger implements LoggerInterface
{
use LoggerTrait;
}

View File

@ -1,7 +0,0 @@
<?php
namespace Psr\Log;
class InvalidArgumentException extends \InvalidArgumentException
{
}

View File

@ -1,18 +0,0 @@
<?php
namespace Psr\Log;
/**
* Describes log levels.
*/
class LogLevel
{
const EMERGENCY = 'emergency';
const ALERT = 'alert';
const CRITICAL = 'critical';
const ERROR = 'error';
const WARNING = 'warning';
const NOTICE = 'notice';
const INFO = 'info';
const DEBUG = 'debug';
}

View File

@ -1,14 +0,0 @@
<?php
namespace Psr\Log;
/**
* Describes a logger-aware instance.
*/
interface LoggerAwareInterface
{
/**
* Sets a logger instance on the object.
*/
public function setLogger(LoggerInterface $logger): void;
}

View File

@ -1,22 +0,0 @@
<?php
namespace Psr\Log;
/**
* Basic Implementation of LoggerAwareInterface.
*/
trait LoggerAwareTrait
{
/**
* The logger instance.
*/
protected ?LoggerInterface $logger = null;
/**
* Sets a logger.
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
}

View File

@ -1,98 +0,0 @@
<?php
namespace Psr\Log;
/**
* Describes a logger instance.
*
* The message MUST be a string or object implementing __toString().
*
* The message MAY contain placeholders in the form: {foo} where foo
* will be replaced by the context data in key "foo".
*
* The context array can contain arbitrary data. The only assumption that
* can be made by implementors is that if an Exception instance is given
* to produce a stack trace, it MUST be in a key named "exception".
*
* See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
* for the full interface specification.
*/
interface LoggerInterface
{
/**
* System is unusable.
*
* @param mixed[] $context
*/
public function emergency(string|\Stringable $message, array $context = []): void;
/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
* @param mixed[] $context
*/
public function alert(string|\Stringable $message, array $context = []): void;
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param mixed[] $context
*/
public function critical(string|\Stringable $message, array $context = []): void;
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param mixed[] $context
*/
public function error(string|\Stringable $message, array $context = []): void;
/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
* @param mixed[] $context
*/
public function warning(string|\Stringable $message, array $context = []): void;
/**
* Normal but significant events.
*
* @param mixed[] $context
*/
public function notice(string|\Stringable $message, array $context = []): void;
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param mixed[] $context
*/
public function info(string|\Stringable $message, array $context = []): void;
/**
* Detailed debug information.
*
* @param mixed[] $context
*/
public function debug(string|\Stringable $message, array $context = []): void;
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param mixed[] $context
*
* @throws \Psr\Log\InvalidArgumentException
*/
public function log($level, string|\Stringable $message, array $context = []): void;
}

View File

@ -1,98 +0,0 @@
<?php
namespace Psr\Log;
/**
* This is a simple Logger trait that classes unable to extend AbstractLogger
* (because they extend another class, etc) can include.
*
* It simply delegates all log-level-specific methods to the `log` method to
* reduce boilerplate code that a simple Logger that does the same thing with
* messages regardless of the error level has to implement.
*/
trait LoggerTrait
{
/**
* System is unusable.
*/
public function emergency(string|\Stringable $message, array $context = []): void
{
$this->log(LogLevel::EMERGENCY, $message, $context);
}
/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*/
public function alert(string|\Stringable $message, array $context = []): void
{
$this->log(LogLevel::ALERT, $message, $context);
}
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*/
public function critical(string|\Stringable $message, array $context = []): void
{
$this->log(LogLevel::CRITICAL, $message, $context);
}
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*/
public function error(string|\Stringable $message, array $context = []): void
{
$this->log(LogLevel::ERROR, $message, $context);
}
/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*/
public function warning(string|\Stringable $message, array $context = []): void
{
$this->log(LogLevel::WARNING, $message, $context);
}
/**
* Normal but significant events.
*/
public function notice(string|\Stringable $message, array $context = []): void
{
$this->log(LogLevel::NOTICE, $message, $context);
}
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*/
public function info(string|\Stringable $message, array $context = []): void
{
$this->log(LogLevel::INFO, $message, $context);
}
/**
* Detailed debug information.
*/
public function debug(string|\Stringable $message, array $context = []): void
{
$this->log(LogLevel::DEBUG, $message, $context);
}
/**
* Logs with an arbitrary level.
*
* @param mixed $level
*
* @throws \Psr\Log\InvalidArgumentException
*/
abstract public function log($level, string|\Stringable $message, array $context = []): void;
}

View File

@ -1,26 +0,0 @@
<?php
namespace Psr\Log;
/**
* This Logger can be used to avoid conditional log calls.
*
* Logging should always be optional, and if no logger is provided to your
* library creating a NullLogger instance to have something to throw logs at
* is a good way to avoid littering your code with `if ($this->logger) { }`
* blocks.
*/
class NullLogger extends AbstractLogger
{
/**
* Logs with an arbitrary level.
*
* @param mixed[] $context
*
* @throws \Psr\Log\InvalidArgumentException
*/
public function log($level, string|\Stringable $message, array $context = []): void
{
// noop
}
}

View File

@ -1,21 +0,0 @@
---
name: Bug report
about: Use this if you believe there is a bug in this repo
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
Please provide a clear and concise description of the suspected issue.
**How to reproduce**
If possible, provide information - possibly including code snippets - on how to reproduce the issue.
**Logs**
If possible, provide logs that indicate the issue. See https://github.com/Textalk/websocket-php/blob/master/docs/Examples.md#echo-logger on how to use the EchoLog.
**Versions**
* Version of this library
* PHP version

View File

@ -1,14 +0,0 @@
---
name: Feature request
about: Suggest an idea for this library
title: ''
labels: feature request
assignees: ''
---
**Is it within the scope of this library?**
Consider and describe why the feature would be beneficial in this library, and not implemented as a separate project using this as a dependency.
**Describe the solution you'd like**
A clear and concise description of what you want to happen.

View File

@ -1,10 +0,0 @@
---
name: Other issue
about: Use this for other issues
title: ''
labels: ''
assignees: ''
---
**Describe your issue**

View File

@ -1,97 +0,0 @@
name: Acceptance
on: [push, pull_request]
jobs:
test-7-4:
runs-on: ubuntu-latest
name: Test PHP 7.4
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up PHP 7.4
uses: shivammathur/setup-php@v2
with:
php-version: '7.4'
- name: Composer
run: make install
- name: Test
run: make test
test-8-0:
runs-on: ubuntu-latest
name: Test PHP 8.0
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up PHP 8.0
uses: shivammathur/setup-php@v2
with:
php-version: '8.0'
- name: Composer
run: make install
- name: Test
run: make test
test-8-1:
runs-on: ubuntu-latest
name: Test PHP 8.1
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up PHP 8.1
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
- name: Composer
run: make install
- name: Test
run: make test
test-8-2:
runs-on: ubuntu-latest
name: Test PHP 8.2
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up PHP 8.2
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Composer
run: make install
- name: Test
run: make test
cs-check:
runs-on: ubuntu-latest
name: Code standard
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up PHP 8.0
uses: shivammathur/setup-php@v2
with:
php-version: '8.0'
- name: Composer
run: make install
- name: Code standard
run: make cs-check
coverage:
runs-on: ubuntu-latest
name: Code coverage
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up PHP 8.0
uses: shivammathur/setup-php@v2
with:
php-version: '8.0'
extensions: xdebug
- name: Composer
run: make install
- name: Code coverage
env:
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: make coverage

View File

@ -1,6 +0,0 @@
.DS_Store
.phpunit.result.cache
build/
composer.lock
composer.phar
vendor/

View File

@ -1,16 +0,0 @@
# Websocket: License
Websocket PHP is free software released under the following license:
[ISC License](http://en.wikipedia.org/wiki/ISC_license)
Permission to use, copy, modify, and/or distribute this software for any purpose with or without
fee is hereby granted, provided that the above copyright notice and this permission notice appear
in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.

View File

@ -1,32 +0,0 @@
install: composer.phar
./composer.phar install
update: composer.phar
./composer.phar self-update
./composer.phar update
test: composer.lock
./vendor/bin/phpunit
cs-check: composer.lock
./vendor/bin/phpcs --standard=PSR1,PSR12 --encoding=UTF-8 --report=full --colors lib tests examples
coverage: composer.lock build
XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml
./vendor/bin/php-coveralls -v
composer.phar:
curl -s http://getcomposer.org/installer | php
composer.lock: composer.phar
./composer.phar --no-interaction install
vendor/bin/phpunit: install
build:
mkdir build
clean:
rm composer.phar
rm -r vendor
rm -r build

View File

@ -1,76 +0,0 @@
# Websocket Client and Server for PHP
[![Build Status](https://github.com/Textalk/websocket-php/actions/workflows/acceptance.yml/badge.svg)](https://github.com/Textalk/websocket-php/actions)
[![Coverage Status](https://coveralls.io/repos/github/Textalk/websocket-php/badge.svg?branch=master)](https://coveralls.io/github/Textalk/websocket-php)
## Archived project
This project has been archived and is no longer maintained. No bug fix and no additional features will be added.<br>
You won't be able to submit new issues or pull requests, and no additional features will be added
This library has been replaced by [sirn-se/websocket-php](https://github.com/sirn-se/websocket-php)
## Websocket Client and Server for PHP
This library contains WebSocket client and server for PHP.
The client and server provides methods for reading and writing to WebSocket streams.
It does not include convenience operations such as listeners and implicit error handling.
## Documentation
- [Client](docs/Client.md)
- [Server](docs/Server.md)
- [Examples](docs/Examples.md)
- [Changelog](docs/Changelog.md)
- [Contributing](docs/Contributing.md)
## Installing
Preferred way to install is with [Composer](https://getcomposer.org/).
```
composer require textalk/websocket
```
* Current version support PHP versions `^7.4|^8.0`.
* For PHP `7.2` and `7.3` support use version [`1.5`](https://github.com/Textalk/websocket-php/tree/1.5.0).
* For PHP `7.1` support use version [`1.4`](https://github.com/Textalk/websocket-php/tree/1.4.0).
* For PHP `^5.4` and `7.0` support use version [`1.3`](https://github.com/Textalk/websocket-php/tree/1.3.0).
## Client
The [client](docs/Client.md) can read and write on a WebSocket stream.
It internally supports Upgrade handshake and implicit close and ping/pong operations.
```php
$client = new WebSocket\Client("ws://echo.websocket.org/");
$client->text("Hello WebSocket.org!");
echo $client->receive();
$client->close();
```
## Server
The library contains a rudimentary single stream/single thread [server](docs/Server.md).
It internally supports Upgrade handshake and implicit close and ping/pong operations.
Note that it does **not** support threading or automatic association ot continuous client requests.
If you require this kind of server behavior, you need to build it on top of provided server implementation.
```php
$server = new WebSocket\Server();
$server->accept();
$message = $server->receive();
$server->text($message);
$server->close();
```
### License and Contributors
[ISC License](COPYING.md)
Fredrik Liljegren, Armen Baghumian Sankbarani, Ruslan Bekenev,
Joshua Thijssen, Simon Lipp, Quentin Bellus, Patrick McCarren, swmcdonnell,
Ignas Bernotas, Mark Herhold, Andreas Palm, Sören Jensen, pmaasz, Alexey Stavrov,
Michael Slezak, Pierre Seznec, rmeisler, Nickolay V. Shmyrev, Christoph Kempen,
Marc Roberts, Antonio Mora, Simon Podlipsky, etrinh.

View File

@ -1,36 +0,0 @@
{
"name": "textalk/websocket",
"description": "WebSocket client and server",
"license": "ISC",
"type": "library",
"authors": [
{
"name": "Fredrik Liljegren"
},
{
"name": "Sören Jensen"
}
],
"autoload": {
"psr-4": {
"WebSocket\\": "lib"
}
},
"autoload-dev": {
"psr-4": {
"WebSocket\\": "tests/mock"
}
},
"require": {
"php": "^7.4 | ^8.0",
"phrity/net-uri": "^1.0",
"phrity/util-errorhandler": "^1.0",
"psr/log": "^1.0 | ^2.0 | ^3.0",
"psr/http-message": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^9.0",
"php-coveralls/php-coveralls": "^2.0",
"squizlabs/php_codesniffer": "^3.5"
}
}

View File

@ -1,167 +0,0 @@
[Client](Client.md) • [Server](Server.md) • [Message](Message.md) • [Examples](Examples.md) • Changelog • [Contributing](Contributing.md)
# Websocket: Changelog
## `v1.6`
> PHP version `^7.4|^8.0`
### `1.6.3`
* Fix issue with implicit default ports (@etrinh, @sirn-se)
### `1.6.2`
* Fix issue where port was missing in socket uri (@sirn-se)
### `1.6.1`
* Fix client path for http request (@simPod, @sirn-se)
### `1.6.0`
* Connection separate from Client and Server (@sirn-se)
* getPier() deprecated, replaced by getRemoteName() (@sirn-se)
* Client accepts `Psr\Http\Message\UriInterface` as input for URI:s (@sirn-se)
* Bad URI throws exception when Client is instanciated, previously when used (@sirn-se)
* Preparations for multiple conection and listeners (@sirn-se)
* Major internal refactoring (@sirn-se)
## `v1.5`
> PHP version `^7.2|^8.0`
### `1.5.8`
* Handle read error during handshake (@sirn-se)
### `1.5.7`
* Large header block fix (@sirn-se)
### `1.5.6`
* Add test for PHP 8.1 (@sirn-se)
* Code standard (@sirn-se)
### `1.5.5`
* Support for psr/log v2 and v3 (@simPod)
* GitHub Actions replaces Travis (@sirn-se)
### `1.5.4`
* Keep open connection on read timeout (@marcroberts)
### `1.5.3`
* Fix for persistent connection (@sirn-se)
### `1.5.2`
* Fix for getName() method (@sirn-se)
### `1.5.1`
* Fix for persistent connections (@rmeisler)
### `1.5.0`
* Convenience send methods; text(), binary(), ping(), pong() (@sirn-se)
* Optional Message instance as receive() method return (@sirn-se)
* Opcode filter for receive() method (@sirn-se)
* Added PHP `8.0` support (@webpatser)
* Dropped PHP `7.1` support (@sirn-se)
* Fix for unordered fragmented messages (@sirn-se)
* Improved error handling on stream calls (@sirn-se)
* Various code re-write (@sirn-se)
## `v1.4`
> PHP version `^7.1`
#### `1.4.3`
* Solve stream closure/get meta conflict (@sirn-se)
* Examples and documentation overhaul (@sirn-se)
#### `1.4.2`
* Force stream close on read error (@sirn-se)
* Authorization headers line feed (@sirn-se)
* Documentation (@matias-pool, @sirn-se)
#### `1.4.1`
* Ping/Pong, handled internally to avoid breaking fragmented messages (@nshmyrev, @sirn-se)
* Fix for persistent connections (@rmeisler)
* Fix opcode bitmask (@peterjah)
#### `1.4.0`
* Dropped support of old PHP versions (@sirn-se)
* Added PSR-3 Logging support (@sirn-se)
* Persistent connection option (@slezakattack)
* TimeoutException on connection time out (@slezakattack)
## `v1.3`
> PHP version `^5.4` and `^7.0`
#### `1.3.1`
* Allow control messages without payload (@Logioniz)
* Error code in ConnectionException (@sirn-se)
#### `1.3.0`
* Implements ping/pong frames (@pmccarren @Logioniz)
* Close behaviour (@sirn-se)
* Various fixes concerning connection handling (@sirn-se)
* Overhaul of Composer, Travis and Coveralls setup, PSR code standard and unit tests (@sirn-se)
## `v1.2`
> PHP version `^5.4` and `^7.0`
#### `1.2.0`
* Adding stream context options (to set e.g. SSL `allow_self_signed`).
## `v1.1`
> PHP version `^5.4` and `^7.0`
#### `1.1.2`
* Fixed error message on broken frame.
#### `1.1.1`
* Adding license information.
#### `1.1.0`
* Supporting huge payloads.
## `v1.0`
> PHP version `^5.4` and `^7.0`
#### `1.0.3`
* Bugfix: Correcting address in error-message
#### `1.0.2`
* Bugfix: Add port in request-header.
#### `1.0.1`
* Fixing a bug from empty payloads.
#### `1.0.0`
* Release as production ready.
* Adding option to set/override headers.
* Supporting basic authentication from user:pass in URL.

View File

@ -1,137 +0,0 @@
Client • [Server](Server.md) • [Message](Message.md) • [Examples](Examples.md) • [Changelog](Changelog.md) • [Contributing](Contributing.md)
# Websocket: Client
The client can read and write on a WebSocket stream.
It internally supports Upgrade handshake and implicit close and ping/pong operations.
## Class synopsis
```php
WebSocket\Client {
public __construct(UriInterface|string $uri, array $options = []);
public __destruct();
public __toString() : string;
public text(string $payload) : void;
public binary(string $payload) : void;
public ping(string $payload = '') : void;
public pong(string $payload = '') : void;
public send(Message|string $payload, string $opcode = 'text', bool $masked = true) : void;
public close(int $status = 1000, mixed $message = 'ttfn') : void;
public receive() : Message|string|null;
public getName() : string|null;
public getRemoteName() : string|null;
public getLastOpcode() : string;
public getCloseStatus() : int;
public isConnected() : bool;
public setTimeout(int $seconds) : void;
public setFragmentSize(int $fragment_size) : self;
public getFragmentSize() : int;
public setLogger(Psr\Log\LoggerInterface $logger = null) : void;
}
```
## Examples
### Simple send-receive operation
This example send a single message to a server, and output the response.
```php
$client = new WebSocket\Client("ws://echo.websocket.org/");
$client->text("Hello WebSocket.org!");
echo $client->receive();
$client->close();
```
### Listening to a server
To continuously listen to incoming messages, you need to put the receive operation within a loop.
Note that these functions **always** throw exception on any failure, including recoverable failures such as connection time out.
By consuming exceptions, the code will re-connect the socket in next loop iteration.
```php
$client = new WebSocket\Client("ws://echo.websocket.org/");
while (true) {
try {
$message = $client->receive();
// Act on received message
// Break while loop to stop listening
} catch (\WebSocket\ConnectionException $e) {
// Possibly log errors
}
}
$client->close();
```
### Filtering received messages
By default the `receive()` method return messages of 'text' and 'binary' opcode.
The filter option allows you to specify which message types to return.
```php
$client = new WebSocket\Client("ws://echo.websocket.org/", ['filter' => ['text']]);
$client->receive(); // Only return 'text' messages
$client = new WebSocket\Client("ws://echo.websocket.org/", ['filter' => ['text', 'binary', 'ping', 'pong', 'close']]);
$client->receive(); // Return all messages
```
### Sending messages
There are convenience methods to send messages with different opcodes.
```php
$client = new WebSocket\Client("ws://echo.websocket.org/");
// Convenience methods
$client->text('A plain text message'); // Send an opcode=text message
$client->binary($binary_string); // Send an opcode=binary message
$client->ping(); // Send an opcode=ping frame
$client->pong(); // Send an unsolicited opcode=pong frame
// Generic send method
$client->send($payload); // Sent as masked opcode=text
$client->send($payload, 'binary'); // Sent as masked opcode=binary
$client->send($payload, 'binary', false); // Sent as unmasked opcode=binary
```
## Constructor options
The `$options` parameter in constructor accepts an associative array of options.
* `context` - A stream context created using [stream_context_create](https://www.php.net/manual/en/function.stream-context-create).
* `filter` - Array of opcodes to return on receive, default `['text', 'binary']`
* `fragment_size` - Maximum payload size. Default 4096 chars.
* `headers` - Additional headers as associative array name => content.
* `logger` - A [PSR-3](https://www.php-fig.org/psr/psr-3/) compatible logger.
* `persistent` - Connection is re-used between requests until time out is reached. Default false.
* `return_obj` - Return a [Message](Message.md) instance on receive, default false
* `timeout` - Time out in seconds. Default 5 seconds.
```php
$context = stream_context_create();
stream_context_set_option($context, 'ssl', 'verify_peer', false);
stream_context_set_option($context, 'ssl', 'verify_peer_name', false);
$client = new WebSocket\Client("ws://echo.websocket.org/", [
'context' => $context, // Attach stream context created above
'filter' => ['text', 'binary', 'ping'], // Specify message types for receive() to return
'headers' => [ // Additional headers, used to specify subprotocol
'Sec-WebSocket-Protocol' => 'soap',
'origin' => 'localhost',
],
'logger' => $my_psr3_logger, // Attach a PSR3 compatible logger
'return_obj' => true, // Return Message instance rather than just text
'timeout' => 60, // 1 minute time out
]);
```
## Exceptions
* `WebSocket\BadOpcodeException` - Thrown if provided opcode is invalid.
* `WebSocket\BadUriException` - Thrown if provided URI is invalid.
* `WebSocket\ConnectionException` - Thrown on any socket I/O failure.
* `WebSocket\TimeoutException` - Thrown when the socket experiences a time out.

View File

@ -1,51 +0,0 @@
[Client](Client.md) • [Server](Server.md) • [Message](Message.md) • [Examples](Examples.md) • [Changelog](Changelog.md) • Contributing
# Websocket: Contributing
Everyone is welcome to help out!
But to keep this project sustainable, please ensure your contribution respects the requirements below.
## PR Requirements
Requirements on pull requests;
* All tests **MUST** pass.
* Code coverage **MUST** remain at 100%.
* Code **MUST** adhere to PSR-1 and PSR-12 code standards.
Base your patch on corresponding version branch, and target that version branch in your pull request.
* `v1.6-master` current version
* `v1.5-master` previous version, bug fixes only
* Older versions should not be target of pull requests
## Dependency management
Install or update dependencies using [Composer](https://getcomposer.org/).
```
# Install dependencies
make install
# Update dependencies
make update
```
## Code standard
This project uses [PSR-1](https://www.php-fig.org/psr/psr-1/) and [PSR-12](https://www.php-fig.org/psr/psr-12/) code standards.
```
# Check code standard adherence
make cs-check
```
## Unit testing
Unit tests with [PHPUnit](https://phpunit.readthedocs.io/), coverage with [Coveralls](https://github.com/php-coveralls/php-coveralls)
```
# Run unit tests
make test
# Create coverage
make coverage
```

View File

@ -1,101 +0,0 @@
[Client](Client.md) • [Server](Server.md) • [Message](Message.md) • Examples • [Changelog](Changelog.md) • [Contributing](Contributing.md)
# Websocket: Examples
Here are some examples on how to use the WebSocket library.
## Echo logger
In dev environment (as in having run composer to include dev dependencies) you have
access to a simple echo logger that print out information synchronously.
This is usable for debugging. For production, use a proper logger.
```php
namespace WebSocket;
$logger = new EchoLogger();
$client = new Client('ws://echo.websocket.org/');
$client->setLogger($logger);
$server = new Server();
$server->setLogger($logger);
```
An example of server output;
```
info | Server listening to port 8000 []
debug | Wrote 129 of 129 bytes. []
info | Server connected to port 8000 []
info | Received 'text' message []
debug | Wrote 9 of 9 bytes. []
info | Sent 'text' message []
debug | Received 'close', status: 1000. []
debug | Wrote 32 of 32 bytes. []
info | Sent 'close' message []
info | Received 'close' message []
```
## The `send` client
Source: [examples/send.php](../examples/send.php)
A simple, single send/receive client.
Example use:
```
php examples/send.php --opcode text "A text message" // Send a text message to localhost
php examples/send.php --opcode ping "ping it" // Send a ping message to localhost
php examples/send.php --uri ws://echo.websocket.org "A text message" // Send a text message to echo.websocket.org
php examples/send.php --opcode text --debug "A text message" // Use runtime debugging
```
## The `echoserver` server
Source: [examples/echoserver.php](../examples/echoserver.php)
A simple server that responds to recevied commands.
Example use:
```
php examples/echoserver.php // Run with default settings
php examples/echoserver.php --port 8080 // Listen on port 8080
php examples/echoserver.php --debug // Use runtime debugging
```
These strings can be sent as message to trigger server to perform actions;
* `auth` - Server will respond with auth header if provided by client
* `close` - Server will close current connection
* `exit` - Server will close all active connections
* `headers` - Server will respond with all headers provided by client
* `ping` - Server will send a ping message
* `pong` - Server will send a pong message
* `stop` - Server will stop listening
* For other sent strings, server will respond with the same strings
## The `random` client
Source: [examples/random_client.php](../examples/random_client.php)
The random client will use random options and continuously send/receive random messages.
Example use:
```
php examples/random_client.php --uri ws://echo.websocket.org // Connect to echo.websocket.org
php examples/random_client.php --timeout 5 --fragment_size 16 // Specify settings
php examples/random_client.php --debug // Use runtime debugging
```
## The `random` server
Source: [examples/random_server.php](../examples/random_server.php)
The random server will use random options and continuously send/receive random messages.
Example use:
```
php examples/random_server.php --port 8080 // // Listen on port 8080
php examples/random_server.php --timeout 5 --fragment_size 16 // Specify settings
php examples/random_server.php --debug // Use runtime debugging
```

View File

@ -1,60 +0,0 @@
[Client](Client.md) • [Server](Server.md) • Message • [Examples](Examples.md) • [Changelog](Changelog.md) • [Contributing](Contributing.md)
# Websocket: Messages
If option `return_obj` is set to `true` on [client](Client.md) or [server](Server.md),
the `receive()` method will return a Message instance instead of a string.
Available classes correspond to opcode;
* WebSocket\Message\Text
* WebSocket\Message\Binary
* WebSocket\Message\Ping
* WebSocket\Message\Pong
* WebSocket\Message\Close
Additionally;
* WebSocket\Message\Message - abstract base class for all messages above
* WebSocket\Message\Factory - Factory class to create Message instances
## Message abstract class synopsis
```php
WebSocket\Message\Message {
public __construct(string $payload = '');
public __toString() : string;
public getOpcode() : string;
public getLength() : int;
public getTimestamp() : DateTime;
public getContent() : string;
public setContent(string $payload = '') : void;
public hasContent() : bool;
}
```
## Factory class synopsis
```php
WebSocket\Message\Factory {
public create(string $opcode, string $payload = '') : Message;
}
```
## Example
Receving a Message and echo some methods.
```php
$client = new WebSocket\Client('ws://echo.websocket.org/', ['return_obj' => true]);
$client->text('Hello WebSocket.org!');
// Echo return same message as sent
$message = $client->receive();
echo $message->getOpcode(); // -> "text"
echo $message->getLength(); // -> 20
echo $message->getContent(); // -> "Hello WebSocket.org!"
echo $message->hasContent(); // -> true
echo $message->getTimestamp()->format('H:i:s'); // -> 19:37:18
$client->close();
```

View File

@ -1,136 +0,0 @@
[Client](Client.md) • Server • [Message](Message.md) • [Examples](Examples.md) • [Changelog](Changelog.md) • [Contributing](Contributing.md)
# Websocket: Server
The library contains a rudimentary single stream/single thread server.
It internally supports Upgrade handshake and implicit close and ping/pong operations.
Note that it does **not** support threading or automatic association ot continuous client requests.
If you require this kind of server behavior, you need to build it on top of provided server implementation.
## Class synopsis
```php
WebSocket\Server {
public __construct(array $options = []);
public __destruct();
public __toString() : string;
public accept() : bool;
public text(string $payload) : void;
public binary(string $payload) : void;
public ping(string $payload = '') : void;
public pong(string $payload = '') : void;
public send(Message|string $payload, string $opcode = 'text', bool $masked = true) : void;
public close(int $status = 1000, mixed $message = 'ttfn') : void;
public receive() : Message|string|null;
public getPort() : int;
public getPath() : string;
public getRequest() : array;
public getHeader(string $header_name) : string|null;
public getName() : string|null;
public getRemoteName() : string|null;
public getLastOpcode() : string;
public getCloseStatus() : int;
public isConnected() : bool;
public setTimeout(int $seconds) : void;
public setFragmentSize(int $fragment_size) : self;
public getFragmentSize() : int;
public setLogger(Psr\Log\LoggerInterface $logger = null) : void;
}
```
## Examples
### Simple receive-send operation
This example reads a single message from a client, and respond with the same message.
```php
$server = new WebSocket\Server();
$server->accept();
$message = $server->receive();
$server->text($message);
$server->close();
```
### Listening to clients
To continuously listen to incoming messages, you need to put the receive operation within a loop.
Note that these functions **always** throw exception on any failure, including recoverable failures such as connection time out.
By consuming exceptions, the code will re-connect the socket in next loop iteration.
```php
$server = new WebSocket\Server();
while ($server->accept()) {
try {
$message = $server->receive();
// Act on received message
// Break while loop to stop listening
} catch (\WebSocket\ConnectionException $e) {
// Possibly log errors
}
}
$server->close();
```
### Filtering received messages
By default the `receive()` method return messages of 'text' and 'binary' opcode.
The filter option allows you to specify which message types to return.
```php
$server = new WebSocket\Server(['filter' => ['text']]);
$server->receive(); // only return 'text' messages
$server = new WebSocket\Server(['filter' => ['text', 'binary', 'ping', 'pong', 'close']]);
$server->receive(); // return all messages
```
### Sending messages
There are convenience methods to send messages with different opcodes.
```php
$server = new WebSocket\Server();
// Convenience methods
$server->text('A plain text message'); // Send an opcode=text message
$server->binary($binary_string); // Send an opcode=binary message
$server->ping(); // Send an opcode=ping frame
$server->pong(); // Send an unsolicited opcode=pong frame
// Generic send method
$server->send($payload); // Sent as masked opcode=text
$server->send($payload, 'binary'); // Sent as masked opcode=binary
$server->send($payload, 'binary', false); // Sent as unmasked opcode=binary
```
## Constructor options
The `$options` parameter in constructor accepts an associative array of options.
* `filter` - Array of opcodes to return on receive, default `['text', 'binary']`
* `fragment_size` - Maximum payload size. Default 4096 chars.
* `logger` - A [PSR-3](https://www.php-fig.org/psr/psr-3/) compatible logger.
* `port` - The server port to listen to. Default 8000.
* `return_obj` - Return a [Message](Message.md) instance on receive, default false
* `timeout` - Time out in seconds. Default 5 seconds.
```php
$server = new WebSocket\Server([
'filter' => ['text', 'binary', 'ping'], // Specify message types for receive() to return
'logger' => $my_psr3_logger, // Attach a PSR3 compatible logger
'port' => 9000, // Listening port
'return_obj' => true, // Return Message instance rather than just text
'timeout' => 60, // 1 minute time out
]);
```
## Exceptions
* `WebSocket\BadOpcodeException` - Thrown if provided opcode is invalid.
* `WebSocket\ConnectionException` - Thrown on any socket I/O failure.
* `WebSocket\TimeoutException` - Thrown when the socket experiences a time out.

View File

@ -1,87 +0,0 @@
<?php
/**
* This file is used for the tests, but can also serve as an example of a WebSocket\Server.
* Run in console: php examples/echoserver.php
*
* Console options:
* --port <int> : The port to listen to, default 8000
* --timeout <int> : Timeout in seconds, default 200 seconds
* --debug : Output log data (if logger is available)
*/
namespace WebSocket;
require __DIR__ . '/../vendor/autoload.php';
error_reporting(-1);
echo "> Echo server\n";
// Server options specified or random
$options = array_merge([
'port' => 8000,
'timeout' => 200,
'filter' => ['text', 'binary', 'ping', 'pong', 'close'],
], getopt('', ['port:', 'timeout:', 'debug']));
// If debug mode and logger is available
if (isset($options['debug']) && class_exists('WebSocket\EchoLog')) {
$logger = new EchoLog();
$options['logger'] = $logger;
echo "> Using logger\n";
}
// Initiate server.
try {
$server = new Server($options);
} catch (ConnectionException $e) {
echo "> ERROR: {$e->getMessage()}\n";
die();
}
echo "> Listening to port {$server->getPort()}\n";
// Force quit to close server
while (true) {
try {
while ($server->accept()) {
echo "> Accepted on port {$server->getPort()}\n";
while (true) {
$message = $server->receive();
$opcode = $server->getLastOpcode();
if (is_null($message)) {
echo "> Closing connection\n";
continue 2;
}
echo "> Got '{$message}' [opcode: {$opcode}]\n";
if (in_array($opcode, ['ping', 'pong'])) {
$server->send($message);
continue;
}
// Allow certain string to trigger server action
switch ($message) {
case 'exit':
echo "> Client told me to quit. Bye bye.\n";
$server->close();
echo "> Close status: {$server->getCloseStatus()}\n";
exit;
case 'headers':
$server->text(implode("\r\n", $server->getRequest()));
break;
case 'ping':
$server->ping($message);
break;
case 'auth':
$auth = $server->getHeader('Authorization');
$server->text("{$auth} - {$message}");
break;
default:
$server->text($message);
}
}
}
} catch (ConnectionException $e) {
echo "> ERROR: {$e->getMessage()}\n";
}
}

View File

@ -1,94 +0,0 @@
<?php
/**
* Websocket client that read/write random data.
* Run in console: php examples/random_client.php
*
* Console options:
* --uri <uri> : The URI to connect to, default ws://localhost:8000
* --timeout <int> : Timeout in seconds, random default
* --fragment_size <int> : Fragment size as bytes, random default
* --debug : Output log data (if logger is available)
*/
namespace WebSocket;
require __DIR__ . '/../vendor/autoload.php';
error_reporting(-1);
$randStr = function (int $maxlength = 4096) {
$string = '';
$length = rand(1, $maxlength);
for ($i = 0; $i < $length; $i++) {
$string .= chr(rand(33, 126));
}
return $string;
};
echo "> Random client\n";
// Server options specified or random
$options = array_merge([
'uri' => 'ws://localhost:8000',
'timeout' => rand(1, 60),
'fragment_size' => rand(1, 4096) * 8,
], getopt('', ['uri:', 'timeout:', 'fragment_size:', 'debug']));
// If debug mode and logger is available
if (isset($options['debug']) && class_exists('WebSocket\EchoLog')) {
$logger = new EchoLog();
$options['logger'] = $logger;
echo "> Using logger\n";
}
// Main loop
while (true) {
try {
$client = new Client($options['uri'], $options);
$info = json_encode([
'uri' => $options['uri'],
'timeout' => $options['timeout'],
'framgemt_size' => $client->getFragmentSize(),
]);
echo "> Creating client {$info}\n";
try {
while (true) {
// Random actions
switch (rand(1, 10)) {
case 1:
echo "> Sending text\n";
$client->text("Text message {$randStr()}");
break;
case 2:
echo "> Sending binary\n";
$client->binary("Binary message {$randStr()}");
break;
case 3:
echo "> Sending close\n";
$client->close(rand(1000, 2000), "Close message {$randStr(8)}");
break;
case 4:
echo "> Sending ping\n";
$client->ping("Ping message {$randStr(8)}");
break;
case 5:
echo "> Sending pong\n";
$client->pong("Pong message {$randStr(8)}");
break;
default:
echo "> Receiving\n";
$received = $client->receive();
echo "> Received {$client->getLastOpcode()}: {$received}\n";
}
sleep(rand(1, 5));
}
} catch (\Throwable $e) {
echo "ERROR I/O: {$e->getMessage()} [{$e->getCode()}]\n";
}
} catch (\Throwable $e) {
echo "ERROR: {$e->getMessage()} [{$e->getCode()}]\n";
}
sleep(rand(1, 5));
}

View File

@ -1,93 +0,0 @@
<?php
/**
* Websocket server that read/write random data.
* Run in console: php examples/random_server.php
*
* Console options:
* --port <int> : The port to listen to, default 8000
* --timeout <int> : Timeout in seconds, random default
* --fragment_size <int> : Fragment size as bytes, random default
* --debug : Output log data (if logger is available)
*/
namespace WebSocket;
require __DIR__ . '/../vendor/autoload.php';
error_reporting(-1);
$randStr = function (int $maxlength = 4096) {
$string = '';
$length = rand(1, $maxlength);
for ($i = 0; $i < $length; $i++) {
$string .= chr(rand(33, 126));
}
return $string;
};
echo "> Random server\n";
// Server options specified or random
$options = array_merge([
'port' => 8000,
'timeout' => rand(1, 60),
'fragment_size' => rand(1, 4096) * 8,
], getopt('', ['port:', 'timeout:', 'fragment_size:', 'debug']));
// If debug mode and logger is available
if (isset($options['debug']) && class_exists('WebSocket\EchoLog')) {
$logger = new EchoLog();
$options['logger'] = $logger;
echo "> Using logger\n";
}
// Force quit to close server
while (true) {
try {
// Setup server
$server = new Server($options);
$info = json_encode([
'port' => $server->getPort(),
'timeout' => $options['timeout'],
'framgemt_size' => $server->getFragmentSize(),
]);
echo "> Creating server {$info}\n";
while ($server->accept()) {
while (true) {
// Random actions
switch (rand(1, 10)) {
case 1:
echo "> Sending text\n";
$server->text("Text message {$randStr()}");
break;
case 2:
echo "> Sending binary\n";
$server->binary("Binary message {$randStr()}");
break;
case 3:
echo "> Sending close\n";
$server->close(rand(1000, 2000), "Close message {$randStr(8)}");
break;
case 4:
echo "> Sending ping\n";
$server->ping("Ping message {$randStr(8)}");
break;
case 5:
echo "> Sending pong\n";
$server->pong("Pong message {$randStr(8)}");
break;
default:
echo "> Receiving\n";
$received = $server->receive();
echo "> Received {$server->getLastOpcode()}: {$received}\n";
}
sleep(rand(1, 5));
}
}
} catch (\Throwable $e) {
echo "ERROR: {$e->getMessage()} [{$e->getCode()}]\n";
}
sleep(rand(1, 5));
}

View File

@ -1,51 +0,0 @@
<?php
/**
* Simple send & receive client for test purpose.
* Run in console: php examples/send.php <options> <message>
*
* Console options:
* --uri <uri> : The URI to connect to, default ws://localhost:8000
* --opcode <string> : Opcode to send, default 'text'
* --debug : Output log data (if logger is available)
*/
namespace WebSocket;
require __DIR__ . '/../vendor/autoload.php';
error_reporting(-1);
echo "> Send client\n";
// Server options specified or random
$options = array_merge([
'uri' => 'ws://localhost:8000',
'opcode' => 'text',
], getopt('', ['uri:', 'opcode:', 'debug']));
$message = array_pop($argv);
// If debug mode and logger is available
if (isset($options['debug']) && class_exists('WebSocket\EchoLog')) {
$logger = new EchoLog();
$options['logger'] = $logger;
echo "> Using logger\n";
}
try {
// Create client, send and recevie
$client = new Client($options['uri'], $options);
$client->send($message, $options['opcode']);
echo "> Sent '{$message}' [opcode: {$options['opcode']}]\n";
if (in_array($options['opcode'], ['text', 'binary'])) {
$message = $client->receive();
$opcode = $client->getLastOpcode();
if (!is_null($message)) {
echo "> Got '{$message}' [opcode: {$opcode}]\n";
}
}
$client->close();
echo "> Closing client\n";
} catch (\Throwable $e) {
echo "ERROR: {$e->getMessage()} [{$e->getCode()}]\n";
}

View File

@ -1,14 +0,0 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket;
class BadOpcodeException extends Exception
{
}

View File

@ -1,7 +0,0 @@
<?php
namespace WebSocket;
class BadUriException extends Exception
{
}

View File

@ -1,490 +0,0 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket;
use ErrorException;
use InvalidArgumentException;
use Phrity\Net\Uri;
use Phrity\Util\ErrorHandler;
use Psr\Http\Message\UriInterface;
use Psr\Log\{
LoggerAwareInterface,
LoggerAwareTrait,
LoggerInterface,
NullLogger
};
use WebSocket\Message\Factory;
class Client implements LoggerAwareInterface
{
use LoggerAwareTrait; // provides setLogger(LoggerInterface $logger)
use OpcodeTrait;
// Default options
protected static $default_options = [
'context' => null,
'filter' => ['text', 'binary'],
'fragment_size' => 4096,
'headers' => null,
'logger' => null,
'origin' => null, // @deprecated
'persistent' => false,
'return_obj' => false,
'timeout' => 5,
];
private $socket_uri;
private $connection;
private $options = [];
private $listen = false;
private $last_opcode = null;
/* ---------- Magic methods ------------------------------------------------------ */
/**
* @param UriInterface|string $uri A ws/wss-URI
* @param array $options
* Associative array containing:
* - context: Set the stream context. Default: empty context
* - timeout: Set the socket timeout in seconds. Default: 5
* - fragment_size: Set framgemnt size. Default: 4096
* - headers: Associative array of headers to set/override.
*/
public function __construct($uri, array $options = [])
{
$this->socket_uri = $this->parseUri($uri);
$this->options = array_merge(self::$default_options, [
'logger' => new NullLogger(),
], $options);
$this->setLogger($this->options['logger']);
}
/**
* Get string representation of instance.
* @return string String representation.
*/
public function __toString(): string
{
return sprintf(
"%s(%s)",
get_class($this),
$this->getName() ?: 'closed'
);
}
/* ---------- Client option functions -------------------------------------------- */
/**
* Set timeout.
* @param int $timeout Timeout in seconds.
*/
public function setTimeout(int $timeout): void
{
$this->options['timeout'] = $timeout;
if (!$this->isConnected()) {
return;
}
$this->connection->setTimeout($timeout);
$this->connection->setOptions($this->options);
}
/**
* Set fragmentation size.
* @param int $fragment_size Fragment size in bytes.
* @return self.
*/
public function setFragmentSize(int $fragment_size): self
{
$this->options['fragment_size'] = $fragment_size;
$this->connection->setOptions($this->options);
return $this;
}
/**
* Get fragmentation size.
* @return int $fragment_size Fragment size in bytes.
*/
public function getFragmentSize(): int
{
return $this->options['fragment_size'];
}
/* ---------- Connection operations ---------------------------------------------- */
/**
* Send text message.
* @param string $payload Content as string.
*/
public function text(string $payload): void
{
$this->send($payload);
}
/**
* Send binary message.
* @param string $payload Content as binary string.
*/
public function binary(string $payload): void
{
$this->send($payload, 'binary');
}
/**
* Send ping.
* @param string $payload Optional text as string.
*/
public function ping(string $payload = ''): void
{
$this->send($payload, 'ping');
}
/**
* Send unsolicited pong.
* @param string $payload Optional text as string.
*/
public function pong(string $payload = ''): void
{
$this->send($payload, 'pong');
}
/**
* Send message.
* @param string $payload Message to send.
* @param string $opcode Opcode to use, default: 'text'.
* @param bool $masked If message should be masked default: true.
*/
public function send(string $payload, string $opcode = 'text', bool $masked = true): void
{
if (!$this->isConnected()) {
$this->connect();
}
if (!in_array($opcode, array_keys(self::$opcodes))) {
$warning = "Bad opcode '{$opcode}'. Try 'text' or 'binary'.";
$this->logger->warning($warning);
throw new BadOpcodeException($warning);
}
$factory = new Factory();
$message = $factory->create($opcode, $payload);
$this->connection->pushMessage($message, $masked);
}
/**
* Tell the socket to close.
* @param integer $status http://tools.ietf.org/html/rfc6455#section-7.4
* @param string $message A closing message, max 125 bytes.
*/
public function close(int $status = 1000, string $message = 'ttfn'): void
{
if (!$this->isConnected()) {
return;
}
$this->connection->close($status, $message);
}
/**
* Disconnect from server.
*/
public function disconnect(): void
{
if ($this->isConnected()) {
$this->connection->disconnect();
}
}
/**
* Receive message.
* Note that this operation will block reading.
* @return mixed Message, text or null depending on settings.
*/
public function receive()
{
$filter = $this->options['filter'];
$return_obj = $this->options['return_obj'];
if (!$this->isConnected()) {
$this->connect();
}
while (true) {
$message = $this->connection->pullMessage();
$opcode = $message->getOpcode();
if (in_array($opcode, $filter)) {
$this->last_opcode = $opcode;
$return = $return_obj ? $message : $message->getContent();
break;
} elseif ($opcode == 'close') {
$this->last_opcode = null;
$return = $return_obj ? $message : null;
break;
}
}
return $return;
}
/* ---------- Connection functions ----------------------------------------------- */
/**
* Get last received opcode.
* @return string|null Opcode.
*/
public function getLastOpcode(): ?string
{
return $this->last_opcode;
}
/**
* Get close status on connection.
* @return int|null Close status.
*/
public function getCloseStatus(): ?int
{
return $this->connection ? $this->connection->getCloseStatus() : null;
}
/**
* If Client has active connection.
* @return bool True if active connection.
*/
public function isConnected(): bool
{
return $this->connection && $this->connection->isConnected();
}
/**
* Get name of local socket, or null if not connected.
* @return string|null
*/
public function getName(): ?string
{
return $this->isConnected() ? $this->connection->getName() : null;
}
/**
* Get name of remote socket, or null if not connected.
* @return string|null
*/
public function getRemoteName(): ?string
{
return $this->isConnected() ? $this->connection->getRemoteName() : null;
}
/**
* Get name of remote socket, or null if not connected.
* @return string|null
* @deprecated Will be removed in future version, use getPeer() instead.
*/
public function getPier(): ?string
{
trigger_error(
'getPier() is deprecated and will be removed in future version. Use getRemoteName() instead.',
E_USER_DEPRECATED
);
return $this->getRemoteName();
}
/* ---------- Helper functions --------------------------------------------------- */
/**
* Perform WebSocket handshake
*/
protected function connect(): void
{
$this->connection = null;
$host_uri = $this->socket_uri
->withScheme($this->socket_uri->getScheme() == 'wss' ? 'ssl' : 'tcp')
->withPort($this->socket_uri->getPort() ?? ($this->socket_uri->getScheme() == 'wss' ? 443 : 80))
->withPath('')
->withQuery('')
->withFragment('')
->withUserInfo('');
// Path must be absolute
$http_path = $this->socket_uri->getPath();
if ($http_path === '' || $http_path[0] !== '/') {
$http_path = "/{$http_path}";
}
$http_uri = (new Uri())
->withPath($http_path)
->withQuery($this->socket_uri->getQuery());
// Set the stream context options if they're already set in the config
if (isset($this->options['context'])) {
// Suppress the error since we'll catch it below
if (@get_resource_type($this->options['context']) === 'stream-context') {
$context = $this->options['context'];
} else {
$error = "Stream context in \$options['context'] isn't a valid context.";
$this->logger->error($error);
throw new \InvalidArgumentException($error);
}
} else {
$context = stream_context_create();
}
$persistent = $this->options['persistent'] === true;
$flags = STREAM_CLIENT_CONNECT;
$flags = $persistent ? $flags | STREAM_CLIENT_PERSISTENT : $flags;
$socket = null;
try {
$handler = new ErrorHandler();
$socket = $handler->with(function () use ($host_uri, $flags, $context) {
$error = $errno = $errstr = null;
// Open the socket.
return stream_socket_client(
$host_uri,
$errno,
$errstr,
$this->options['timeout'],
$flags,
$context
);
});
if (!$socket) {
throw new ErrorException('No socket');
}
} catch (ErrorException $e) {
$error = "Could not open socket to \"{$host_uri->getAuthority()}\": {$e->getMessage()} ({$e->getCode()}).";
$this->logger->error($error, ['severity' => $e->getSeverity()]);
throw new ConnectionException($error, 0, [], $e);
}
$this->connection = new Connection($socket, $this->options);
$this->connection->setLogger($this->logger);
if (!$this->isConnected()) {
$error = "Invalid stream on \"{$host_uri->getAuthority()}\".";
$this->logger->error($error);
throw new ConnectionException($error);
}
if (!$persistent || $this->connection->tell() == 0) {
// Set timeout on the stream as well.
$this->connection->setTimeout($this->options['timeout']);
// Generate the WebSocket key.
$key = self::generateKey();
// Default headers
$headers = [
'Host' => $host_uri->getAuthority(),
'User-Agent' => 'websocket-client-php',
'Connection' => 'Upgrade',
'Upgrade' => 'websocket',
'Sec-WebSocket-Key' => $key,
'Sec-WebSocket-Version' => '13',
];
// Handle basic authentication.
if ($userinfo = $this->socket_uri->getUserInfo()) {
$headers['authorization'] = 'Basic ' . base64_encode($userinfo);
}
// Deprecated way of adding origin (use headers instead).
if (isset($this->options['origin'])) {
$headers['origin'] = $this->options['origin'];
}
// Add and override with headers from options.
if (isset($this->options['headers'])) {
$headers = array_merge($headers, $this->options['headers']);
}
$header = "GET {$http_uri} HTTP/1.1\r\n" . implode(
"\r\n",
array_map(
function ($key, $value) {
return "$key: $value";
},
array_keys($headers),
$headers
)
) . "\r\n\r\n";
// Send headers.
$this->connection->write($header);
// Get server response header (terminated with double CR+LF).
$response = '';
try {
do {
$buffer = $this->connection->gets(1024);
$response .= $buffer;
} while (substr_count($response, "\r\n\r\n") == 0);
} catch (Exception $e) {
throw new ConnectionException('Client handshake error', $e->getCode(), $e->getData(), $e);
}
// Validate response.
if (!preg_match('#Sec-WebSocket-Accept:\s(.*)$#mUi', $response, $matches)) {
$error = sprintf(
"Connection to '%s' failed: Server sent invalid upgrade response: %s",
(string)$this->socket_uri,
(string)$response
);
$this->logger->error($error);
throw new ConnectionException($error);
}
$keyAccept = trim($matches[1]);
$expectedResonse = base64_encode(
pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))
);
if ($keyAccept !== $expectedResonse) {
$error = 'Server sent bad upgrade response.';
$this->logger->error($error);
throw new ConnectionException($error);
}
}
$this->logger->info("Client connected to {$this->socket_uri}");
}
/**
* Generate a random string for WebSocket key.
* @return string Random string
*/
protected static function generateKey(): string
{
$key = '';
for ($i = 0; $i < 16; $i++) {
$key .= chr(rand(33, 126));
}
return base64_encode($key);
}
protected function parseUri($uri): UriInterface
{
if ($uri instanceof UriInterface) {
$uri = $uri;
} elseif (is_string($uri)) {
try {
$uri = new Uri($uri);
} catch (InvalidArgumentException $e) {
throw new BadUriException("Invalid URI '{$uri}' provided.", 0, $e);
}
} else {
throw new BadUriException("Provided URI must be a UriInterface or string.");
}
if (!in_array($uri->getScheme(), ['ws', 'wss'])) {
throw new BadUriException("Invalid URI scheme, must be 'ws' or 'wss'.");
}
return $uri;
}
}

View File

@ -1,518 +0,0 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket;
use Psr\Log\{
LoggerAwareInterface,
LoggerAwareTrait,
LoggerInterface, NullLogger
};
use WebSocket\Message\{
Factory,
Message
};
class Connection implements LoggerAwareInterface
{
use LoggerAwareTrait;
use OpcodeTrait;
private $stream;
private $read_buffer;
private $msg_factory;
private $options = [];
protected $is_closing = false;
protected $close_status = null;
private $uid;
/* ---------- Construct & Destruct ----------------------------------------------- */
public function __construct($stream, array $options = [])
{
$this->stream = $stream;
$this->setOptions($options);
$this->setLogger(new NullLogger());
$this->msg_factory = new Factory();
}
public function __destruct()
{
if ($this->getType() === 'stream') {
fclose($this->stream);
}
}
public function setOptions(array $options = []): void
{
$this->options = array_merge($this->options, $options);
}
public function getCloseStatus(): ?int
{
return $this->close_status;
}
/**
* Tell the socket to close.
*
* @param integer $status http://tools.ietf.org/html/rfc6455#section-7.4
* @param string $message A closing message, max 125 bytes.
*/
public function close(int $status = 1000, string $message = 'ttfn'): void
{
if (!$this->isConnected()) {
return;
}
$status_binstr = sprintf('%016b', $status);
$status_str = '';
foreach (str_split($status_binstr, 8) as $binstr) {
$status_str .= chr(bindec($binstr));
}
$message = $this->msg_factory->create('close', $status_str . $message);
$this->pushMessage($message, true);
$this->logger->debug("Closing with status: {$status}.");
$this->is_closing = true;
while (true) {
$message = $this->pullMessage();
if ($message->getOpcode() == 'close') {
break;
}
}
}
/* ---------- Message methods ---------------------------------------------------- */
// Push a message to stream
public function pushMessage(Message $message, bool $masked = true): void
{
$frames = $message->getFrames($masked, $this->options['fragment_size']);
foreach ($frames as $frame) {
$this->pushFrame($frame);
}
$this->logger->info("[connection] Pushed {$message}", [
'opcode' => $message->getOpcode(),
'content-length' => $message->getLength(),
'frames' => count($frames),
]);
}
// Pull a message from stream
public function pullMessage(): Message
{
do {
$frame = $this->pullFrame();
$frame = $this->autoRespond($frame);
list ($final, $payload, $opcode, $masked) = $frame;
if ($opcode == 'close') {
$this->close();
}
// Continuation and factual opcode
$continuation = $opcode == 'continuation';
$payload_opcode = $continuation ? $this->read_buffer['opcode'] : $opcode;
// First continuation frame, create buffer
if (!$final && !$continuation) {
$this->read_buffer = ['opcode' => $opcode, 'payload' => $payload, 'frames' => 1];
continue; // Continue reading
}
// Subsequent continuation frames, add to buffer
if ($continuation) {
$this->read_buffer['payload'] .= $payload;
$this->read_buffer['frames']++;
}
} while (!$final);
// Final, return payload
$frames = 1;
if ($continuation) {
$payload = $this->read_buffer['payload'];
$frames = $this->read_buffer['frames'];
$this->read_buffer = null;
}
$message = $this->msg_factory->create($payload_opcode, $payload);
$this->logger->info("[connection] Pulled {$message}", [
'opcode' => $payload_opcode,
'content-length' => strlen($payload),
'frames' => $frames,
]);
return $message;
}
/* ---------- Frame I/O methods -------------------------------------------------- */
// Pull frame from stream
private function pullFrame(): array
{
// Read the fragment "header" first, two bytes.
$data = $this->read(2);
list ($byte_1, $byte_2) = array_values(unpack('C*', $data));
$final = (bool)($byte_1 & 0b10000000); // Final fragment marker.
$rsv = $byte_1 & 0b01110000; // Unused bits, ignore
// Parse opcode
$opcode_int = $byte_1 & 0b00001111;
$opcode_ints = array_flip(self::$opcodes);
if (!array_key_exists($opcode_int, $opcode_ints)) {
$warning = "Bad opcode in websocket frame: {$opcode_int}";
$this->logger->warning($warning);
throw new ConnectionException($warning, ConnectionException::BAD_OPCODE);
}
$opcode = $opcode_ints[$opcode_int];
// Masking bit
$masked = (bool)($byte_2 & 0b10000000);
$payload = '';
// Payload length
$payload_length = $byte_2 & 0b01111111;
if ($payload_length > 125) {
if ($payload_length === 126) {
$data = $this->read(2); // 126: Payload is a 16-bit unsigned int
$payload_length = current(unpack('n', $data));
} else {
$data = $this->read(8); // 127: Payload is a 64-bit unsigned int
$payload_length = current(unpack('J', $data));
}
}
// Get masking key.
if ($masked) {
$masking_key = $this->read(4);
}
// Get the actual payload, if any (might not be for e.g. close frames.
if ($payload_length > 0) {
$data = $this->read($payload_length);
if ($masked) {
// Unmask payload.
for ($i = 0; $i < $payload_length; $i++) {
$payload .= ($data[$i] ^ $masking_key[$i % 4]);
}
} else {
$payload = $data;
}
}
$this->logger->debug("[connection] Pulled '{opcode}' frame", [
'opcode' => $opcode,
'final' => $final,
'content-length' => strlen($payload),
]);
return [$final, $payload, $opcode, $masked];
}
// Push frame to stream
private function pushFrame(array $frame): void
{
list ($final, $payload, $opcode, $masked) = $frame;
$data = '';
$byte_1 = $final ? 0b10000000 : 0b00000000; // Final fragment marker.
$byte_1 |= self::$opcodes[$opcode]; // Set opcode.
$data .= pack('C', $byte_1);
$byte_2 = $masked ? 0b10000000 : 0b00000000; // Masking bit marker.
// 7 bits of payload length...
$payload_length = strlen($payload);
if ($payload_length > 65535) {
$data .= pack('C', $byte_2 | 0b01111111);
$data .= pack('J', $payload_length);
} elseif ($payload_length > 125) {
$data .= pack('C', $byte_2 | 0b01111110);
$data .= pack('n', $payload_length);
} else {
$data .= pack('C', $byte_2 | $payload_length);
}
// Handle masking
if ($masked) {
// generate a random mask:
$mask = '';
for ($i = 0; $i < 4; $i++) {
$mask .= chr(rand(0, 255));
}
$data .= $mask;
// Append payload to frame:
for ($i = 0; $i < $payload_length; $i++) {
$data .= $payload[$i] ^ $mask[$i % 4];
}
} else {
$data .= $payload;
}
$this->write($data);
$this->logger->debug("[connection] Pushed '{$opcode}' frame", [
'opcode' => $opcode,
'final' => $final,
'content-length' => strlen($payload),
]);
}
// Trigger auto response for frame
private function autoRespond(array $frame)
{
list ($final, $payload, $opcode, $masked) = $frame;
$payload_length = strlen($payload);
switch ($opcode) {
case 'ping':
// If we received a ping, respond with a pong
$this->logger->debug("[connection] Received 'ping', sending 'pong'.");
$message = $this->msg_factory->create('pong', $payload);
$this->pushMessage($message, $masked);
return [$final, $payload, $opcode, $masked];
case 'close':
// If we received close, possibly acknowledge and close connection
$status_bin = '';
$status = '';
if ($payload_length > 0) {
$status_bin = $payload[0] . $payload[1];
$status = current(unpack('n', $payload));
$this->close_status = $status;
}
// Get additional close message
if ($payload_length >= 2) {
$payload = substr($payload, 2);
}
$this->logger->debug("[connection] Received 'close', status: {$status}.");
if (!$this->is_closing) {
$ack = "{$status_bin}Close acknowledged: {$status}";
$message = $this->msg_factory->create('close', $ack);
$this->pushMessage($message, $masked);
} else {
$this->is_closing = false; // A close response, all done.
}
$this->disconnect();
return [$final, $payload, $opcode, $masked];
default:
return [$final, $payload, $opcode, $masked];
}
}
/* ---------- Stream I/O methods ------------------------------------------------- */
/**
* Close connection stream.
* @return bool
*/
public function disconnect(): bool
{
$this->logger->debug('Closing connection');
return fclose($this->stream);
}
/**
* If connected to stream.
* @return bool
*/
public function isConnected(): bool
{
return in_array($this->getType(), ['stream', 'persistent stream']);
}
/**
* Return type of connection.
* @return string|null Type of connection or null if invalid type.
*/
public function getType(): ?string
{
return get_resource_type($this->stream);
}
/**
* Get name of local socket, or null if not connected.
* @return string|null
*/
public function getName(): ?string
{
return stream_socket_get_name($this->stream, false);
}
/**
* Get name of remote socket, or null if not connected.
* @return string|null
*/
public function getRemoteName(): ?string
{
return stream_socket_get_name($this->stream, true);
}
/**
* Get meta data for connection.
* @return array
*/
public function getMeta(): array
{
return stream_get_meta_data($this->stream);
}
/**
* Returns current position of stream pointer.
* @return int
* @throws ConnectionException
*/
public function tell(): int
{
$tell = ftell($this->stream);
if ($tell === false) {
$this->throwException('Could not resolve stream pointer position');
}
return $tell;
}
/**
* If stream pointer is at end of file.
* @return bool
*/
public function eof(): int
{
return feof($this->stream);
}
/* ---------- Stream option methods ---------------------------------------------- */
/**
* Set time out on connection.
* @param int $seconds Timeout part in seconds
* @param int $microseconds Timeout part in microseconds
* @return bool
*/
public function setTimeout(int $seconds, int $microseconds = 0): bool
{
$this->logger->debug("Setting timeout {$seconds}:{$microseconds} seconds");
return stream_set_timeout($this->stream, $seconds, $microseconds);
}
/* ---------- Stream read/write methods ------------------------------------------ */
/**
* Read line from stream.
* @param int $length Maximum number of bytes to read
* @param string $ending Line delimiter
* @return string Read data
*/
public function getLine(int $length, string $ending): string
{
$line = stream_get_line($this->stream, $length, $ending);
if ($line === false) {
$this->throwException('Could not read from stream');
}
$read = strlen($line);
$this->logger->debug("Read {$read} bytes of line.");
return $line;
}
/**
* Read line from stream.
* @param int $length Maximum number of bytes to read
* @return string Read data
*/
public function gets(int $length): string
{
$line = fgets($this->stream, $length);
if ($line === false) {
$this->throwException('Could not read from stream');
}
$read = strlen($line);
$this->logger->debug("Read {$read} bytes of line.");
return $line;
}
/**
* Read characters from stream.
* @param int $length Maximum number of bytes to read
* @return string Read data
*/
public function read(string $length): string
{
$data = '';
while (strlen($data) < $length) {
$buffer = fread($this->stream, $length - strlen($data));
if (!$buffer) {
$meta = stream_get_meta_data($this->stream);
if (!empty($meta['timed_out'])) {
$message = 'Client read timeout';
$this->logger->error($message, $meta);
throw new TimeoutException($message, ConnectionException::TIMED_OUT, $meta);
}
}
if ($buffer === false) {
$read = strlen($data);
$this->throwException("Broken frame, read {$read} of stated {$length} bytes.");
}
if ($buffer === '') {
$this->throwException("Empty read; connection dead?");
}
$data .= $buffer;
$read = strlen($data);
$this->logger->debug("Read {$read} of {$length} bytes.");
}
return $data;
}
/**
* Write characters to stream.
* @param string $data Data to read
*/
public function write(string $data): void
{
$length = strlen($data);
$written = fwrite($this->stream, $data);
if ($written === false) {
$this->throwException("Failed to write {$length} bytes.");
}
if ($written < strlen($data)) {
$this->throwException("Could only write {$written} out of {$length} bytes.");
}
$this->logger->debug("Wrote {$written} of {$length} bytes.");
}
/* ---------- Internal helper methods -------------------------------------------- */
private function throwException(string $message, int $code = 0): void
{
$meta = ['closed' => true];
if ($this->isConnected()) {
$meta = $this->getMeta();
$this->disconnect();
if (!empty($meta['timed_out'])) {
$this->logger->error($message, $meta);
throw new TimeoutException($message, ConnectionException::TIMED_OUT, $meta);
}
if (!empty($meta['eof'])) {
$code = ConnectionException::EOF;
}
}
$this->logger->error($message, $meta);
throw new ConnectionException($message, $code, $meta);
}
}

View File

@ -1,33 +0,0 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket;
use Throwable;
class ConnectionException extends Exception
{
// Native codes in interval 0-106
public const TIMED_OUT = 1024;
public const EOF = 1025;
public const BAD_OPCODE = 1026;
private $data;
public function __construct(string $message, int $code = 0, array $data = [], Throwable $prev = null)
{
parent::__construct($message, $code, $prev);
$this->data = $data;
}
public function getData(): array
{
return $this->data;
}
}

View File

@ -1,7 +0,0 @@
<?php
namespace WebSocket;
class Exception extends \Exception
{
}

View File

@ -1,15 +0,0 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket\Message;
class Binary extends Message
{
protected $opcode = 'binary';
}

View File

@ -1,15 +0,0 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket\Message;
class Close extends Message
{
protected $opcode = 'close';
}

View File

@ -1,32 +0,0 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket\Message;
use WebSocket\BadOpcodeException;
class Factory
{
public function create(string $opcode, string $payload = ''): Message
{
switch ($opcode) {
case 'text':
return new Text($payload);
case 'binary':
return new Binary($payload);
case 'ping':
return new Ping($payload);
case 'pong':
return new Pong($payload);
case 'close':
return new Close($payload);
}
throw new BadOpcodeException("Invalid opcode '{$opcode}' provided");
}
}

View File

@ -1,74 +0,0 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket\Message;
use DateTime;
abstract class Message
{
protected $opcode;
protected $payload;
protected $timestamp;
public function __construct(string $payload = '')
{
$this->payload = $payload;
$this->timestamp = new DateTime();
}
public function getOpcode(): string
{
return $this->opcode;
}
public function getLength(): int
{
return strlen($this->payload);
}
public function getTimestamp(): DateTime
{
return $this->timestamp;
}
public function getContent(): string
{
return $this->payload;
}
public function setContent(string $payload = ''): void
{
$this->payload = $payload;
}
public function hasContent(): bool
{
return $this->payload != '';
}
public function __toString(): string
{
return get_class($this);
}
// Split messages into frames
public function getFrames(bool $masked = true, int $framesize = 4096): array
{
$frames = [];
$split = str_split($this->getContent(), $framesize) ?: [''];
foreach ($split as $payload) {
$frames[] = [false, $payload, 'continuation', $masked];
}
$frames[0][2] = $this->opcode;
$frames[array_key_last($frames)][0] = true;
return $frames;
}
}

View File

@ -1,15 +0,0 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket\Message;
class Ping extends Message
{
protected $opcode = 'ping';
}

View File

@ -1,15 +0,0 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket\Message;
class Pong extends Message
{
protected $opcode = 'pong';
}

View File

@ -1,15 +0,0 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket\Message;
class Text extends Message
{
protected $opcode = 'text';
}

View File

@ -1,22 +0,0 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket;
trait OpcodeTrait
{
private static $opcodes = [
'continuation' => 0,
'text' => 1,
'binary' => 2,
'close' => 8,
'ping' => 9,
'pong' => 10,
];
}

View File

@ -1,470 +0,0 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket;
use Closure;
use ErrorException;
use Phrity\Util\ErrorHandler;
use Psr\Log\{
LoggerAwareInterface,
LoggerAwareTrait,
LoggerInterface,
NullLogger
};
use Throwable;
use WebSocket\Message\Factory;
class Server implements LoggerAwareInterface
{
use LoggerAwareTrait; // Provides setLogger(LoggerInterface $logger)
use OpcodeTrait;
// Default options
protected static $default_options = [
'filter' => ['text', 'binary'],
'fragment_size' => 4096,
'logger' => null,
'port' => 8000,
'return_obj' => false,
'timeout' => null,
];
protected $port;
protected $listening;
protected $request;
protected $request_path;
private $connections = [];
private $options = [];
private $listen = false;
private $last_opcode;
/* ---------- Magic methods ------------------------------------------------------ */
/**
* @param array $options
* Associative array containing:
* - filter: Array of opcodes to handle. Default: ['text', 'binary'].
* - fragment_size: Set framgemnt size. Default: 4096
* - logger: PSR-3 compatible logger. Default NullLogger.
* - port: Chose port for listening. Default 8000.
* - return_obj: If receive() function return Message instance. Default false.
* - timeout: Set the socket timeout in seconds.
*/
public function __construct(array $options = [])
{
$this->options = array_merge(self::$default_options, [
'logger' => new NullLogger(),
], $options);
$this->port = $this->options['port'];
$this->setLogger($this->options['logger']);
$error = $errno = $errstr = null;
set_error_handler(function (int $severity, string $message, string $file, int $line) use (&$error) {
$this->logger->warning($message, ['severity' => $severity]);
$error = $message;
}, E_ALL);
do {
$this->listening = stream_socket_server("tcp://0.0.0.0:$this->port", $errno, $errstr);
} while ($this->listening === false && $this->port++ < 10000);
restore_error_handler();
if (!$this->listening) {
$error = "Could not open listening socket: {$errstr} ({$errno}) {$error}";
$this->logger->error($error);
throw new ConnectionException($error, (int)$errno);
}
$this->logger->info("Server listening to port {$this->port}");
}
/**
* Get string representation of instance.
* @return string String representation.
*/
public function __toString(): string
{
return sprintf(
"%s(%s)",
get_class($this),
$this->getName() ?: 'closed'
);
}
/* ---------- Server operations -------------------------------------------------- */
/**
* Accept a single incoming request.
* Note that this operation will block accepting additional requests.
* @return bool True if listening.
*/
public function accept(): bool
{
$this->disconnect();
return (bool)$this->listening;
}
/* ---------- Server option functions -------------------------------------------- */
/**
* Get current port.
* @return int port.
*/
public function getPort(): int
{
return $this->port;
}
/**
* Set timeout.
* @param int $timeout Timeout in seconds.
*/
public function setTimeout(int $timeout): void
{
$this->options['timeout'] = $timeout;
if (!$this->isConnected()) {
return;
}
foreach ($this->connections as $connection) {
$connection->setTimeout($timeout);
$connection->setOptions($this->options);
}
}
/**
* Set fragmentation size.
* @param int $fragment_size Fragment size in bytes.
* @return self.
*/
public function setFragmentSize(int $fragment_size): self
{
$this->options['fragment_size'] = $fragment_size;
foreach ($this->connections as $connection) {
$connection->setOptions($this->options);
}
return $this;
}
/**
* Get fragmentation size.
* @return int $fragment_size Fragment size in bytes.
*/
public function getFragmentSize(): int
{
return $this->options['fragment_size'];
}
/* ---------- Connection broadcast operations ------------------------------------ */
/**
* Broadcast text message to all conenctions.
* @param string $payload Content as string.
*/
public function text(string $payload): void
{
$this->send($payload);
}
/**
* Broadcast binary message to all conenctions.
* @param string $payload Content as binary string.
*/
public function binary(string $payload): void
{
$this->send($payload, 'binary');
}
/**
* Broadcast ping message to all conenctions.
* @param string $payload Optional text as string.
*/
public function ping(string $payload = ''): void
{
$this->send($payload, 'ping');
}
/**
* Broadcast pong message to all conenctions.
* @param string $payload Optional text as string.
*/
public function pong(string $payload = ''): void
{
$this->send($payload, 'pong');
}
/**
* Send message on all connections.
* @param string $payload Message to send.
* @param string $opcode Opcode to use, default: 'text'.
* @param bool $masked If message should be masked default: true.
*/
public function send(string $payload, string $opcode = 'text', bool $masked = true): void
{
if (!$this->isConnected()) {
$this->connect();
}
if (!in_array($opcode, array_keys(self::$opcodes))) {
$warning = "Bad opcode '{$opcode}'. Try 'text' or 'binary'.";
$this->logger->warning($warning);
throw new BadOpcodeException($warning);
}
$factory = new Factory();
$message = $factory->create($opcode, $payload);
foreach ($this->connections as $connection) {
$connection->pushMessage($message, $masked);
}
}
/**
* Close all connections.
* @param int $status Close status, default: 1000.
* @param string $message Close message, default: 'ttfn'.
*/
public function close(int $status = 1000, string $message = 'ttfn'): void
{
foreach ($this->connections as $connection) {
if ($connection->isConnected()) {
$connection->close($status, $message);
}
}
}
/**
* Disconnect all connections.
*/
public function disconnect(): void
{
foreach ($this->connections as $connection) {
if ($connection->isConnected()) {
$connection->disconnect();
}
}
$this->connections = [];
}
/**
* Receive message from single connection.
* Note that this operation will block reading and only read from first available connection.
* @return mixed Message, text or null depending on settings.
*/
public function receive()
{
$filter = $this->options['filter'];
$return_obj = $this->options['return_obj'];
if (!$this->isConnected()) {
$this->connect();
}
$connection = current($this->connections);
while (true) {
$message = $connection->pullMessage();
$opcode = $message->getOpcode();
if (in_array($opcode, $filter)) {
$this->last_opcode = $opcode;
$return = $return_obj ? $message : $message->getContent();
break;
} elseif ($opcode == 'close') {
$this->last_opcode = null;
$return = $return_obj ? $message : null;
break;
}
}
return $return;
}
/* ---------- Connection functions ----------------------------------------------- */
/**
* Get requested path from last connection.
* @return string Path.
*/
public function getPath(): string
{
return $this->request_path;
}
/**
* Get request from last connection.
* @return array Request.
*/
public function getRequest(): array
{
return $this->request;
}
/**
* Get headers from last connection.
* @return string|null Headers.
*/
public function getHeader($header): ?string
{
foreach ($this->request as $row) {
if (stripos($row, $header) !== false) {
list($headername, $headervalue) = explode(":", $row);
return trim($headervalue);
}
}
return null;
}
/**
* Get last received opcode.
* @return string|null Opcode.
*/
public function getLastOpcode(): ?string
{
return $this->last_opcode;
}
/**
* Get close status from single connection.
* @return int|null Close status.
*/
public function getCloseStatus(): ?int
{
return $this->connections ? current($this->connections)->getCloseStatus() : null;
}
/**
* If Server has active connections.
* @return bool True if active connection.
*/
public function isConnected(): bool
{
foreach ($this->connections as $connection) {
if ($connection->isConnected()) {
return true;
}
}
return false;
}
/**
* Get name of local socket from single connection.
* @return string|null Name of local socket.
*/
public function getName(): ?string
{
return $this->isConnected() ? current($this->connections)->getName() : null;
}
/**
* Get name of remote socket from single connection.
* @return string|null Name of remote socket.
*/
public function getRemoteName(): ?string
{
return $this->isConnected() ? current($this->connections)->getRemoteName() : null;
}
/**
* @deprecated Will be removed in future version.
*/
public function getPier(): ?string
{
trigger_error(
'getPier() is deprecated and will be removed in future version. Use getRemoteName() instead.',
E_USER_DEPRECATED
);
return $this->getRemoteName();
}
/* ---------- Helper functions --------------------------------------------------- */
// Connect when read/write operation is performed.
private function connect(): void
{
try {
$handler = new ErrorHandler();
$socket = $handler->with(function () {
if (isset($this->options['timeout'])) {
$socket = stream_socket_accept($this->listening, $this->options['timeout']);
} else {
$socket = stream_socket_accept($this->listening);
}
if (!$socket) {
throw new ErrorException('No socket');
}
return $socket;
});
} catch (ErrorException $e) {
$error = "Server failed to connect. {$e->getMessage()}";
$this->logger->error($error, ['severity' => $e->getSeverity()]);
throw new ConnectionException($error, 0, [], $e);
}
$connection = new Connection($socket, $this->options);
$connection->setLogger($this->logger);
if (isset($this->options['timeout'])) {
$connection->setTimeout($this->options['timeout']);
}
$this->logger->info("Client has connected to port {port}", [
'port' => $this->port,
'peer' => $connection->getRemoteName(),
]);
$this->performHandshake($connection);
$this->connections = ['*' => $connection];
}
// Perform upgrade handshake on new connections.
private function performHandshake(Connection $connection): void
{
$request = '';
do {
$buffer = $connection->getLine(1024, "\r\n");
$request .= $buffer . "\n";
$metadata = $connection->getMeta();
} while (!$connection->eof() && $metadata['unread_bytes'] > 0);
if (!preg_match('/GET (.*) HTTP\//mUi', $request, $matches)) {
$error = "No GET in request: {$request}";
$this->logger->error($error);
throw new ConnectionException($error);
}
$get_uri = trim($matches[1]);
$uri_parts = parse_url($get_uri);
$this->request = explode("\n", $request);
$this->request_path = $uri_parts['path'];
/// @todo Get query and fragment as well.
if (!preg_match('#Sec-WebSocket-Key:\s(.*)$#mUi', $request, $matches)) {
$error = "Client had no Key in upgrade request: {$request}";
$this->logger->error($error);
throw new ConnectionException($error);
}
$key = trim($matches[1]);
/// @todo Validate key length and base 64...
$response_key = base64_encode(pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
$header = "HTTP/1.1 101 Switching Protocols\r\n"
. "Upgrade: websocket\r\n"
. "Connection: Upgrade\r\n"
. "Sec-WebSocket-Accept: $response_key\r\n"
. "\r\n";
$connection->write($header);
$this->logger->debug("Handshake on {$get_uri}");
}
}

View File

@ -1,14 +0,0 @@
<?php
/**
* Copyright (C) 2014-2022 Textalk/Abicart and contributors.
*
* This file is part of Websocket PHP and is free software under the ISC License.
* License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
*/
namespace WebSocket;
class TimeoutException extends ConnectionException
{
}

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="tests/bootstrap.php" colors="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<coverage>
<include>
<directory suffix=".php">lib/</directory>
</include>
</coverage>
<testsuites>
<testsuite name="Unit tests">
<directory suffix=".php">tests</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@ -1,568 +0,0 @@
<?php
/**
* Test case for Client.
* Note that this test is performed by mocking socket/stream calls.
*/
declare(strict_types=1);
namespace WebSocket;
use ErrorException;
use Phrity\Net\Uri;
use Phrity\Util\ErrorHandler;
use PHPUnit\Framework\TestCase;
class ClientTest extends TestCase
{
public function setUp(): void
{
error_reporting(-1);
}
public function testClientMasked(): void
{
MockSocket::initialize('client.connect', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$client->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
$this->assertEquals(4096, $client->getFragmentSize());
MockSocket::initialize('send-receive', $this);
$client->send('Sending a message');
$message = $client->receive();
$this->assertTrue(MockSocket::isEmpty());
$this->assertEquals('text', $client->getLastOpcode());
MockSocket::initialize('client.close', $this);
$this->assertTrue($client->isConnected());
$this->assertNull($client->getCloseStatus());
$client->close();
$this->assertFalse($client->isConnected());
$this->assertEquals(1000, $client->getCloseStatus());
$this->assertTrue(MockSocket::isEmpty());
}
public function testDestruct(): void
{
MockSocket::initialize('client.connect', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$client->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('client.destruct', $this);
}
public function testClienExtendedUrl(): void
{
MockSocket::initialize('client.connect-extended', $this);
$client = new Client('ws://localhost:8000/my/mock/path?my_query=yes#my_fragment');
$client->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
}
public function testClientNoPath(): void
{
MockSocket::initialize('client.connect-root', $this);
$client = new Client('ws://localhost:8000');
$client->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
}
public function testClientRelativePath(): void
{
MockSocket::initialize('client.connect', $this);
$uri = new Uri('ws://localhost:8000');
$uri = $uri->withPath('my/mock/path');
$client = new Client($uri);
$client->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
}
public function testClientWsDefaultPort(): void
{
MockSocket::initialize('client.connect-default-port-ws', $this);
$uri = new Uri('ws://localhost');
$uri = $uri->withPath('my/mock/path');
$client = new Client($uri);
$client->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
}
public function testClientWssDefaultPort(): void
{
MockSocket::initialize('client.connect-default-port-wss', $this);
$uri = new Uri('wss://localhost');
$uri = $uri->withPath('my/mock/path');
$client = new Client($uri);
$client->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
}
public function testClientWithTimeout(): void
{
MockSocket::initialize('client.connect-timeout', $this);
$client = new Client('ws://localhost:8000/my/mock/path', ['timeout' => 300]);
$client->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
}
public function testClientWithContext(): void
{
MockSocket::initialize('client.connect-context', $this);
$client = new Client('ws://localhost:8000/my/mock/path', ['context' => '@mock-stream-context']);
$client->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
}
public function testClientAuthed(): void
{
MockSocket::initialize('client.connect-authed', $this);
$client = new Client('wss://usename:password@localhost:8000/my/mock/path');
$client->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
}
public function testWithHeaders(): void
{
MockSocket::initialize('client.connect-headers', $this);
$client = new Client('ws://localhost:8000/my/mock/path', [
'origin' => 'Origin header',
'headers' => ['Generic header' => 'Generic content'],
]);
$client->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
}
public function testPayload128(): void
{
MockSocket::initialize('client.connect', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$client->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
$payload = file_get_contents(__DIR__ . '/mock/payload.128.txt');
MockSocket::initialize('send-receive-128', $this);
$client->send($payload, 'text', false);
$message = $client->receive();
$this->assertEquals($payload, $message);
$this->assertTrue(MockSocket::isEmpty());
}
public function testPayload65536(): void
{
MockSocket::initialize('client.connect', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$client->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
$payload = file_get_contents(__DIR__ . '/mock/payload.65536.txt');
$client->setFragmentSize(65540);
MockSocket::initialize('send-receive-65536', $this);
$client->send($payload, 'text', false);
$message = $client->receive();
$this->assertEquals($payload, $message);
$this->assertTrue(MockSocket::isEmpty());
$this->assertEquals(65540, $client->getFragmentSize());
}
public function testMultiFragment(): void
{
MockSocket::initialize('client.connect', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$client->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('send-receive-multi-fragment', $this);
$client->setFragmentSize(8);
$client->send('Multi fragment test');
$message = $client->receive();
$this->assertEquals('Multi fragment test', $message);
$this->assertTrue(MockSocket::isEmpty());
$this->assertEquals(8, $client->getFragmentSize());
}
public function testPingPong(): void
{
MockSocket::initialize('client.connect', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$client->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('ping-pong', $this);
$client->send('Server ping', 'ping');
$client->send('', 'ping');
$message = $client->receive();
$this->assertEquals('Receiving a message', $message);
$this->assertEquals('text', $client->getLastOpcode());
$this->assertTrue(MockSocket::isEmpty());
}
public function testRemoteClose(): void
{
MockSocket::initialize('client.connect', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$client->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('close-remote', $this);
$message = $client->receive();
$this->assertNull($message);
$this->assertFalse($client->isConnected());
$this->assertEquals(17260, $client->getCloseStatus());
$this->assertNull($client->getLastOpcode());
$this->assertTrue(MockSocket::isEmpty());
}
public function testSetTimeout(): void
{
MockSocket::initialize('client.connect', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$client->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('config-timeout', $this);
$client->setTimeout(300);
$this->assertTrue($client->isConnected());
$this->assertTrue(MockSocket::isEmpty());
}
public function testReconnect(): void
{
MockSocket::initialize('client.connect', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$client->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('client.close', $this);
$this->assertTrue($client->isConnected());
$this->assertNull($client->getCloseStatus());
$client->close();
$this->assertFalse($client->isConnected());
$this->assertEquals(1000, $client->getCloseStatus());
$this->assertNull($client->getLastOpcode());
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('client.reconnect', $this);
$message = $client->receive();
$this->assertTrue($client->isConnected());
$this->assertTrue(MockSocket::isEmpty());
}
public function testPersistentConnection(): void
{
MockSocket::initialize('client.connect-persistent', $this);
$client = new Client('ws://localhost:8000/my/mock/path', ['persistent' => true]);
$client->send('Connect');
$client->disconnect();
$this->assertFalse($client->isConnected());
$this->assertTrue(MockSocket::isEmpty());
}
public function testFailedPersistentConnection(): void
{
MockSocket::initialize('client.connect-persistent-failure', $this);
$client = new Client('ws://localhost:8000/my/mock/path', ['persistent' => true]);
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionMessage('Could not resolve stream pointer position');
$client->send('Connect');
}
public function testBadScheme(): void
{
MockSocket::initialize('client.connect', $this);
$this->expectException('WebSocket\BadUriException');
$this->expectExceptionMessage("Invalid URI scheme, must be 'ws' or 'wss'.");
$client = new Client('bad://localhost:8000/my/mock/path');
}
public function testBadUri(): void
{
MockSocket::initialize('client.connect', $this);
$this->expectException('WebSocket\BadUriException');
$this->expectExceptionMessage("Invalid URI '--:this is not an uri:--' provided.");
$client = new Client('--:this is not an uri:--');
}
public function testInvalidUriType(): void
{
MockSocket::initialize('client.connect', $this);
$this->expectException('WebSocket\BadUriException');
$this->expectExceptionMessage("Provided URI must be a UriInterface or string.");
$client = new Client([]);
}
public function testUriInterface(): void
{
MockSocket::initialize('client.connect', $this);
$uri = new Uri('ws://localhost:8000/my/mock/path');
$client = new Client($uri);
$client->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
}
public function testBadStreamContext(): void
{
MockSocket::initialize('client.connect-bad-context', $this);
$client = new Client('ws://localhost:8000/my/mock/path', ['context' => 'BAD']);
$this->expectException('InvalidArgumentException');
$this->expectExceptionMessage('Stream context in $options[\'context\'] isn\'t a valid context');
$client->send('Connect');
}
public function testFailedConnection(): void
{
MockSocket::initialize('client.connect-failed', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(0);
$this->expectExceptionMessage('Could not open socket to "localhost:8000"');
$client->send('Connect');
}
public function testFailedConnectionWithError(): void
{
MockSocket::initialize('client.connect-error', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(0);
$this->expectExceptionMessage('Could not open socket to "localhost:8000"');
$client->send('Connect');
}
public function testBadStreamConnection(): void
{
MockSocket::initialize('client.connect-bad-stream', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(0);
$this->expectExceptionMessage('Invalid stream on "localhost:8000"');
$client->send('Connect');
}
public function testHandshakeFailure(): void
{
MockSocket::initialize('client.connect-handshake-failure', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(0);
$this->expectExceptionMessage('Client handshake error');
$client->send('Connect');
}
public function testInvalidUpgrade(): void
{
MockSocket::initialize('client.connect-invalid-upgrade', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(0);
$this->expectExceptionMessage('Connection to \'ws://localhost:8000/my/mock/path\' failed');
$client->send('Connect');
}
public function testInvalidKey(): void
{
MockSocket::initialize('client.connect-invalid-key', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(0);
$this->expectExceptionMessage('Server sent bad upgrade response');
$client->send('Connect');
}
public function testSendBadOpcode(): void
{
MockSocket::initialize('client.connect', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$client->send('Connect');
MockSocket::initialize('send-bad-opcode', $this);
$this->expectException('WebSocket\BadOpcodeException');
$this->expectExceptionMessage('Bad opcode \'bad\'. Try \'text\' or \'binary\'.');
$client->send('Bad Opcode', 'bad');
}
public function testRecieveBadOpcode(): void
{
MockSocket::initialize('client.connect', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$client->send('Connect');
MockSocket::initialize('receive-bad-opcode', $this);
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(1026);
$this->expectExceptionMessage('Bad opcode in websocket frame: 12');
$message = $client->receive();
}
public function testBrokenWrite(): void
{
MockSocket::initialize('client.connect', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$client->send('Connect');
MockSocket::initialize('send-broken-write', $this);
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(1025);
$this->expectExceptionMessage('Could only write 18 out of 22 bytes.');
$client->send('Failing to write');
}
public function testFailedWrite(): void
{
MockSocket::initialize('client.connect', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$client->send('Connect');
MockSocket::initialize('send-failed-write', $this);
$this->expectException('WebSocket\TimeoutException');
$this->expectExceptionCode(1024);
$this->expectExceptionMessage('Failed to write 22 bytes.');
$client->send('Failing to write');
}
public function testBrokenRead(): void
{
MockSocket::initialize('client.connect', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$client->send('Connect');
MockSocket::initialize('receive-broken-read', $this);
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(1025);
$this->expectExceptionMessage('Broken frame, read 0 of stated 2 bytes.');
$client->receive();
}
public function testHandshakeError(): void
{
MockSocket::initialize('client.connect-handshake-error', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(1024);
$this->expectExceptionMessage('Client handshake error');
$client->send('Connect');
}
public function testReadTimeout(): void
{
MockSocket::initialize('client.connect', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$client->send('Connect');
MockSocket::initialize('receive-client-timeout', $this);
$this->expectException('WebSocket\TimeoutException');
$this->expectExceptionCode(1024);
$this->expectExceptionMessage('Client read timeout');
$client->receive();
}
public function testEmptyRead(): void
{
MockSocket::initialize('client.connect', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$client->send('Connect');
MockSocket::initialize('receive-empty-read', $this);
$this->expectException('WebSocket\TimeoutException');
$this->expectExceptionCode(1024);
$this->expectExceptionMessage('Empty read; connection dead?');
$client->receive();
}
public function testFrameFragmentation(): void
{
MockSocket::initialize('client.connect', $this);
$client = new Client(
'ws://localhost:8000/my/mock/path',
['filter' => ['text', 'binary', 'pong', 'close']]
);
$client->send('Connect');
MockSocket::initialize('receive-fragmentation', $this);
$message = $client->receive();
$this->assertEquals('Server ping', $message);
$this->assertEquals('pong', $client->getLastOpcode());
$message = $client->receive();
$this->assertEquals('Multi fragment test', $message);
$this->assertEquals('text', $client->getLastOpcode());
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('close-remote', $this);
$message = $client->receive();
$this->assertEquals('Closing', $message);
$this->assertTrue(MockSocket::isEmpty());
$this->assertFalse($client->isConnected());
$this->assertEquals(17260, $client->getCloseStatus());
$this->assertEquals('close', $client->getLastOpcode());
}
public function testMessageFragmentation(): void
{
MockSocket::initialize('client.connect', $this);
$client = new Client(
'ws://localhost:8000/my/mock/path',
['filter' => ['text', 'binary', 'pong', 'close'], 'return_obj' => true]
);
$client->send('Connect');
MockSocket::initialize('receive-fragmentation', $this);
$message = $client->receive();
$this->assertInstanceOf('WebSocket\Message\Message', $message);
$this->assertInstanceOf('WebSocket\Message\Pong', $message);
$this->assertEquals('Server ping', $message->getContent());
$this->assertEquals('pong', $message->getOpcode());
$message = $client->receive();
$this->assertInstanceOf('WebSocket\Message\Message', $message);
$this->assertInstanceOf('WebSocket\Message\Text', $message);
$this->assertEquals('Multi fragment test', $message->getContent());
$this->assertEquals('text', $message->getOpcode());
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('close-remote', $this);
$message = $client->receive();
$this->assertInstanceOf('WebSocket\Message\Message', $message);
$this->assertInstanceOf('WebSocket\Message\Close', $message);
$this->assertEquals('Closing', $message->getContent());
$this->assertEquals('close', $message->getOpcode());
}
public function testConvenicanceMethods(): void
{
MockSocket::initialize('client.connect', $this);
$client = new Client('ws://localhost:8000/my/mock/path');
$this->assertNull($client->getName());
$this->assertNull($client->getRemoteName());
$this->assertEquals('WebSocket\Client(closed)', "{$client}");
$client->text('Connect');
MockSocket::initialize('send-convenicance', $this);
$client->binary(base64_encode('Binary content'));
$client->ping();
$client->pong();
$this->assertEquals('127.0.0.1:12345', $client->getName());
$this->assertEquals('127.0.0.1:8000', $client->getRemoteName());
$this->assertEquals('WebSocket\Client(127.0.0.1:12345)', "{$client}");
}
public function testUnconnectedClient(): void
{
$client = new Client('ws://localhost:8000/my/mock/path');
$this->assertFalse($client->isConnected());
$client->setTimeout(30);
$client->close();
$this->assertFalse($client->isConnected());
$this->assertNull($client->getName());
$this->assertNull($client->getRemoteName());
$this->assertNull($client->getCloseStatus());
}
public function testDeprecated(): void
{
$client = new Client('ws://localhost:8000/my/mock/path');
(new ErrorHandler())->withAll(function () use ($client) {
$this->assertNull($client->getPier());
}, function ($exceptions, $result) {
$this->assertEquals(
'getPier() is deprecated and will be removed in future version. Use getRemoteName() instead.',
$exceptions[0]->getMessage()
);
}, E_USER_DEPRECATED);
}
}

View File

@ -1,51 +0,0 @@
<?php
/**
* Test case for Exceptions.
*/
declare(strict_types=1);
namespace WebSocket;
use PHPUnit\Framework\TestCase;
use Throwable;
class ExceptionTest extends TestCase
{
public function setUp(): void
{
error_reporting(-1);
}
public function testConnectionException(): void
{
try {
throw new ConnectionException(
'An error message',
ConnectionException::EOF,
['test' => 'with data'],
new TimeoutException(
'Nested exception',
ConnectionException::TIMED_OUT
)
);
} catch (Throwable $e) {
}
$this->assertInstanceOf('WebSocket\ConnectionException', $e);
$this->assertInstanceOf('WebSocket\Exception', $e);
$this->assertInstanceOf('Exception', $e);
$this->assertInstanceOf('Throwable', $e);
$this->assertEquals('An error message', $e->getMessage());
$this->assertEquals(1025, $e->getCode());
$this->assertEquals(['test' => 'with data'], $e->getData());
$p = $e->getPrevious();
$this->assertInstanceOf('WebSocket\TimeoutException', $p);
$this->assertInstanceOf('WebSocket\ConnectionException', $p);
$this->assertEquals('Nested exception', $p->getMessage());
$this->assertEquals(1024, $p->getCode());
$this->assertEquals([], $p->getData());
}
}

View File

@ -1,60 +0,0 @@
<?php
/**
* Test case for Message subsection.
*/
declare(strict_types=1);
namespace WebSocket;
use PHPUnit\Framework\TestCase;
use WebSocket\Message\Factory;
use WebSocket\Message\Text;
class MessageTest extends TestCase
{
public function setUp(): void
{
error_reporting(-1);
}
public function testFactory(): void
{
$factory = new Factory();
$message = $factory->create('text', 'Some content');
$this->assertInstanceOf('WebSocket\Message\Text', $message);
$message = $factory->create('binary', 'Some content');
$this->assertInstanceOf('WebSocket\Message\Binary', $message);
$message = $factory->create('ping', 'Some content');
$this->assertInstanceOf('WebSocket\Message\Ping', $message);
$message = $factory->create('pong', 'Some content');
$this->assertInstanceOf('WebSocket\Message\Pong', $message);
$message = $factory->create('close', 'Some content');
$this->assertInstanceOf('WebSocket\Message\Close', $message);
}
public function testMessage()
{
$message = new Text('Some content');
$this->assertInstanceOf('WebSocket\Message\Message', $message);
$this->assertInstanceOf('WebSocket\Message\Text', $message);
$this->assertEquals('Some content', $message->getContent());
$this->assertEquals('text', $message->getOpcode());
$this->assertEquals(12, $message->getLength());
$this->assertTrue($message->hasContent());
$this->assertInstanceOf('DateTime', $message->getTimestamp());
$message->setContent('');
$this->assertEquals(0, $message->getLength());
$this->assertFalse($message->hasContent());
$this->assertEquals('WebSocket\Message\Text', "{$message}");
}
public function testBadOpcode()
{
$factory = new Factory();
$this->expectException('WebSocket\BadOpcodeException');
$this->expectExceptionMessage("Invalid opcode 'invalid' provided");
$message = $factory->create('invalid', 'Some content');
}
}

View File

@ -1,28 +0,0 @@
# Testing
Unit tests with [PHPUnit](https://phpunit.readthedocs.io/).
## How to run
To run all test, run in console.
```
make test
```
## Continuous integration
GitHub Actions are run on PHP versions `7.4`, `8.0`, `8.1` and `8.2`.
Code coverage by [Coveralls](https://coveralls.io/github/Textalk/websocket-php).
## Test strategy
Test set up overloads various stream and socket functions,
and use "scripts" to define and mock input/output of these functions.
This set up negates the dependency on running servers,
and allow testing various errors that might occur.

View File

@ -1,511 +0,0 @@
<?php
/**
* Test case for Server.
* Note that this test is performed by mocking socket/stream calls.
*/
declare(strict_types=1);
namespace WebSocket;
use ErrorException;
use Phrity\Util\ErrorHandler;
use PHPUnit\Framework\TestCase;
class ServerTest extends TestCase
{
public function setUp(): void
{
error_reporting(-1);
}
public function testServerMasked(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('server.accept', $this);
$server->accept();
$server->send('Connect');
$this->assertEquals(8000, $server->getPort());
$this->assertEquals('/my/mock/path', $server->getPath());
$this->assertTrue($server->isConnected());
$this->assertEquals(4096, $server->getFragmentSize());
$this->assertNull($server->getCloseStatus());
$this->assertEquals([
'GET /my/mock/path HTTP/1.1',
'host: localhost:8000',
'user-agent: websocket-client-php',
'connection: Upgrade',
'upgrade: websocket',
'sec-websocket-key: cktLWXhUdDQ2OXF0ZCFqOQ==',
'sec-websocket-version: 13',
'',
'',
], $server->getRequest());
$this->assertEquals('websocket-client-php', $server->getHeader('USER-AGENT'));
$this->assertNull($server->getHeader('no such header'));
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('send-receive', $this);
$server->send('Sending a message');
$message = $server->receive();
$this->assertEquals('Receiving a message', $message);
$this->assertTrue(MockSocket::isEmpty());
$this->assertNull($server->getCloseStatus());
$this->assertEquals('text', $server->getLastOpcode());
MockSocket::initialize('server.close', $this);
$server->close();
$this->assertFalse($server->isConnected());
$this->assertEquals(1000, $server->getCloseStatus());
$this->assertTrue(MockSocket::isEmpty());
$server->close(); // Already closed
}
public function testDestruct(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
MockSocket::initialize('server.accept-destruct', $this);
$server->accept();
$message = $server->receive();
}
public function testServerWithTimeout(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server(['timeout' => 300]);
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('server.accept-timeout', $this);
$server->accept();
$server->send('Connect');
$this->assertTrue(MockSocket::isEmpty());
}
public function testPayload128(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('server.accept', $this);
$server->accept();
$server->send('Connect');
$this->assertTrue($server->isConnected());
$this->assertTrue(MockSocket::isEmpty());
$payload = file_get_contents(__DIR__ . '/mock/payload.128.txt');
MockSocket::initialize('send-receive-128', $this);
$server->send($payload, 'text', false);
$message = $server->receive();
$this->assertEquals($payload, $message);
$this->assertTrue(MockSocket::isEmpty());
}
public function testPayload65536(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('server.accept', $this);
$server->accept();
$server->send('Connect');
$this->assertTrue($server->isConnected());
$this->assertTrue(MockSocket::isEmpty());
$payload = file_get_contents(__DIR__ . '/mock/payload.65536.txt');
$server->setFragmentSize(65540);
MockSocket::initialize('send-receive-65536', $this);
$server->send($payload, 'text', false);
$message = $server->receive();
$this->assertEquals($payload, $message);
$this->assertTrue(MockSocket::isEmpty());
}
public function testMultiFragment(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('server.accept', $this);
$server->accept();
$server->send('Connect');
$this->assertTrue($server->isConnected());
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('send-receive-multi-fragment', $this);
$server->setFragmentSize(8);
$server->send('Multi fragment test');
$message = $server->receive();
$this->assertEquals('Multi fragment test', $message);
$this->assertTrue(MockSocket::isEmpty());
}
public function testPingPong(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('server.accept', $this);
$server->accept();
$server->send('Connect');
$this->assertTrue($server->isConnected());
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('ping-pong', $this);
$server->send('Server ping', 'ping');
$server->send('', 'ping');
$message = $server->receive();
$this->assertEquals('Receiving a message', $message);
$this->assertEquals('text', $server->getLastOpcode());
$this->assertTrue(MockSocket::isEmpty());
}
public function testRemoteClose(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('server.accept', $this);
$server->accept();
$server->send('Connect');
$this->assertTrue($server->isConnected());
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('close-remote', $this);
$message = $server->receive();
$this->assertEquals('', $message);
$this->assertTrue(MockSocket::isEmpty());
$this->assertFalse($server->isConnected());
$this->assertEquals(17260, $server->getCloseStatus());
$this->assertNull($server->getLastOpcode());
}
public function testSetTimeout(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('server.accept', $this);
$server->accept();
$server->send('Connect');
$this->assertTrue($server->isConnected());
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('config-timeout', $this);
$server->setTimeout(300);
$this->assertTrue($server->isConnected());
$this->assertTrue(MockSocket::isEmpty());
}
public function testFailedSocketServer(): void
{
MockSocket::initialize('server.construct-failed-socket-server', $this);
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(0);
$this->expectExceptionMessage('Could not open listening socket:');
$server = new Server(['port' => 9999]);
}
public function testFailedSocketServerWithError(): void
{
MockSocket::initialize('server.construct-error-socket-server', $this);
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(0);
$this->expectExceptionMessage('Could not open listening socket:');
$server = new Server(['port' => 9999]);
}
public function testFailedConnect(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
MockSocket::initialize('server.accept-failed-connect', $this);
$server->accept();
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(0);
$this->expectExceptionMessage('Server failed to connect');
$server->send('Connect');
}
public function testFailedConnectWithError(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
MockSocket::initialize('server.accept-error-connect', $this);
$server->accept();
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(0);
$this->expectExceptionMessage('Server failed to connect');
$server->send('Connect');
}
public function testFailedConnectTimeout(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server(['timeout' => 300]);
MockSocket::initialize('server.accept-failed-connect', $this);
$server->accept();
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(0);
$this->expectExceptionMessage('Server failed to connect');
$server->send('Connect');
}
public function testFailedHttp(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
MockSocket::initialize('server.accept-failed-http', $this);
$server->accept();
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(0);
$this->expectExceptionMessage('No GET in request');
$server->send('Connect');
}
public function testFailedWsKey(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
MockSocket::initialize('server.accept-failed-ws-key', $this);
$server->accept();
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(0);
$this->expectExceptionMessage('Client had no Key in upgrade request');
$server->send('Connect');
}
public function testSendBadOpcode(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
MockSocket::initialize('server.accept', $this);
$server->accept();
$server->send('Connect');
$this->expectException('WebSocket\BadOpcodeException');
$this->expectExceptionCode(0);
$this->expectExceptionMessage('Bad opcode \'bad\'. Try \'text\' or \'binary\'.');
$server->send('Bad Opcode', 'bad');
}
public function testRecieveBadOpcode(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
MockSocket::initialize('server.accept', $this);
$server->accept();
$server->send('Connect');
MockSocket::initialize('receive-bad-opcode', $this);
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(1026);
$this->expectExceptionMessage('Bad opcode in websocket frame: 12');
$message = $server->receive();
}
public function testBrokenWrite(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
MockSocket::initialize('server.accept', $this);
$server->accept();
$server->send('Connect');
MockSocket::initialize('send-broken-write', $this);
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(1025);
$this->expectExceptionMessage('Could only write 18 out of 22 bytes.');
$server->send('Failing to write');
}
public function testFailedWrite(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
MockSocket::initialize('server.accept', $this);
$server->accept();
$server->send('Connect');
MockSocket::initialize('send-failed-write', $this);
$this->expectException('WebSocket\TimeoutException');
$this->expectExceptionCode(1024);
$this->expectExceptionMessage('Failed to write 22 bytes.');
$server->send('Failing to write');
}
public function testBrokenRead(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
MockSocket::initialize('server.accept', $this);
$server->accept();
$server->send('Connect');
MockSocket::initialize('receive-broken-read', $this);
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(1025);
$this->expectExceptionMessage('Broken frame, read 0 of stated 2 bytes.');
$server->receive();
}
public function testEmptyRead(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
MockSocket::initialize('server.accept', $this);
$server->accept();
$server->send('Connect');
MockSocket::initialize('receive-empty-read', $this);
$this->expectException('WebSocket\TimeoutException');
$this->expectExceptionCode(1024);
$this->expectExceptionMessage('Empty read; connection dead?');
$server->receive();
}
public function testFrameFragmentation(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server(['filter' => ['text', 'binary', 'pong', 'close']]);
MockSocket::initialize('server.accept', $this);
$server->accept();
$server->send('Connect');
MockSocket::initialize('receive-fragmentation', $this);
$message = $server->receive();
$this->assertEquals('Server ping', $message);
$this->assertEquals('pong', $server->getLastOpcode());
$message = $server->receive();
$this->assertEquals('Multi fragment test', $message);
$this->assertEquals('text', $server->getLastOpcode());
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('close-remote', $this);
$message = $server->receive();
$this->assertEquals('Closing', $message);
$this->assertTrue(MockSocket::isEmpty());
$this->assertFalse($server->isConnected());
$this->assertEquals(17260, $server->getCloseStatus());
$this->assertEquals('close', $server->getLastOpcode());
}
public function testMessageFragmentation(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server(['filter' => ['text', 'binary', 'pong', 'close'], 'return_obj' => true]);
MockSocket::initialize('server.accept', $this);
$server->accept();
$server->send('Connect');
MockSocket::initialize('receive-fragmentation', $this);
$message = $server->receive();
$this->assertInstanceOf('WebSocket\Message\Message', $message);
$this->assertInstanceOf('WebSocket\Message\Pong', $message);
$this->assertEquals('Server ping', $message->getContent());
$this->assertEquals('pong', $message->getOpcode());
$message = $server->receive();
$this->assertInstanceOf('WebSocket\Message\Message', $message);
$this->assertInstanceOf('WebSocket\Message\Text', $message);
$this->assertEquals('Multi fragment test', $message->getContent());
$this->assertEquals('text', $message->getOpcode());
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('close-remote', $this);
$message = $server->receive();
$this->assertInstanceOf('WebSocket\Message\Message', $message);
$this->assertInstanceOf('WebSocket\Message\Close', $message);
$this->assertEquals('Closing', $message->getContent());
$this->assertEquals('close', $message->getOpcode());
}
public function testConvenicanceMethods(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
$this->assertNull($server->getName());
$this->assertNull($server->getRemoteName());
$this->assertEquals('WebSocket\Server(closed)', "{$server}");
MockSocket::initialize('server.accept', $this);
$server->accept();
$server->text('Connect');
MockSocket::initialize('send-convenicance', $this);
$server->binary(base64_encode('Binary content'));
$server->ping();
$server->pong();
$this->assertEquals('127.0.0.1:12345', $server->getName());
$this->assertEquals('127.0.0.1:8000', $server->getRemoteName());
$this->assertEquals('WebSocket\Server(127.0.0.1:12345)', "{$server}");
$this->assertTrue(MockSocket::isEmpty());
}
public function testUnconnectedServer(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
$this->assertFalse($server->isConnected());
$server->setTimeout(30);
$server->close();
$this->assertFalse($server->isConnected());
$this->assertNull($server->getName());
$this->assertNull($server->getRemoteName());
$this->assertNull($server->getCloseStatus());
$this->assertTrue(MockSocket::isEmpty());
}
public function testFailedHandshake(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('server.accept-failed-handshake', $this);
$server->accept();
$this->expectException('WebSocket\ConnectionException');
$this->expectExceptionCode(0);
$this->expectExceptionMessage('Could not read from stream');
$server->send('Connect');
$this->assertFalse($server->isConnected());
$this->assertTrue(MockSocket::isEmpty());
}
public function testServerDisconnect(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('server.accept', $this);
$server->accept();
$server->send('Connect');
$this->assertTrue($server->isConnected());
$this->assertTrue(MockSocket::isEmpty());
MockSocket::initialize('server.disconnect', $this);
$server->disconnect();
$this->assertFalse($server->isConnected());
$this->assertTrue(MockSocket::isEmpty());
}
public function testDeprecated(): void
{
MockSocket::initialize('server.construct', $this);
$server = new Server();
$this->assertTrue(MockSocket::isEmpty());
(new ErrorHandler())->withAll(function () use ($server) {
$this->assertNull($server->getPier());
}, function ($exceptions, $result) {
$this->assertEquals(
'getPier() is deprecated and will be removed in future version. Use getRemoteName() instead.',
$exceptions[0]->getMessage()
);
}, E_USER_DEPRECATED);
}
}

View File

@ -1,6 +0,0 @@
<?php
namespace WebSocket;
require dirname(__DIR__) . '/vendor/autoload.php';
require __DIR__ . '/mock/mock-socket.php';

View File

@ -1,34 +0,0 @@
<?php
/**
* Simple echo logger (only available when running in dev environment)
*/
namespace WebSocket;
class EchoLog implements \Psr\Log\LoggerInterface
{
use \Psr\Log\LoggerTrait;
public function log($level, $message, array $context = [])
{
$message = $this->interpolate($message, $context);
$context_string = empty($context) ? '' : json_encode($context);
echo str_pad($level, 8) . " | {$message} {$context_string}\n";
}
public function interpolate($message, array $context = [])
{
// Build a replacement array with braces around the context keys
$replace = [];
foreach ($context as $key => $val) {
// Check that the value can be cast to string
if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) {
$replace['{' . $key . '}'] = $val;
}
}
// Interpolate replacement values into the message and return
return strtr($message, $replace);
}
}

Some files were not shown because too many files have changed in this diff Show More