Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
/examples/ export-ignore
/phpunit.xml.dist export-ignore
/phpunit.xml.legacy export-ignore
/phpstan.neon.dist export-ignore
/tests/ export-ignore
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,28 @@ jobs:
coverage: pcov
- run: composer install
- run: vendor/bin/phpunit --coverage-text

PHPStan:
name: PHPStan (PHP ${{ matrix.php }} on ubuntu-24.04)
runs-on: ubuntu-24.04
strategy:
matrix:
php:
- 8.5
- 8.4
- 8.3
- 8.2
- 8.1
- 8.0
- 7.4
- 7.3
- 7.2
- 7.1
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: none
- run: composer install
- run: vendor/bin/phpstan analyse --no-progress
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ connections for [ReactPHP](https://reactphp.org/).
> The upcoming v3 release will be the way forward for this package. However,
> we will still actively support v1 for those not yet on the latest version.
> See also [installation instructions](#install) for more details.
> Custom implementations should follow the [v3 type declaration upgrade guide](UPGRADE.md).

The socket library provides re-usable interfaces for a socket-layer
server and client based on the [`EventLoop`](https://github.com/reactphp/event-loop)
Expand Down Expand Up @@ -1532,6 +1533,17 @@ If you do not want to run these, they can simply be skipped like this:
vendor/bin/phpunit --exclude-group internet
```

To check the source code with PHPStan at its maximum level, run:

```bash
vendor/bin/phpstan analyse
```

Composer installs a PHPStan release compatible with your PHP version:
PHPStan 1.4 on PHP 7.1, PHPStan 1.12 on PHP 7.2–7.3, and PHPStan 2
on PHP 7.4+. Each runs at its maximum level and analyses compatibility with PHP 7.1.
CI runs this check on the same PHP and operating system matrix as PHPUnit.

## License

MIT, see [LICENSE file](LICENSE).
29 changes: 29 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Upgrading to Socket v3

## Native type declarations

Socket APIs now declare parameter and return types. Custom implementations and
subclasses must use compatible signatures:

| Interface | Method |
| --- | --- |
| `ConnectorInterface` | `connect(string $uri): React\Promise\PromiseInterface` |
| `ServerInterface` | `getAddress(): ?string` |
| `ServerInterface` | `pause(): void`, `resume(): void`, `close(): void` |
| `ConnectionInterface` | `getRemoteAddress(): ?string`, `getLocalAddress(): ?string` |

Connector promises still resolve with `ConnectionInterface`. Addresses may still
be `null` when a connection or server has closed or its address is unknown.

Pass URI and path strings to connectors and servers, a `float` timeout to
`TimeoutConnector`, and an `int` or `null` connection limit and a `bool` pause flag
to `LimitingServer`. `TcpServer` still accepts a port-only string, such as `'8080'`.
Callers using `declare(strict_types=1)` must convert integer ports to strings.

Values incompatible with the declarations now raise `TypeError` immediately.
Malformed URI strings retain the existing exception or rejected-promise behavior.
Scalar coercion continues to follow PHP's normal `strict_types` rules.

Methods inherited from the current Stream and EventEmitter dependencies retain
compatible parameter signatures. PHPDoc describes resource handles and union
types that PHP 7.1 cannot express natively.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"react/stream": "^1.4"
},
"require-dev": {
"phpstan/phpstan": "^1.4.10 || ^2.1",
"phpunit/phpunit": "^9.6 || ^8.5 || ^7.5",
"react/async": "^4.3 || ^3",
"react/promise-stream": "^1.4",
Expand Down
7 changes: 7 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
parameters:
level: max
paths:
- src
phpVersion: 70100
# Retain runtime guards for older supported Promise implementations.
treatPhpDocTypesAsCertain: false
35 changes: 22 additions & 13 deletions src/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class Connection extends EventEmitter implements ConnectionInterface
* Internal flag whether this is a Unix domain socket (UDS) connection
*
* @internal
* @var bool
*/
public $unix = false;

Expand All @@ -33,14 +34,20 @@ class Connection extends EventEmitter implements ConnectionInterface
* `tls://` scheme for encrypted connections instead of `tcp://`.
*
* @internal
* @var bool
*/
public $encryptionEnabled = false;

/** @internal */
/**
* @internal
* @var resource
*/
public $stream;

/** @var DuplexResourceStream */
private $input;

/** @param resource $resource */
public function __construct($resource, LoopInterface $loop)
{
// Legacy PHP < 7.3.3 (and PHP < 7.2.15) suffers from a bug where feof()
Expand Down Expand Up @@ -78,49 +85,50 @@ public function __construct($resource, LoopInterface $loop)
$this->input->on('close', [$this, 'close']);
}

public function isReadable()
public function isReadable(): bool
{
return $this->input->isReadable();
}

public function isWritable()
public function isWritable(): bool
{
return $this->input->isWritable();
}

public function pause()
public function pause(): void
{
$this->input->pause();
}

public function resume()
public function resume(): void
{
$this->input->resume();
}

public function pipe(WritableStreamInterface $dest, array $options = [])
/** @param array{end?: bool} $options */
public function pipe(WritableStreamInterface $dest, array $options = []): WritableStreamInterface
{
return $this->input->pipe($dest, $options);
}

public function write($data)
public function write($data): bool
{
return $this->input->write($data);
}

public function end($data = null)
public function end($data = null): void
{
$this->input->end($data);
}

public function close()
public function close(): void
{
$this->input->close();
$this->handleClose();
$this->removeAllListeners();
}

public function handleClose()
public function handleClose(): void
{
if (!\is_resource($this->stream)) {
return;
Expand All @@ -132,7 +140,7 @@ public function handleClose()
@\stream_socket_shutdown($this->stream, \STREAM_SHUT_RDWR);
}

public function getRemoteAddress()
public function getRemoteAddress(): ?string
{
if (!\is_resource($this->stream)) {
return null;
Expand All @@ -141,7 +149,7 @@ public function getRemoteAddress()
return $this->parseAddress(\stream_socket_get_name($this->stream, true));
}

public function getLocalAddress()
public function getLocalAddress(): ?string
{
if (!\is_resource($this->stream)) {
return null;
Expand All @@ -150,7 +158,8 @@ public function getLocalAddress()
return $this->parseAddress(\stream_socket_get_name($this->stream, false));
}

private function parseAddress($address)
/** @param string|false $address */
private function parseAddress($address): ?string
{
if ($address === false) {
return null;
Expand Down
4 changes: 2 additions & 2 deletions src/ConnectionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ interface ConnectionInterface extends DuplexStreamInterface
*
* @return ?string remote address (URI) or null if unknown
*/
public function getRemoteAddress();
public function getRemoteAddress(): ?string;

/**
* Returns the full local address (full URI with scheme, IP and port) where this connection has been established with
Expand Down Expand Up @@ -115,5 +115,5 @@ public function getRemoteAddress();
* @return ?string local address (URI) or null if unknown
* @see self::getRemoteAddress()
*/
public function getLocalAddress();
public function getLocalAddress(): ?string;
}
12 changes: 7 additions & 5 deletions src/Connector.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use React\Dns\Resolver\Factory as DnsFactory;
use React\Dns\Resolver\ResolverInterface;
use React\EventLoop\LoopInterface;
use React\Promise\PromiseInterface;
use function React\Promise\reject;

/**
Expand All @@ -25,6 +26,7 @@
*/
final class Connector implements ConnectorInterface
{
/** @var array<string, ConnectorInterface> */
private $connectors = [];

/**
Expand All @@ -46,7 +48,7 @@ final class Connector implements ConnectorInterface
* This value SHOULD NOT be given unless you're sure you want to explicitly use a
* given event loop instance.
*
* @param array $context
* @param array{tcp?: bool|array<string, mixed>|ConnectorInterface, tls?: bool|array<string, mixed>|ConnectorInterface, unix?: bool|ConnectorInterface, dns?: bool|string|DnsConfig|ResolverInterface, timeout?: bool|float, happy_eyeballs?: bool} $context
* @param ?LoopInterface $loop
* @throws \InvalidArgumentException for invalid arguments
*/
Expand Down Expand Up @@ -86,7 +88,7 @@ public function __construct(array $context = [], ?LoopInterface $loop = null)
// try to load nameservers from system config or default to Google's public DNS
$config = DnsConfig::loadSystemConfigBlocking();
if (!$config->nameservers) {
$config->nameservers[] = '8.8.8.8'; // @codeCoverageIgnore
$config->nameservers = ['8.8.8.8']; // @codeCoverageIgnore
}
}

Expand Down Expand Up @@ -146,7 +148,7 @@ public function __construct(array $context = [], ?LoopInterface $loop = null)
}
}

public function connect($uri)
public function connect(string $uri): PromiseInterface
{
$scheme = 'tcp';
if (\strpos($uri, '://') !== false) {
Expand All @@ -167,13 +169,13 @@ public function connect($uri)
/**
* [internal] Builds on URI from the given URI parts and ip address with original hostname as query
*
* @param array $parts
* @param array{scheme?: string, host?: string, port?: int, path?: string, query?: string, fragment?: string} $parts
* @param string $host
* @param string $ip
* @return string
* @internal
*/
public static function uri(array $parts, $host, $ip)
public static function uri(array $parts, string $host, string $ip): string
{
$uri = '';

Expand Down
4 changes: 3 additions & 1 deletion src/ConnectorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace React\Socket;

use React\Promise\PromiseInterface;

/**
* The `ConnectorInterface` is responsible for providing an interface for
* establishing streaming connections, such as a normal TCP/IP connection.
Expand Down Expand Up @@ -55,5 +57,5 @@ interface ConnectorInterface
* Resolves with a `ConnectionInterface` on success or rejects with an `Exception` on error.
* @see ConnectionInterface
*/
public function connect($uri);
public function connect(string $uri): PromiseInterface;
}
19 changes: 13 additions & 6 deletions src/DnsConnector.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

final class DnsConnector implements ConnectorInterface
{
/** @var ConnectorInterface */
private $connector;
/** @var ResolverInterface */
private $resolver;

public function __construct(ConnectorInterface $connector, ResolverInterface $resolver)
Expand All @@ -18,13 +20,13 @@ public function __construct(ConnectorInterface $connector, ResolverInterface $re
$this->resolver = $resolver;
}

public function connect($uri)
public function connect(string $uri): PromiseInterface
{
$original = $uri;
if (\strpos($uri, '://') === false) {
$uri = 'tcp://' . $uri;
$parts = \parse_url($uri);
if (isset($parts['scheme'])) {
if ($parts !== false && isset($parts['scheme'])) {
unset($parts['scheme']);
}
} else {
Expand All @@ -46,17 +48,20 @@ public function connect($uri)
}

$promise = $this->resolver->resolve($host);
/** @var ?string $resolved */
$resolved = null;

return new Promise(
/** @var Promise<ConnectionInterface> $result */
$result = new Promise(
function ($resolve, $reject) use (&$promise, &$resolved, $uri, $host, $parts) {
// resolve/reject with result of DNS lookup
/** @var PromiseInterface<string> $promise */
$promise->then(function ($ip) use (&$promise, &$resolved, $uri, $host, $parts) {
$resolved = $ip;

return $promise = $this->connector->connect(
Connector::uri($parts, $host, $ip)
)->then(null, function (\Exception $e) use ($uri) {
)->then(null, function (\Throwable $e) use ($uri) {
if ($e instanceof \RuntimeException) {
$message = \preg_replace('/^(Connection to [^ ]+)[&?]hostname=[^ &]+/', '$1', $e->getMessage());
$e = new \RuntimeException(
Expand All @@ -71,7 +76,7 @@ function ($resolve, $reject) use (&$promise, &$resolved, $uri, $host, $parts) {
if (\PHP_VERSION_ID < 80100) {
$r->setAccessible(true);
}
$trace = $r->getValue($e);
$trace = $e->getTrace();

// Exception trace arguments are not available on some PHP 7.4 installs
// @codeCoverageIgnoreStart
Expand Down Expand Up @@ -105,7 +110,7 @@ function ($_, $reject) use (&$promise, &$resolved, $uri) {
}

// (try to) cancel pending DNS lookup / connection attempt
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
if ($promise instanceof PromiseInterface && \is_callable([$promise, 'cancel'])) {
// overwrite callback arguments for PHP7+ only, so they do not show
// up in the Exception trace and do not cause a possible cyclic reference.
$_ = $reject = null;
Expand All @@ -115,5 +120,7 @@ function ($_, $reject) use (&$promise, &$resolved, $uri) {
}
}
);

return $result;
}
}
Loading
Loading