Update psalm baseline

Signed-off-by: GitHub <noreply@github.com>
This commit is contained in:
Nextcloud-PR-Bot 2020-10-30 04:18:38 +00:00 committed by GitHub
parent 11fca45e4c
commit ac8a5ca18c
2533 changed files with 363506 additions and 39 deletions

View File

@ -1713,7 +1713,6 @@
<file src="apps/files_sharing/lib/Controller/ShareController.php">
<InvalidArgument occurrences="2">
<code>$files_list</code>
<code>'Share is read-only'</code>
</InvalidArgument>
<InvalidScalarArgument occurrences="3">
<code>$freeSpace</code>
@ -2476,18 +2475,6 @@
<code>Resource</code>
</UndefinedDocblockClass>
</file>
<file src="apps/user_ldap/lib/LDAPProviderFactory.php">
<ImplementedReturnTypeMismatch occurrences="1">
<code>OCP\LDAP\ILDAPProvider</code>
</ImplementedReturnTypeMismatch>
<InvalidReturnStatement occurrences="1"/>
<InvalidReturnType occurrences="1">
<code>OCP\LDAP\ILDAPProvider</code>
</InvalidReturnType>
<UndefinedDocblockClass occurrences="1">
<code>OCP\LDAP\ILDAPProvider</code>
</UndefinedDocblockClass>
</file>
<file src="apps/user_ldap/lib/LogWrapper.php">
<InvalidReturnType occurrences="1">
<code>bool</code>
@ -3213,11 +3200,6 @@
<code>$default</code>
</MoreSpecificImplementedParamType>
</file>
<file src="lib/private/AppFramework/Services/InitialState.php">
<ImplementedParamTypeMismatch occurrences="1">
<code>$closure</code>
</ImplementedParamTypeMismatch>
</file>
<file src="lib/private/Archive/TAR.php">
<FalsableReturnStatement occurrences="1">
<code>false</code>
@ -3342,21 +3324,12 @@
<ImplementedReturnTypeMismatch occurrences="1">
<code>Color</code>
</ImplementedReturnTypeMismatch>
<ImplicitToStringCast occurrences="1">
<code>$avatar</code>
</ImplicitToStringCast>
<InvalidNullableReturnType occurrences="1">
<code>string|boolean</code>
</InvalidNullableReturnType>
<InvalidReturnStatement occurrences="1">
<code>$finalPalette[$this-&gt;hashToInt($hash, $steps * 3)]</code>
</InvalidReturnStatement>
<InvalidReturnType occurrences="1">
<code>Color</code>
</InvalidReturnType>
<NullableReturnStatement occurrences="1">
<code>$image-&gt;data()</code>
</NullableReturnStatement>
</file>
<file src="lib/private/Avatar/GuestAvatar.php">
<ImplementedReturnTypeMismatch occurrences="1">
@ -5747,14 +5720,6 @@
<code>$this-&gt;resources</code>
</InvalidPropertyAssignmentValue>
</file>
<file src="lib/public/AppFramework/Services/IInitialState.php">
<MismatchingDocblockParamType occurrences="1">
<code>Closure</code>
</MismatchingDocblockParamType>
<UndefinedDocblockClass occurrences="1">
<code>Closure</code>
</UndefinedDocblockClass>
</file>
<file src="lib/public/BackgroundJob/TimedJob.php">
<MoreSpecificImplementedParamType occurrences="1">
<code>$jobList</code>

View File

@ -0,0 +1,23 @@
The MIT License (MIT)
Copyright (c) 2015-2019 amphp
Copyright (c) 2016 PHP Asynchronous 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

@ -0,0 +1,74 @@
{
"name": "amphp/amp",
"homepage": "http://amphp.org/amp",
"description": "A non-blocking concurrency framework for PHP applications.",
"keywords": [
"async",
"asynchronous",
"concurrency",
"promise",
"awaitable",
"future",
"non-blocking",
"event",
"event-loop"
],
"license": "MIT",
"authors": [
{
"name": "Daniel Lowrey",
"email": "rdlowrey@php.net"
},
{
"name": "Aaron Piotrowski",
"email": "aaron@trowski.com"
},
{
"name": "Bob Weinand",
"email": "bobwei9@hotmail.com"
},
{
"name": "Niklas Keller",
"email": "me@kelunik.com"
}
],
"require": {
"php": ">=7"
},
"require-dev": {
"ext-json": "*",
"amphp/phpunit-util": "^1",
"amphp/php-cs-fixer-config": "dev-master",
"react/promise": "^2",
"phpunit/phpunit": "^6.0.9 | ^7",
"psalm/phar": "^3.11@dev",
"jetbrains/phpstorm-stubs": "^2019.3"
},
"autoload": {
"psr-4": {
"Amp\\": "lib"
},
"files": [
"lib/functions.php",
"lib/Internal/functions.php"
]
},
"autoload-dev": {
"psr-4": {
"Amp\\Test\\": "test"
}
},
"support": {
"issues": "https://github.com/amphp/amp/issues",
"irc": "irc://irc.freenode.org/amphp"
},
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
}
},
"scripts": {
"test": "@php -dzend.assertions=1 -dassert.exception=1 ./vendor/bin/phpunit",
"code-style": "@php ./vendor/bin/php-cs-fixer fix"
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace Amp;
// @codeCoverageIgnoreStart
if (\PHP_VERSION_ID < 70100) {
/** @psalm-suppress DuplicateClass */
trait CallableMaker
{
/** @var \ReflectionClass */
private static $__reflectionClass;
/** @var \ReflectionMethod[] */
private static $__reflectionMethods = [];
/**
* Creates a callable from a protected or private instance method that may be invoked by callers requiring a
* publicly invokable callback.
*
* @param string $method Instance method name.
*
* @return callable
*
* @psalm-suppress MixedInferredReturnType
*/
private function callableFromInstanceMethod(string $method): callable
{
if (!isset(self::$__reflectionMethods[$method])) {
if (self::$__reflectionClass === null) {
self::$__reflectionClass = new \ReflectionClass(self::class);
}
self::$__reflectionMethods[$method] = self::$__reflectionClass->getMethod($method);
}
return self::$__reflectionMethods[$method]->getClosure($this);
}
/**
* Creates a callable from a protected or private static method that may be invoked by methods requiring a
* publicly invokable callback.
*
* @param string $method Static method name.
*
* @return callable
*
* @psalm-suppress MixedInferredReturnType
*/
private static function callableFromStaticMethod(string $method): callable
{
if (!isset(self::$__reflectionMethods[$method])) {
if (self::$__reflectionClass === null) {
self::$__reflectionClass = new \ReflectionClass(self::class);
}
self::$__reflectionMethods[$method] = self::$__reflectionClass->getMethod($method);
}
return self::$__reflectionMethods[$method]->getClosure();
}
}
} else {
/** @psalm-suppress DuplicateClass */
trait CallableMaker
{
/**
* @deprecated Use \Closure::fromCallable() instead of this method in PHP 7.1.
*/
private function callableFromInstanceMethod(string $method): callable
{
return \Closure::fromCallable([$this, $method]);
}
/**
* @deprecated Use \Closure::fromCallable() instead of this method in PHP 7.1.
*/
private static function callableFromStaticMethod(string $method): callable
{
return \Closure::fromCallable([self::class, $method]);
}
}
} // @codeCoverageIgnoreEnd

View File

@ -0,0 +1,49 @@
<?php
namespace Amp;
/**
* Cancellation tokens are simple objects that allow registering handlers to subscribe to cancellation requests.
*/
interface CancellationToken
{
/**
* Subscribes a new handler to be invoked on a cancellation request.
*
* This handler might be invoked immediately in case the token has already been cancelled. Returned generators will
* automatically be run as coroutines. Any unhandled exceptions will be throw into the event loop.
*
* @param callable(CancelledException) $callback Callback to be invoked on a cancellation request. Will receive a
* `CancelledException` as first argument that may be used to fail the operation's promise.
*
* @return string Identifier that can be used to cancel the subscription.
*/
public function subscribe(callable $callback): string;
/**
* Unsubscribes a previously registered handler.
*
* The handler will no longer be called as long as this method isn't invoked from a subscribed callback.
*
* @param string $id
*
* @return void
*/
public function unsubscribe(string $id);
/**
* Returns whether cancellation has been requested yet.
*
* @return bool
*/
public function isRequested(): bool;
/**
* Throws the `CancelledException` if cancellation has been requested, otherwise does nothing.
*
* @return void
*
* @throws CancelledException
*/
public function throwIfRequested();
}

View File

@ -0,0 +1,163 @@
<?php
namespace Amp;
use React\Promise\PromiseInterface as ReactPromise;
use function Amp\Promise\rethrow;
/**
* A cancellation token source provides a mechanism to cancel operations.
*
* Cancellation of operation works by creating a cancellation token source and passing the corresponding token when
* starting the operation. To cancel the operation, invoke `CancellationTokenSource::cancel()`.
*
* Any operation can decide what to do on a cancellation request, it has "don't care" semantics. An operation SHOULD be
* aborted, but MAY continue. Example: A DNS client might continue to receive and cache the response, as the query has
* been sent anyway. An HTTP client would usually close a connection, but might not do so in case a response is close to
* be fully received to reuse the connection.
*
* **Example**
*
* ```php
* $tokenSource = new CancellationTokenSource;
* $token = $tokenSource->getToken();
*
* $response = yield $httpClient->request("https://example.com/stream", $token);
* $responseBody = $response->getBody();
*
* while (($chunk = yield $response->read()) !== null) {
* // consume $chunk
*
* if ($noLongerInterested) {
* $cancellationTokenSource->cancel();
* break;
* }
* }
* ```
*
* @see CancellationToken
* @see CancelledException
*/
final class CancellationTokenSource
{
/** @var CancellationToken */
private $token;
/** @var callable|null */
private $onCancel;
public function __construct()
{
$onCancel = null;
$this->token = new class($onCancel) implements CancellationToken {
/** @var string */
private $nextId = "a";
/** @var callable[] */
private $callbacks = [];
/** @var \Throwable|null */
private $exception;
/**
* @param mixed $onCancel
* @param-out callable $onCancel
*/
public function __construct(&$onCancel)
{
/** @psalm-suppress MissingClosureReturnType We still support PHP 7.0 */
$onCancel = function (\Throwable $exception) {
$this->exception = $exception;
$callbacks = $this->callbacks;
$this->callbacks = [];
foreach ($callbacks as $callback) {
$this->invokeCallback($callback);
}
};
}
/**
* @param callable $callback
*
* @return void
*/
private function invokeCallback(callable $callback)
{
// No type declaration to prevent exception outside the try!
try {
/** @var mixed $result */
$result = $callback($this->exception);
if ($result instanceof \Generator) {
/** @psalm-var \Generator<mixed, Promise|ReactPromise|(Promise|ReactPromise)[], mixed, mixed> $result */
$result = new Coroutine($result);
}
if ($result instanceof Promise || $result instanceof ReactPromise) {
rethrow($result);
}
} catch (\Throwable $exception) {
Loop::defer(static function () use ($exception) {
throw $exception;
});
}
}
public function subscribe(callable $callback): string
{
$id = $this->nextId++;
if ($this->exception) {
$this->invokeCallback($callback);
} else {
$this->callbacks[$id] = $callback;
}
return $id;
}
public function unsubscribe(string $id)
{
unset($this->callbacks[$id]);
}
public function isRequested(): bool
{
return isset($this->exception);
}
public function throwIfRequested()
{
if (isset($this->exception)) {
throw $this->exception;
}
}
};
$this->onCancel = $onCancel;
}
public function getToken(): CancellationToken
{
return $this->token;
}
/**
* @param \Throwable|null $previous Exception to be used as the previous exception to CancelledException.
*
* @return void
*/
public function cancel(\Throwable $previous = null)
{
if ($this->onCancel === null) {
return;
}
$onCancel = $this->onCancel;
$this->onCancel = null;
$onCancel(new CancelledException($previous));
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Amp;
/**
* Will be thrown in case an operation is cancelled.
*
* @see CancellationToken
* @see CancellationTokenSource
*/
class CancelledException extends \Exception
{
public function __construct(\Throwable $previous = null)
{
parent::__construct("The operation was cancelled", 0, $previous);
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace Amp;
final class CombinedCancellationToken implements CancellationToken
{
/** @var array{0: CancellationToken, 1: string}[] */
private $tokens = [];
/** @var string */
private $nextId = "a";
/** @var callable[] */
private $callbacks = [];
/** @var CancelledException|null */
private $exception;
public function __construct(CancellationToken ...$tokens)
{
foreach ($tokens as $token) {
$id = $token->subscribe(function (CancelledException $exception) {
$this->exception = $exception;
$callbacks = $this->callbacks;
$this->callbacks = [];
foreach ($callbacks as $callback) {
asyncCall($callback, $this->exception);
}
});
$this->tokens[] = [$token, $id];
}
}
public function __destruct()
{
foreach ($this->tokens as list($token, $id)) {
/** @var CancellationToken $token */
$token->unsubscribe($id);
}
}
/** @inheritdoc */
public function subscribe(callable $callback): string
{
$id = $this->nextId++;
if ($this->exception) {
asyncCall($callback, $this->exception);
} else {
$this->callbacks[$id] = $callback;
}
return $id;
}
/** @inheritdoc */
public function unsubscribe(string $id)
{
unset($this->callbacks[$id]);
}
/** @inheritdoc */
public function isRequested(): bool
{
foreach ($this->tokens as list($token)) {
if ($token->isRequested()) {
return true;
}
}
return false;
}
/** @inheritdoc */
public function throwIfRequested()
{
foreach ($this->tokens as list($token)) {
$token->throwIfRequested();
}
}
}

View File

@ -0,0 +1,160 @@
<?php
namespace Amp;
use React\Promise\PromiseInterface as ReactPromise;
/**
* Creates a promise from a generator function yielding promises.
*
* When a promise is yielded, execution of the generator is interrupted until the promise is resolved. A success
* value is sent into the generator, while a failure reason is thrown into the generator. Using a coroutine,
* asynchronous code can be written without callbacks and be structured like synchronous code.
*
* @template-covariant TReturn
* @template-implements Promise<TReturn>
*/
final class Coroutine implements Promise
{
use Internal\Placeholder;
/**
* Attempts to transform the non-promise yielded from the generator into a promise, otherwise returns an instance
* `Amp\Failure` failed with an instance of `Amp\InvalidYieldError`.
*
* @param mixed $yielded Non-promise yielded from generator.
* @param \Generator $generator No type for performance, we already know the type.
*
* @return Promise
*/
private static function transform($yielded, $generator): Promise
{
$exception = null; // initialize here, see https://github.com/vimeo/psalm/issues/2951
try {
if (\is_array($yielded)) {
return Promise\all($yielded);
}
if ($yielded instanceof ReactPromise) {
return Promise\adapt($yielded);
}
// No match, continue to returning Failure below.
} catch (\Throwable $exception) {
// Conversion to promise failed, fall-through to returning Failure below.
}
return new Failure(new InvalidYieldError(
$generator,
\sprintf(
"Unexpected yield; Expected an instance of %s or %s or an array of such instances",
Promise::class,
ReactPromise::class
),
$exception
));
}
/**
* @param \Generator $generator
* @psalm-param \Generator<mixed,Promise|ReactPromise|array<array-key,
* Promise|ReactPromise>,mixed,Promise<TReturn>|ReactPromise|TReturn> $generator
*/
public function __construct(\Generator $generator)
{
try {
$yielded = $generator->current();
if (!$yielded instanceof Promise) {
if (!$generator->valid()) {
$this->resolve($generator->getReturn());
return;
}
$yielded = self::transform($yielded, $generator);
}
} catch (\Throwable $exception) {
$this->fail($exception);
return;
}
/**
* @param \Throwable|null $e Exception to be thrown into the generator.
* @param mixed $v Value to be sent into the generator.
*
* @return void
*
* @psalm-suppress MissingClosureParamType
* @psalm-suppress MissingClosureReturnType
*/
$onResolve = function (\Throwable $e = null, $v) use ($generator, &$onResolve) {
/** @var bool $immediate Used to control iterative coroutine continuation. */
static $immediate = true;
/** @var \Throwable|null $exception Promise failure reason when executing next coroutine step, null at all other times. */
static $exception;
/** @var mixed $value Promise success value when executing next coroutine step, null at all other times. */
static $value;
$exception = $e;
/** @psalm-suppress MixedAssignment */
$value = $v;
if (!$immediate) {
$immediate = true;
return;
}
try {
try {
do {
if ($exception) {
// Throw exception at current execution point.
$yielded = $generator->throw($exception);
} else {
// Send the new value and execute to next yield statement.
$yielded = $generator->send($value);
}
if (!$yielded instanceof Promise) {
if (!$generator->valid()) {
$this->resolve($generator->getReturn());
$onResolve = null;
return;
}
$yielded = self::transform($yielded, $generator);
}
$immediate = false;
$yielded->onResolve($onResolve);
} while ($immediate);
$immediate = true;
} catch (\Throwable $exception) {
$this->fail($exception);
$onResolve = null;
} finally {
$exception = null;
$value = null;
}
} catch (\Throwable $e) {
Loop::defer(static function () use ($e) {
throw $e;
});
}
};
try {
$yielded->onResolve($onResolve);
unset($generator, $yielded, $onResolve);
} catch (\Throwable $e) {
Loop::defer(static function () use ($e) {
throw $e;
});
}
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace Amp;
/**
* Deferred is a container for a promise that is resolved using the resolve() and fail() methods of this object.
* The contained promise may be accessed using the promise() method. This object should not be part of a public
* API, but used internally to create and resolve a promise.
*
* @template TValue
*/
final class Deferred
{
/** @var Promise<TValue> Has public resolve and fail methods. */
private $resolver;
/** @var Promise<TValue> Hides placeholder methods */
private $promise;
public function __construct()
{
$this->resolver = new class implements Promise {
use Internal\Placeholder {
resolve as public;
fail as public;
}
};
$this->promise = new Internal\PrivatePromise($this->resolver);
}
/**
* @return Promise<TValue>
*/
public function promise(): Promise
{
return $this->promise;
}
/**
* Fulfill the promise with the given value.
*
* @param mixed $value
*
* @psalm-param TValue|Promise<TValue> $value
*
* @return void
*/
public function resolve($value = null)
{
/** @psalm-suppress UndefinedInterfaceMethod */
$this->resolver->resolve($value);
}
/**
* Fails the promise the the given reason.
*
* @param \Throwable $reason
*
* @return void
*/
public function fail(\Throwable $reason)
{
/** @psalm-suppress UndefinedInterfaceMethod */
$this->resolver->fail($reason);
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace Amp;
/**
* Creates a promise that resolves itself with a given value after a number of milliseconds.
*
* @template-covariant TReturn
* @template-implements Promise<TReturn>
*/
final class Delayed implements Promise
{
use Internal\Placeholder;
/** @var string|null Event loop watcher identifier. */
private $watcher;
/**
* @param int $time Milliseconds before succeeding the promise.
* @param TReturn $value Succeed the promise with this value.
*/
public function __construct(int $time, $value = null)
{
$this->watcher = Loop::delay($time, function () use ($value) {
$this->watcher = null;
$this->resolve($value);
});
}
/**
* References the internal watcher in the event loop, keeping the loop running while this promise is pending.
*
* @return self
*/
public function reference(): self
{
if ($this->watcher !== null) {
Loop::reference($this->watcher);
}
return $this;
}
/**
* Unreferences the internal watcher in the event loop, allowing the loop to stop while this promise is pending if
* no other events are pending in the loop.
*
* @return self
*/
public function unreference(): self
{
if ($this->watcher !== null) {
Loop::unreference($this->watcher);
}
return $this;
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace Amp;
/**
* Emitter is a container for an iterator that can emit values using the emit() method and completed using the
* complete() and fail() methods of this object. The contained iterator may be accessed using the iterate()
* method. This object should not be part of a public API, but used internally to create and emit values to an
* iterator.
*
* @template TValue
*/
final class Emitter
{
/** @var Iterator<TValue> Has public emit, complete, and fail methods. */
private $emitter;
/** @var Iterator<TValue> Hides producer methods. */
private $iterator;
public function __construct()
{
$this->emitter = new class implements Iterator {
use Internal\Producer {
emit as public;
complete as public;
fail as public;
}
};
$this->iterator = new Internal\PrivateIterator($this->emitter);
}
/**
* @return Iterator
* @psalm-return Iterator<TValue>
*/
public function iterate(): Iterator
{
return $this->iterator;
}
/**
* Emits a value to the iterator.
*
* @param mixed $value
*
* @psalm-param TValue $value
*
* @return Promise
* @psalm-return Promise<null>
* @psalm-suppress MixedInferredReturnType
* @psalm-suppress MixedReturnStatement
*/
public function emit($value): Promise
{
/** @psalm-suppress UndefinedInterfaceMethod */
return $this->emitter->emit($value);
}
/**
* Completes the iterator.
*
* @return void
*/
public function complete()
{
/** @psalm-suppress UndefinedInterfaceMethod */
$this->emitter->complete();
}
/**
* Fails the iterator with the given reason.
*
* @param \Throwable $reason
*
* @return void
*/
public function fail(\Throwable $reason)
{
/** @psalm-suppress UndefinedInterfaceMethod */
$this->emitter->fail($reason);
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Amp;
use React\Promise\PromiseInterface as ReactPromise;
/**
* Creates a failed promise using the given exception.
*
* @template-covariant TValue
* @template-implements Promise<TValue>
*/
final class Failure implements Promise
{
/** @var \Throwable $exception */
private $exception;
/**
* @param \Throwable $exception Rejection reason.
*/
public function __construct(\Throwable $exception)
{
$this->exception = $exception;
}
/**
* {@inheritdoc}
*/
public function onResolve(callable $onResolved)
{
try {
/** @var mixed $result */
$result = $onResolved($this->exception, null);
if ($result === null) {
return;
}
if ($result instanceof \Generator) {
$result = new Coroutine($result);
}
if ($result instanceof Promise || $result instanceof ReactPromise) {
Promise\rethrow($result);
}
} catch (\Throwable $exception) {
Loop::defer(static function () use ($exception) {
throw $exception;
});
}
}
}

View File

@ -0,0 +1,179 @@
<?php
namespace Amp\Internal;
use Amp\Coroutine;
use Amp\Failure;
use Amp\Loop;
use Amp\Promise;
use React\Promise\PromiseInterface as ReactPromise;
/**
* Trait used by Promise implementations. Do not use this trait in your code, instead compose your class from one of
* the available classes implementing \Amp\Promise.
*
* @internal
*/
trait Placeholder
{
/** @var bool */
private $resolved = false;
/** @var mixed */
private $result;
/** @var ResolutionQueue|null|callable(\Throwable|null, mixed): (Promise|\React\Promise\PromiseInterface|\Generator<mixed,
* Promise|\React\Promise\PromiseInterface|array<array-key, Promise|\React\Promise\PromiseInterface>, mixed,
* mixed>|null)|callable(\Throwable|null, mixed): void */
private $onResolved;
/** @var null|array */
private $resolutionTrace;
/**
* @inheritdoc
*/
public function onResolve(callable $onResolved)
{
if ($this->resolved) {
if ($this->result instanceof Promise) {
$this->result->onResolve($onResolved);
return;
}
try {
/** @var mixed $result */
$result = $onResolved(null, $this->result);
if ($result === null) {
return;
}
if ($result instanceof \Generator) {
$result = new Coroutine($result);
}
if ($result instanceof Promise || $result instanceof ReactPromise) {
Promise\rethrow($result);
}
} catch (\Throwable $exception) {
Loop::defer(static function () use ($exception) {
throw $exception;
});
}
return;
}
if (null === $this->onResolved) {
$this->onResolved = $onResolved;
return;
}
if (!$this->onResolved instanceof ResolutionQueue) {
/** @psalm-suppress InternalClass */
$this->onResolved = new ResolutionQueue($this->onResolved);
}
/** @psalm-suppress InternalMethod */
$this->onResolved->push($onResolved);
}
public function __destruct()
{
try {
$this->result = null;
} catch (\Throwable $e) {
Loop::defer(static function () use ($e) {
throw $e;
});
}
}
/**
* @param mixed $value
*
* @return void
*
* @throws \Error Thrown if the promise has already been resolved.
*/
private function resolve($value = null)
{
if ($this->resolved) {
$message = "Promise has already been resolved";
if (isset($this->resolutionTrace)) {
$trace = formatStacktrace($this->resolutionTrace);
$message .= ". Previous resolution trace:\n\n{$trace}\n\n";
} else {
// @codeCoverageIgnoreStart
$message .= ", define environment variable AMP_DEBUG or const AMP_DEBUG = true and enable assertions "
. "for a stacktrace of the previous resolution.";
// @codeCoverageIgnoreEnd
}
throw new \Error($message);
}
\assert((function () {
$env = \getenv("AMP_DEBUG") ?: "0";
if (($env !== "0" && $env !== "false") || (\defined("AMP_DEBUG") && \AMP_DEBUG)) {
$trace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS);
\array_shift($trace); // remove current closure
$this->resolutionTrace = $trace;
}
return true;
})());
if ($value instanceof ReactPromise) {
$value = Promise\adapt($value);
}
$this->resolved = true;
$this->result = $value;
if ($this->onResolved === null) {
return;
}
$onResolved = $this->onResolved;
$this->onResolved = null;
if ($this->result instanceof Promise) {
$this->result->onResolve($onResolved);
return;
}
try {
/** @var mixed $result */
$result = $onResolved(null, $this->result);
$onResolved = null; // allow garbage collection of $onResolved, to catch any exceptions from destructors
if ($result === null) {
return;
}
if ($result instanceof \Generator) {
$result = new Coroutine($result);
}
if ($result instanceof Promise || $result instanceof ReactPromise) {
Promise\rethrow($result);
}
} catch (\Throwable $exception) {
Loop::defer(static function () use ($exception) {
throw $exception;
});
}
}
/**
* @param \Throwable $reason Failure reason.
*
* @return void
*/
private function fail(\Throwable $reason)
{
$this->resolve(new Failure($reason));
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace Amp\Internal;
use Amp\Iterator;
use Amp\Promise;
/**
* Wraps an Iterator instance that has public methods to emit, complete, and fail into an object that only allows
* access to the public API methods.
*
* @template-covariant TValue
* @template-implements Iterator<TValue>
*/
final class PrivateIterator implements Iterator
{
/** @var Iterator<TValue> */
private $iterator;
/**
* @param Iterator $iterator
*
* @psalm-param Iterator<TValue> $iterator
*/
public function __construct(Iterator $iterator)
{
$this->iterator = $iterator;
}
/**
* @return Promise<bool>
*/
public function advance(): Promise
{
return $this->iterator->advance();
}
/**
* @psalm-return TValue
*/
public function getCurrent()
{
return $this->iterator->getCurrent();
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Amp\Internal;
use Amp\Promise;
/**
* Wraps a Promise instance that has public methods to resolve and fail the promise into an object that only allows
* access to the public API methods.
*/
final class PrivatePromise implements Promise
{
/** @var Promise */
private $promise;
public function __construct(Promise $promise)
{
$this->promise = $promise;
}
public function onResolve(callable $onResolved)
{
$this->promise->onResolve($onResolved);
}
}

View File

@ -0,0 +1,212 @@
<?php
namespace Amp\Internal;
use Amp\Deferred;
use Amp\Failure;
use Amp\Promise;
use Amp\Success;
use React\Promise\PromiseInterface as ReactPromise;
/**
* Trait used by Iterator implementations. Do not use this trait in your code, instead compose your class from one of
* the available classes implementing \Amp\Iterator.
* Note that it is the responsibility of the user of this trait to ensure that listeners have a chance to listen first
* before emitting values.
*
* @internal
* @template-covariant TValue
*/
trait Producer
{
/** @var Promise|null */
private $complete;
/** @var mixed[] */
private $values = [];
/** @var Deferred[] */
private $backPressure = [];
/** @var int */
private $consumePosition = -1;
/** @var int */
private $emitPosition = -1;
/** @var Deferred|null */
private $waiting;
/** @var null|array */
private $resolutionTrace;
/**
* {@inheritdoc}
*
* @return Promise<bool>
*/
public function advance(): Promise
{
if ($this->waiting !== null) {
throw new \Error("The prior promise returned must resolve before invoking this method again");
}
unset($this->values[$this->consumePosition]);
$position = ++$this->consumePosition;
if (\array_key_exists($position, $this->values)) {
\assert(isset($this->backPressure[$position]));
$deferred = $this->backPressure[$position];
unset($this->backPressure[$position]);
$deferred->resolve();
return new Success(true);
}
if ($this->complete) {
return $this->complete;
}
$this->waiting = new Deferred;
return $this->waiting->promise();
}
/**
* {@inheritdoc}
*
* @return TValue
*/
public function getCurrent()
{
if (empty($this->values) && $this->complete) {
throw new \Error("The iterator has completed");
}
if (!\array_key_exists($this->consumePosition, $this->values)) {
throw new \Error("Promise returned from advance() must resolve before calling this method");
}
return $this->values[$this->consumePosition];
}
/**
* Emits a value from the iterator. The returned promise is resolved once the emitted value has been consumed.
*
* @param mixed $value
*
* @return Promise
* @psalm-return Promise<null>
*
* @throws \Error If the iterator has completed.
*/
private function emit($value): Promise
{
if ($this->complete) {
throw new \Error("Iterators cannot emit values after calling complete");
}
if ($value instanceof ReactPromise) {
$value = Promise\adapt($value);
}
if ($value instanceof Promise) {
$deferred = new Deferred;
$value->onResolve(function ($e, $v) use ($deferred) {
if ($this->complete) {
$deferred->fail(
new \Error("The iterator was completed before the promise result could be emitted")
);
return;
}
if ($e) {
$this->fail($e);
$deferred->fail($e);
return;
}
$deferred->resolve($this->emit($v));
});
return $deferred->promise();
}
$position = ++$this->emitPosition;
$this->values[$position] = $value;
if ($this->waiting !== null) {
$waiting = $this->waiting;
$this->waiting = null;
$waiting->resolve(true);
return new Success; // Consumer was already waiting for a new value, so back-pressure is unnecessary.
}
$this->backPressure[$position] = $pressure = new Deferred;
return $pressure->promise();
}
/**
* Completes the iterator.
*
* @return void
*
* @throws \Error If the iterator has already been completed.
*/
private function complete()
{
if ($this->complete) {
$message = "Iterator has already been completed";
if (isset($this->resolutionTrace)) {
$trace = formatStacktrace($this->resolutionTrace);
$message .= ". Previous completion trace:\n\n{$trace}\n\n";
} else {
// @codeCoverageIgnoreStart
$message .= ", define environment variable AMP_DEBUG or const AMP_DEBUG = true and enable assertions "
. "for a stacktrace of the previous resolution.";
// @codeCoverageIgnoreEnd
}
throw new \Error($message);
}
\assert((function () {
$env = \getenv("AMP_DEBUG") ?: "0";
if (($env !== "0" && $env !== "false") || (\defined("AMP_DEBUG") && \AMP_DEBUG)) {
$trace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS);
\array_shift($trace); // remove current closure
$this->resolutionTrace = $trace;
}
return true;
})());
$this->complete = new Success(false);
if ($this->waiting !== null) {
$waiting = $this->waiting;
$this->waiting = null;
$waiting->resolve($this->complete);
}
}
/**
* @param \Throwable $exception
*
* @return void
*/
private function fail(\Throwable $exception)
{
$this->complete = new Failure($exception);
if ($this->waiting !== null) {
$waiting = $this->waiting;
$this->waiting = null;
$waiting->resolve($this->complete);
}
}
}

View File

@ -0,0 +1,90 @@
<?php
namespace Amp\Internal;
use Amp\Coroutine;
use Amp\Loop;
use Amp\Promise;
use React\Promise\PromiseInterface as ReactPromise;
/**
* Stores a set of functions to be invoked when a promise is resolved.
*
* @internal
* @psalm-internal Amp\Internal
*/
class ResolutionQueue
{
/** @var array<array-key, callable(\Throwable|null, mixed): (Promise|\React\Promise\PromiseInterface|\Generator<mixed,
* Promise|\React\Promise\PromiseInterface|array<array-key, Promise|\React\Promise\PromiseInterface>, mixed,
* mixed>|null) | callable(\Throwable|null, mixed): void> */
private $queue = [];
/**
* @param callable|null $callback Initial callback to add to queue.
*
* @psalm-param null|callable(\Throwable|null, mixed): (Promise|\React\Promise\PromiseInterface|\Generator<mixed,
* Promise|\React\Promise\PromiseInterface|array<array-key, Promise|\React\Promise\PromiseInterface>, mixed,
* mixed>|null) | callable(\Throwable|null, mixed): void $callback
*/
public function __construct(callable $callback = null)
{
if ($callback !== null) {
$this->push($callback);
}
}
/**
* Unrolls instances of self to avoid blowing up the call stack on resolution.
*
* @param callable $callback
*
* @psalm-param callable(\Throwable|null, mixed): (Promise|\React\Promise\PromiseInterface|\Generator<mixed,
* Promise|\React\Promise\PromiseInterface|array<array-key, Promise|\React\Promise\PromiseInterface>, mixed,
* mixed>|null) | callable(\Throwable|null, mixed): void $callback
*
* @return void
*/
public function push(callable $callback)
{
if ($callback instanceof self) {
$this->queue = \array_merge($this->queue, $callback->queue);
return;
}
$this->queue[] = $callback;
}
/**
* Calls each callback in the queue, passing the provided values to the function.
*
* @param \Throwable|null $exception
* @param mixed $value
*
* @return void
*/
public function __invoke($exception, $value)
{
foreach ($this->queue as $callback) {
try {
$result = $callback($exception, $value);
if ($result === null) {
continue;
}
if ($result instanceof \Generator) {
$result = new Coroutine($result);
}
if ($result instanceof Promise || $result instanceof ReactPromise) {
Promise\rethrow($result);
}
} catch (\Throwable $exception) {
Loop::defer(static function () use ($exception) {
throw $exception;
});
}
}
}
}

View File

@ -0,0 +1,117 @@
<?php
namespace Amp\Internal;
/**
* Formats a stacktrace obtained via `debug_backtrace()`.
*
* @param array<array{file?: string, line: int, type?: string, class: string, function: string}> $trace Output of
* `debug_backtrace()`.
*
* @return string Formatted stacktrace.
*
* @codeCoverageIgnore
* @internal
*/
function formatStacktrace(array $trace): string
{
return \implode("\n", \array_map(static function ($e, $i) {
$line = "#{$i} ";
if (isset($e["file"])) {
$line .= "{$e['file']}:{$e['line']} ";
}
if (isset($e["type"])) {
$line .= $e["class"] . $e["type"];
}
return $line . $e["function"] . "()";
}, $trace, \array_keys($trace)));
}
/**
* Creates a `TypeError` with a standardized error message.
*
* @param string[] $expected Expected types.
* @param mixed $given Given value.
*
* @return \TypeError
*
* @internal
*/
function createTypeError(array $expected, $given): \TypeError
{
$givenType = \is_object($given) ? \sprintf("instance of %s", \get_class($given)) : \gettype($given);
if (\count($expected) === 1) {
$expectedType = "Expected the following type: " . \array_pop($expected);
} else {
$expectedType = "Expected one of the following types: " . \implode(", ", $expected);
}
return new \TypeError("{$expectedType}; {$givenType} given");
}
/**
* Returns the current time relative to an arbitrary point in time.
*
* @return int Time in milliseconds.
*/
function getCurrentTime(): int
{
/** @var int|null $startTime */
static $startTime;
/** @var int|null $nextWarning */
static $nextWarning;
if (\PHP_INT_SIZE === 4) {
// @codeCoverageIgnoreStart
if ($startTime === null) {
$startTime = \PHP_VERSION_ID >= 70300 ? \hrtime(false)[0] : \time();
$nextWarning = \PHP_INT_MAX - 86400 * 7;
}
if (\PHP_VERSION_ID >= 70300) {
list($seconds, $nanoseconds) = \hrtime(false);
$seconds -= $startTime;
if ($seconds >= $nextWarning) {
$timeToOverflow = (\PHP_INT_MAX - $seconds * 1000) / 1000;
\trigger_error(
"getCurrentTime() will overflow in $timeToOverflow seconds, please restart the process before that. " .
"You're using a 32 bit version of PHP, so time will overflow about every 24 days. Regular restarts are required.",
\E_USER_WARNING
);
/** @psalm-suppress PossiblyNullOperand */
$nextWarning += 600; // every 10 minutes
}
return (int) ($seconds * 1000 + $nanoseconds / 1000000);
}
$seconds = \microtime(true) - $startTime;
if ($seconds >= $nextWarning) {
$timeToOverflow = (\PHP_INT_MAX - $seconds * 1000) / 1000;
\trigger_error(
"getCurrentTime() will overflow in $timeToOverflow seconds, please restart the process before that. " .
"You're using a 32 bit version of PHP, so time will overflow about every 24 days. Regular restarts are required.",
\E_USER_WARNING
);
/** @psalm-suppress PossiblyNullOperand */
$nextWarning += 600; // every 10 minutes
}
return (int) ($seconds * 1000);
// @codeCoverageIgnoreEnd
}
if (\PHP_VERSION_ID >= 70300) {
list($seconds, $nanoseconds) = \hrtime(false);
return (int) ($seconds * 1000 + $nanoseconds / 1000000);
}
return (int) (\microtime(true) * 1000);
}

View File

@ -0,0 +1,39 @@
<?php
namespace Amp;
class InvalidYieldError extends \Error
{
/**
* @param \Generator $generator
* @param string $prefix
* @param \Throwable|null $previous
*/
public function __construct(\Generator $generator, string $prefix, \Throwable $previous = null)
{
$yielded = $generator->current();
$prefix .= \sprintf(
"; %s yielded at key %s",
\is_object($yielded) ? \get_class($yielded) : \gettype($yielded),
\var_export($generator->key(), true)
);
if (!$generator->valid()) {
parent::__construct($prefix, 0, $previous);
return;
}
$reflGen = new \ReflectionGenerator($generator);
$exeGen = $reflGen->getExecutingGenerator();
if ($isSubgenerator = ($exeGen !== $generator)) {
$reflGen = new \ReflectionGenerator($exeGen);
}
parent::__construct(\sprintf(
"%s on line %s in %s",
$prefix,
$reflGen->getExecutingLine(),
$reflGen->getExecutingFile()
), 0, $previous);
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Amp;
/**
* Defines an asynchronous iterator over a set of values that is designed to be used within a coroutine.
*
* @template-covariant TValue
*/
interface Iterator
{
/**
* Succeeds with true if an emitted value is available by calling getCurrent() or false if the iterator has
* resolved. If the iterator fails, the returned promise will fail with the same exception.
*
* @return Promise
* @psalm-return Promise<bool>
*
* @throws \Error If the prior promise returned from this method has not resolved.
* @throws \Throwable The exception used to fail the iterator.
*/
public function advance(): Promise;
/**
* Gets the last emitted value or throws an exception if the iterator has completed.
*
* @return mixed Value emitted from the iterator.
* @psalm-return TValue
*
* @throws \Error If the iterator has resolved or advance() was not called before calling this method.
* @throws \Throwable The exception used to fail the iterator.
*/
public function getCurrent();
}

View File

@ -0,0 +1,44 @@
<?php
namespace Amp;
/**
* Creates a promise that calls $promisor only when the result of the promise is requested (i.e. onResolve() is called
* on the promise). $promisor can return a promise or any value. If $promisor throws an exception, the promise fails
* with that exception. If $promisor returns a Generator, it will be run as a coroutine.
*/
final class LazyPromise implements Promise
{
/** @var callable|null */
private $promisor;
/** @var Promise|null */
private $promise;
/**
* @param callable $promisor Function which starts an async operation, returning a Promise (or any value).
* Generators will be run as a coroutine.
*/
public function __construct(callable $promisor)
{
$this->promisor = $promisor;
}
/**
* {@inheritdoc}
*/
public function onResolve(callable $onResolved)
{
if ($this->promise === null) {
\assert($this->promisor !== null);
$provider = $this->promisor;
$this->promisor = null;
$this->promise = call($provider);
}
\assert($this->promise !== null);
$this->promise->onResolve($onResolved);
}
}

View File

@ -0,0 +1,456 @@
<?php
namespace Amp;
use Amp\Loop\Driver;
use Amp\Loop\DriverFactory;
use Amp\Loop\InvalidWatcherError;
use Amp\Loop\UnsupportedFeatureException;
use Amp\Loop\Watcher;
/**
* Accessor to allow global access to the event loop.
*
* @see \Amp\Loop\Driver
*/
final class Loop
{
/**
* @var Driver
*/
private static $driver;
/**
* Disable construction as this is a static class.
*/
private function __construct()
{
// intentionally left blank
}
/**
* Sets the driver to be used for `Loop::run()`.
*
* @param Driver $driver
*
* @return void
*/
public static function set(Driver $driver)
{
try {
self::$driver = new class extends Driver {
/**
* @return void
*/
protected function activate(array $watchers)
{
throw new \Error("Can't activate watcher during garbage collection.");
}
/**
* @return void
*/
protected function dispatch(bool $blocking)
{
throw new \Error("Can't dispatch during garbage collection.");
}
/**
* @return void
*/
protected function deactivate(Watcher $watcher)
{
// do nothing
}
public function getHandle()
{
return null;
}
};
\gc_collect_cycles();
} finally {
self::$driver = $driver;
}
}
/**
* Run the event loop and optionally execute a callback within the scope of it.
*
* The loop MUST continue to run until it is either stopped explicitly, no referenced watchers exist anymore, or an
* exception is thrown that cannot be handled. Exceptions that cannot be handled are exceptions thrown from an
* error handler or exceptions that would be passed to an error handler but none exists to handle them.
*
* @param callable|null $callback The callback to execute.
*
* @return void
*/
public static function run(callable $callback = null)
{
if ($callback) {
self::$driver->defer($callback);
}
self::$driver->run();
}
/**
* Stop the event loop.
*
* When an event loop is stopped, it continues with its current tick and exits the loop afterwards. Multiple calls
* to stop MUST be ignored and MUST NOT raise an exception.
*
* @return void
*/
public static function stop()
{
self::$driver->stop();
}
/**
* Defer the execution of a callback.
*
* The deferred callable MUST be executed before any other type of watcher in a tick. Order of enabling MUST be
* preserved when executing the callbacks.
*
* The created watcher MUST immediately be marked as enabled, but only be activated (i.e. callback can be called)
* right before the next tick. Callbacks of watchers MUST NOT be called in the tick they were enabled.
*
* @param callable(string $watcherId, mixed $data) $callback The callback to defer. The `$watcherId` will be
* invalidated before the callback call.
* @param mixed $data Arbitrary data given to the callback function as the `$data` parameter.
*
* @return string An unique identifier that can be used to cancel, enable or disable the watcher.
*/
public static function defer(callable $callback, $data = null): string
{
return self::$driver->defer($callback, $data);
}
/**
* Delay the execution of a callback.
*
* The delay is a minimum and approximate, accuracy is not guaranteed. Order of calls MUST be determined by which
* timers expire first, but timers with the same expiration time MAY be executed in any order.
*
* The created watcher MUST immediately be marked as enabled, but only be activated (i.e. callback can be called)
* right before the next tick. Callbacks of watchers MUST NOT be called in the tick they were enabled.
*
* @param int $delay The amount of time, in milliseconds, to delay the execution for.
* @param callable(string $watcherId, mixed $data) $callback The callback to delay. The `$watcherId` will be
* invalidated before the callback call.
* @param mixed $data Arbitrary data given to the callback function as the `$data` parameter.
*
* @return string An unique identifier that can be used to cancel, enable or disable the watcher.
*/
public static function delay(int $delay, callable $callback, $data = null): string
{
return self::$driver->delay($delay, $callback, $data);
}
/**
* Repeatedly execute a callback.
*
* The interval between executions is a minimum and approximate, accuracy is not guaranteed. Order of calls MUST be
* determined by which timers expire first, but timers with the same expiration time MAY be executed in any order.
* The first execution is scheduled after the first interval period.
*
* The created watcher MUST immediately be marked as enabled, but only be activated (i.e. callback can be called)
* right before the next tick. Callbacks of watchers MUST NOT be called in the tick they were enabled.
*
* @param int $interval The time interval, in milliseconds, to wait between executions.
* @param callable(string $watcherId, mixed $data) $callback The callback to repeat.
* @param mixed $data Arbitrary data given to the callback function as the `$data` parameter.
*
* @return string An unique identifier that can be used to cancel, enable or disable the watcher.
*/
public static function repeat(int $interval, callable $callback, $data = null): string
{
return self::$driver->repeat($interval, $callback, $data);
}
/**
* Execute a callback when a stream resource becomes readable or is closed for reading.
*
* Warning: Closing resources locally, e.g. with `fclose`, might not invoke the callback. Be sure to `cancel` the
* watcher when closing the resource locally. Drivers MAY choose to notify the user if there are watchers on invalid
* resources, but are not required to, due to the high performance impact. Watchers on closed resources are
* therefore undefined behavior.
*
* Multiple watchers on the same stream MAY be executed in any order.
*
* The created watcher MUST immediately be marked as enabled, but only be activated (i.e. callback can be called)
* right before the next tick. Callbacks of watchers MUST NOT be called in the tick they were enabled.
*
* @param resource $stream The stream to monitor.
* @param callable(string $watcherId, resource $stream, mixed $data) $callback The callback to execute.
* @param mixed $data Arbitrary data given to the callback function as the `$data` parameter.
*
* @return string An unique identifier that can be used to cancel, enable or disable the watcher.
*/
public static function onReadable($stream, callable $callback, $data = null): string
{
return self::$driver->onReadable($stream, $callback, $data);
}
/**
* Execute a callback when a stream resource becomes writable or is closed for writing.
*
* Warning: Closing resources locally, e.g. with `fclose`, might not invoke the callback. Be sure to `cancel` the
* watcher when closing the resource locally. Drivers MAY choose to notify the user if there are watchers on invalid
* resources, but are not required to, due to the high performance impact. Watchers on closed resources are
* therefore undefined behavior.
*
* Multiple watchers on the same stream MAY be executed in any order.
*
* The created watcher MUST immediately be marked as enabled, but only be activated (i.e. callback can be called)
* right before the next tick. Callbacks of watchers MUST NOT be called in the tick they were enabled.
*
* @param resource $stream The stream to monitor.
* @param callable(string $watcherId, resource $stream, mixed $data) $callback The callback to execute.
* @param mixed $data Arbitrary data given to the callback function as the `$data` parameter.
*
* @return string An unique identifier that can be used to cancel, enable or disable the watcher.
*/
public static function onWritable($stream, callable $callback, $data = null): string
{
return self::$driver->onWritable($stream, $callback, $data);
}
/**
* Execute a callback when a signal is received.
*
* Warning: Installing the same signal on different instances of this interface is deemed undefined behavior.
* Implementations MAY try to detect this, if possible, but are not required to. This is due to technical
* limitations of the signals being registered globally per process.
*
* Multiple watchers on the same signal MAY be executed in any order.
*
* The created watcher MUST immediately be marked as enabled, but only be activated (i.e. callback can be called)
* right before the next tick. Callbacks of watchers MUST NOT be called in the tick they were enabled.
*
* @param int $signo The signal number to monitor.
* @param callable(string $watcherId, int $signo, mixed $data) $callback The callback to execute.
* @param mixed $data Arbitrary data given to the callback function as the $data parameter.
*
* @return string An unique identifier that can be used to cancel, enable or disable the watcher.
*
* @throws UnsupportedFeatureException If signal handling is not supported.
*/
public static function onSignal(int $signo, callable $callback, $data = null): string
{
return self::$driver->onSignal($signo, $callback, $data);
}
/**
* Enable a watcher to be active starting in the next tick.
*
* Watchers MUST immediately be marked as enabled, but only be activated (i.e. callbacks can be called) right before
* the next tick. Callbacks of watchers MUST NOT be called in the tick they were enabled.
*
* @param string $watcherId The watcher identifier.
*
* @return void
*
* @throws InvalidWatcherError If the watcher identifier is invalid.
*/
public static function enable(string $watcherId)
{
self::$driver->enable($watcherId);
}
/**
* Disable a watcher immediately.
*
* A watcher MUST be disabled immediately, e.g. if a defer watcher disables a later defer watcher, the second defer
* watcher isn't executed in this tick.
*
* Disabling a watcher MUST NOT invalidate the watcher. Calling this function MUST NOT fail, even if passed an
* invalid watcher.
*
* @param string $watcherId The watcher identifier.
*
* @return void
*/
public static function disable(string $watcherId)
{
if (\PHP_VERSION_ID < 70200 && !isset(self::$driver)) {
// Prior to PHP 7.2, self::$driver may be unset during destruct.
// See https://github.com/amphp/amp/issues/212.
return;
}
self::$driver->disable($watcherId);
}
/**
* Cancel a watcher.
*
* This will detatch the event loop from all resources that are associated to the watcher. After this operation the
* watcher is permanently invalid. Calling this function MUST NOT fail, even if passed an invalid watcher.
*
* @param string $watcherId The watcher identifier.
*
* @return void
*/
public static function cancel(string $watcherId)
{
if (\PHP_VERSION_ID < 70200 && !isset(self::$driver)) {
// Prior to PHP 7.2, self::$driver may be unset during destruct.
// See https://github.com/amphp/amp/issues/212.
return;
}
self::$driver->cancel($watcherId);
}
/**
* Reference a watcher.
*
* This will keep the event loop alive whilst the watcher is still being monitored. Watchers have this state by
* default.
*
* @param string $watcherId The watcher identifier.
*
* @return void
*
* @throws InvalidWatcherError If the watcher identifier is invalid.
*/
public static function reference(string $watcherId)
{
self::$driver->reference($watcherId);
}
/**
* Unreference a watcher.
*
* The event loop should exit the run method when only unreferenced watchers are still being monitored. Watchers
* are all referenced by default.
*
* @param string $watcherId The watcher identifier.
*
* @return void
*/
public static function unreference(string $watcherId)
{
if (\PHP_VERSION_ID < 70200 && !isset(self::$driver)) {
// Prior to PHP 7.2, self::$driver may be unset during destruct.
// See https://github.com/amphp/amp/issues/212.
return;
}
self::$driver->unreference($watcherId);
}
/**
* Returns the current loop time in millisecond increments. Note this value does not necessarily correlate to
* wall-clock time, rather the value returned is meant to be used in relative comparisons to prior values returned
* by this method (intervals, expiration calculations, etc.) and is only updated once per loop tick.
*
* @return int
*/
public static function now(): int
{
return self::$driver->now();
}
/**
* Stores information in the loop bound registry.
*
* Stored information is package private. Packages MUST NOT retrieve the stored state of other packages. Packages
* MUST use their namespace as prefix for keys. They may do so by using `SomeClass::class` as key.
*
* If packages want to expose loop bound state to consumers other than the package, they SHOULD provide a dedicated
* interface for that purpose instead of sharing the storage key.
*
* @param string $key The namespaced storage key.
* @param mixed $value The value to be stored.
*
* @return void
*/
public static function setState(string $key, $value)
{
self::$driver->setState($key, $value);
}
/**
* Gets information stored bound to the loop.
*
* Stored information is package private. Packages MUST NOT retrieve the stored state of other packages. Packages
* MUST use their namespace as prefix for keys. They may do so by using `SomeClass::class` as key.
*
* If packages want to expose loop bound state to consumers other than the package, they SHOULD provide a dedicated
* interface for that purpose instead of sharing the storage key.
*
* @param string $key The namespaced storage key.
*
* @return mixed The previously stored value or `null` if it doesn't exist.
*/
public static function getState(string $key)
{
return self::$driver->getState($key);
}
/**
* Set a callback to be executed when an error occurs.
*
* The callback receives the error as the first and only parameter. The return value of the callback gets ignored.
* If it can't handle the error, it MUST throw the error. Errors thrown by the callback or during its invocation
* MUST be thrown into the `run` loop and stop the driver.
*
* Subsequent calls to this method will overwrite the previous handler.
*
* @param callable(\Throwable $error)|null $callback The callback to execute. `null` will clear the
* current handler.
*
* @return callable(\Throwable $error)|null The previous handler, `null` if there was none.
*/
public static function setErrorHandler(callable $callback = null)
{
return self::$driver->setErrorHandler($callback);
}
/**
* Retrieve an associative array of information about the event loop driver.
*
* The returned array MUST contain the following data describing the driver's currently registered watchers:
*
* [
* "defer" => ["enabled" => int, "disabled" => int],
* "delay" => ["enabled" => int, "disabled" => int],
* "repeat" => ["enabled" => int, "disabled" => int],
* "on_readable" => ["enabled" => int, "disabled" => int],
* "on_writable" => ["enabled" => int, "disabled" => int],
* "on_signal" => ["enabled" => int, "disabled" => int],
* "enabled_watchers" => ["referenced" => int, "unreferenced" => int],
* "running" => bool
* ];
*
* Implementations MAY optionally add more information in the array but at minimum the above `key => value` format
* MUST always be provided.
*
* @return array Statistics about the loop in the described format.
*/
public static function getInfo(): array
{
return self::$driver->getInfo();
}
/**
* Retrieve the event loop driver that is in scope.
*
* @return Driver
*/
public static function get(): Driver
{
return self::$driver;
}
}
// Default factory, don't move this to a file loaded by the composer "files" autoload mechanism, otherwise custom
// implementations might have issues setting a default loop, because it's overridden by us then.
// @codeCoverageIgnoreStart
Loop::set((new DriverFactory)->create());
// @codeCoverageIgnoreEnd

View File

@ -0,0 +1,742 @@
<?php
namespace Amp\Loop;
use Amp\Coroutine;
use Amp\Promise;
use React\Promise\PromiseInterface as ReactPromise;
use function Amp\Promise\rethrow;
/**
* Event loop driver which implements all basic operations to allow interoperability.
*
* Watchers (enabled or new watchers) MUST immediately be marked as enabled, but only be activated (i.e. callbacks can
* be called) right before the next tick. Callbacks of watchers MUST NOT be called in the tick they were enabled.
*
* All registered callbacks MUST NOT be called from a file with strict types enabled (`declare(strict_types=1)`).
*/
abstract class Driver
{
// Don't use 1e3 / 1e6, they result in a float instead of int
const MILLISEC_PER_SEC = 1000;
const MICROSEC_PER_SEC = 1000000;
/** @var string */
private $nextId = "a";
/** @var Watcher[] */
private $watchers = [];
/** @var Watcher[] */
private $enableQueue = [];
/** @var Watcher[] */
private $deferQueue = [];
/** @var Watcher[] */
private $nextTickQueue = [];
/** @var callable(\Throwable):void|null */
private $errorHandler;
/** @var bool */
private $running = false;
/** @var array */
private $registry = [];
/**
* Run the event loop.
*
* One iteration of the loop is called one "tick". A tick covers the following steps:
*
* 1. Activate watchers created / enabled in the last tick / before `run()`.
* 2. Execute all enabled defer watchers.
* 3. Execute all due timer, pending signal and actionable stream callbacks, each only once per tick.
*
* The loop MUST continue to run until it is either stopped explicitly, no referenced watchers exist anymore, or an
* exception is thrown that cannot be handled. Exceptions that cannot be handled are exceptions thrown from an
* error handler or exceptions that would be passed to an error handler but none exists to handle them.
*
* @return void
*/
public function run()
{
$this->running = true;
try {
while ($this->running) {
if ($this->isEmpty()) {
return;
}
$this->tick();
}
} finally {
$this->stop();
}
}
/**
* @return bool True if no enabled and referenced watchers remain in the loop.
*/
private function isEmpty(): bool
{
foreach ($this->watchers as $watcher) {
if ($watcher->enabled && $watcher->referenced) {
return false;
}
}
return true;
}
/**
* Executes a single tick of the event loop.
*
* @return void
*/
private function tick()
{
if (empty($this->deferQueue)) {
$this->deferQueue = $this->nextTickQueue;
} else {
$this->deferQueue = \array_merge($this->deferQueue, $this->nextTickQueue);
}
$this->nextTickQueue = [];
$this->activate($this->enableQueue);
$this->enableQueue = [];
foreach ($this->deferQueue as $watcher) {
if (!isset($this->deferQueue[$watcher->id])) {
continue; // Watcher disabled by another defer watcher.
}
unset($this->watchers[$watcher->id], $this->deferQueue[$watcher->id]);
try {
/** @var mixed $result */
$result = ($watcher->callback)($watcher->id, $watcher->data);
if ($result === null) {
continue;
}
if ($result instanceof \Generator) {
$result = new Coroutine($result);
}
if ($result instanceof Promise || $result instanceof ReactPromise) {
rethrow($result);
}
} catch (\Throwable $exception) {
$this->error($exception);
}
}
/** @psalm-suppress RedundantCondition */
$this->dispatch(empty($this->nextTickQueue) && empty($this->enableQueue) && $this->running && !$this->isEmpty());
}
/**
* Activates (enables) all the given watchers.
*
* @param Watcher[] $watchers
*
* @return void
*/
abstract protected function activate(array $watchers);
/**
* Dispatches any pending read/write, timer, and signal events.
*
* @param bool $blocking
*
* @return void
*/
abstract protected function dispatch(bool $blocking);
/**
* Stop the event loop.
*
* When an event loop is stopped, it continues with its current tick and exits the loop afterwards. Multiple calls
* to stop MUST be ignored and MUST NOT raise an exception.
*
* @return void
*/
public function stop()
{
$this->running = false;
}
/**
* Defer the execution of a callback.
*
* The deferred callable MUST be executed before any other type of watcher in a tick. Order of enabling MUST be
* preserved when executing the callbacks.
*
* The created watcher MUST immediately be marked as enabled, but only be activated (i.e. callback can be called)
* right before the next tick. Callbacks of watchers MUST NOT be called in the tick they were enabled.
*
* @param callable (string $watcherId, mixed $data) $callback The callback to defer. The `$watcherId` will be
* invalidated before the callback call.
* @param mixed $data Arbitrary data given to the callback function as the `$data` parameter.
*
* @return string An unique identifier that can be used to cancel, enable or disable the watcher.
*/
public function defer(callable $callback, $data = null): string
{
/** @psalm-var Watcher<null> $watcher */
$watcher = new Watcher;
$watcher->type = Watcher::DEFER;
$watcher->id = $this->nextId++;
$watcher->callback = $callback;
$watcher->data = $data;
$this->watchers[$watcher->id] = $watcher;
$this->nextTickQueue[$watcher->id] = $watcher;
return $watcher->id;
}
/**
* Delay the execution of a callback.
*
* The delay is a minimum and approximate, accuracy is not guaranteed. Order of calls MUST be determined by which
* timers expire first, but timers with the same expiration time MAY be executed in any order.
*
* The created watcher MUST immediately be marked as enabled, but only be activated (i.e. callback can be called)
* right before the next tick. Callbacks of watchers MUST NOT be called in the tick they were enabled.
*
* @param int $delay The amount of time, in milliseconds, to delay the execution for.
* @param callable (string $watcherId, mixed $data) $callback The callback to delay. The `$watcherId` will be
* invalidated before the callback call.
* @param mixed $data Arbitrary data given to the callback function as the `$data` parameter.
*
* @return string An unique identifier that can be used to cancel, enable or disable the watcher.
*/
public function delay(int $delay, callable $callback, $data = null): string
{
if ($delay < 0) {
throw new \Error("Delay must be greater than or equal to zero");
}
/** @psalm-var Watcher<int> $watcher */
$watcher = new Watcher;
$watcher->type = Watcher::DELAY;
$watcher->id = $this->nextId++;
$watcher->callback = $callback;
$watcher->value = $delay;
$watcher->expiration = $this->now() + $delay;
$watcher->data = $data;
$this->watchers[$watcher->id] = $watcher;
$this->enableQueue[$watcher->id] = $watcher;
return $watcher->id;
}
/**
* Repeatedly execute a callback.
*
* The interval between executions is a minimum and approximate, accuracy is not guaranteed. Order of calls MUST be
* determined by which timers expire first, but timers with the same expiration time MAY be executed in any order.
* The first execution is scheduled after the first interval period.
*
* The created watcher MUST immediately be marked as enabled, but only be activated (i.e. callback can be called)
* right before the next tick. Callbacks of watchers MUST NOT be called in the tick they were enabled.
*
* @param int $interval The time interval, in milliseconds, to wait between executions.
* @param callable (string $watcherId, mixed $data) $callback The callback to repeat.
* @param mixed $data Arbitrary data given to the callback function as the `$data` parameter.
*
* @return string An unique identifier that can be used to cancel, enable or disable the watcher.
*/
public function repeat(int $interval, callable $callback, $data = null): string
{
if ($interval < 0) {
throw new \Error("Interval must be greater than or equal to zero");
}
/** @psalm-var Watcher<int> $watcher */
$watcher = new Watcher;
$watcher->type = Watcher::REPEAT;
$watcher->id = $this->nextId++;
$watcher->callback = $callback;
$watcher->value = $interval;
$watcher->expiration = $this->now() + $interval;
$watcher->data = $data;
$this->watchers[$watcher->id] = $watcher;
$this->enableQueue[$watcher->id] = $watcher;
return $watcher->id;
}
/**
* Execute a callback when a stream resource becomes readable or is closed for reading.
*
* Warning: Closing resources locally, e.g. with `fclose`, might not invoke the callback. Be sure to `cancel` the
* watcher when closing the resource locally. Drivers MAY choose to notify the user if there are watchers on invalid
* resources, but are not required to, due to the high performance impact. Watchers on closed resources are
* therefore undefined behavior.
*
* Multiple watchers on the same stream MAY be executed in any order.
*
* The created watcher MUST immediately be marked as enabled, but only be activated (i.e. callback can be called)
* right before the next tick. Callbacks of watchers MUST NOT be called in the tick they were enabled.
*
* @param resource $stream The stream to monitor.
* @param callable (string $watcherId, resource $stream, mixed $data) $callback The callback to execute.
* @param mixed $data Arbitrary data given to the callback function as the `$data` parameter.
*
* @return string An unique identifier that can be used to cancel, enable or disable the watcher.
*/
public function onReadable($stream, callable $callback, $data = null): string
{
/** @psalm-var Watcher<resource> $watcher */
$watcher = new Watcher;
$watcher->type = Watcher::READABLE;
$watcher->id = $this->nextId++;
$watcher->callback = $callback;
$watcher->value = $stream;
$watcher->data = $data;
$this->watchers[$watcher->id] = $watcher;
$this->enableQueue[$watcher->id] = $watcher;
return $watcher->id;
}
/**
* Execute a callback when a stream resource becomes writable or is closed for writing.
*
* Warning: Closing resources locally, e.g. with `fclose`, might not invoke the callback. Be sure to `cancel` the
* watcher when closing the resource locally. Drivers MAY choose to notify the user if there are watchers on invalid
* resources, but are not required to, due to the high performance impact. Watchers on closed resources are
* therefore undefined behavior.
*
* Multiple watchers on the same stream MAY be executed in any order.
*
* The created watcher MUST immediately be marked as enabled, but only be activated (i.e. callback can be called)
* right before the next tick. Callbacks of watchers MUST NOT be called in the tick they were enabled.
*
* @param resource $stream The stream to monitor.
* @param callable (string $watcherId, resource $stream, mixed $data) $callback The callback to execute.
* @param mixed $data Arbitrary data given to the callback function as the `$data` parameter.
*
* @return string An unique identifier that can be used to cancel, enable or disable the watcher.
*/
public function onWritable($stream, callable $callback, $data = null): string
{
/** @psalm-var Watcher<resource> $watcher */
$watcher = new Watcher;
$watcher->type = Watcher::WRITABLE;
$watcher->id = $this->nextId++;
$watcher->callback = $callback;
$watcher->value = $stream;
$watcher->data = $data;
$this->watchers[$watcher->id] = $watcher;
$this->enableQueue[$watcher->id] = $watcher;
return $watcher->id;
}
/**
* Execute a callback when a signal is received.
*
* Warning: Installing the same signal on different instances of this interface is deemed undefined behavior.
* Implementations MAY try to detect this, if possible, but are not required to. This is due to technical
* limitations of the signals being registered globally per process.
*
* Multiple watchers on the same signal MAY be executed in any order.
*
* The created watcher MUST immediately be marked as enabled, but only be activated (i.e. callback can be called)
* right before the next tick. Callbacks of watchers MUST NOT be called in the tick they were enabled.
*
* @param int $signo The signal number to monitor.
* @param callable (string $watcherId, int $signo, mixed $data) $callback The callback to execute.
* @param mixed $data Arbitrary data given to the callback function as the $data parameter.
*
* @return string An unique identifier that can be used to cancel, enable or disable the watcher.
*
* @throws UnsupportedFeatureException If signal handling is not supported.
*/
public function onSignal(int $signo, callable $callback, $data = null): string
{
/** @psalm-var Watcher<int> $watcher */
$watcher = new Watcher;
$watcher->type = Watcher::SIGNAL;
$watcher->id = $this->nextId++;
$watcher->callback = $callback;
$watcher->value = $signo;
$watcher->data = $data;
$this->watchers[$watcher->id] = $watcher;
$this->enableQueue[$watcher->id] = $watcher;
return $watcher->id;
}
/**
* Enable a watcher to be active starting in the next tick.
*
* Watchers MUST immediately be marked as enabled, but only be activated (i.e. callbacks can be called) right before
* the next tick. Callbacks of watchers MUST NOT be called in the tick they were enabled.
*
* @param string $watcherId The watcher identifier.
*
* @return void
*
* @throws InvalidWatcherError If the watcher identifier is invalid.
*/
public function enable(string $watcherId)
{
if (!isset($this->watchers[$watcherId])) {
throw new InvalidWatcherError($watcherId, "Cannot enable an invalid watcher identifier: '{$watcherId}'");
}
$watcher = $this->watchers[$watcherId];
if ($watcher->enabled) {
return; // Watcher already enabled.
}
$watcher->enabled = true;
switch ($watcher->type) {
case Watcher::DEFER:
$this->nextTickQueue[$watcher->id] = $watcher;
break;
case Watcher::REPEAT:
case Watcher::DELAY:
\assert(\is_int($watcher->value));
$watcher->expiration = $this->now() + $watcher->value;
$this->enableQueue[$watcher->id] = $watcher;
break;
default:
$this->enableQueue[$watcher->id] = $watcher;
break;
}
}
/**
* Cancel a watcher.
*
* This will detach the event loop from all resources that are associated to the watcher. After this operation the
* watcher is permanently invalid. Calling this function MUST NOT fail, even if passed an invalid watcher.
*
* @param string $watcherId The watcher identifier.
*
* @return void
*/
public function cancel(string $watcherId)
{
$this->disable($watcherId);
unset($this->watchers[$watcherId]);
}
/**
* Disable a watcher immediately.
*
* A watcher MUST be disabled immediately, e.g. if a defer watcher disables a later defer watcher, the second defer
* watcher isn't executed in this tick.
*
* Disabling a watcher MUST NOT invalidate the watcher. Calling this function MUST NOT fail, even if passed an
* invalid watcher.
*
* @param string $watcherId The watcher identifier.
*
* @return void
*/
public function disable(string $watcherId)
{
if (!isset($this->watchers[$watcherId])) {
return;
}
$watcher = $this->watchers[$watcherId];
if (!$watcher->enabled) {
return; // Watcher already disabled.
}
$watcher->enabled = false;
$id = $watcher->id;
switch ($watcher->type) {
case Watcher::DEFER:
if (isset($this->nextTickQueue[$id])) {
// Watcher was only queued to be enabled.
unset($this->nextTickQueue[$id]);
} else {
unset($this->deferQueue[$id]);
}
break;
default:
if (isset($this->enableQueue[$id])) {
// Watcher was only queued to be enabled.
unset($this->enableQueue[$id]);
} else {
$this->deactivate($watcher);
}
break;
}
}
/**
* Deactivates (disables) the given watcher.
*
* @param Watcher $watcher
*
* @return void
*/
abstract protected function deactivate(Watcher $watcher);
/**
* Reference a watcher.
*
* This will keep the event loop alive whilst the watcher is still being monitored. Watchers have this state by
* default.
*
* @param string $watcherId The watcher identifier.
*
* @return void
*
* @throws InvalidWatcherError If the watcher identifier is invalid.
*/
public function reference(string $watcherId)
{
if (!isset($this->watchers[$watcherId])) {
throw new InvalidWatcherError($watcherId, "Cannot reference an invalid watcher identifier: '{$watcherId}'");
}
$this->watchers[$watcherId]->referenced = true;
}
/**
* Unreference a watcher.
*
* The event loop should exit the run method when only unreferenced watchers are still being monitored. Watchers
* are all referenced by default.
*
* @param string $watcherId The watcher identifier.
*
* @return void
*/
public function unreference(string $watcherId)
{
if (!isset($this->watchers[$watcherId])) {
return;
}
$this->watchers[$watcherId]->referenced = false;
}
/**
* Stores information in the loop bound registry.
*
* Stored information is package private. Packages MUST NOT retrieve the stored state of other packages. Packages
* MUST use their namespace as prefix for keys. They may do so by using `SomeClass::class` as key.
*
* If packages want to expose loop bound state to consumers other than the package, they SHOULD provide a dedicated
* interface for that purpose instead of sharing the storage key.
*
* @param string $key The namespaced storage key.
* @param mixed $value The value to be stored.
*
* @return void
*/
final public function setState(string $key, $value)
{
if ($value === null) {
unset($this->registry[$key]);
} else {
$this->registry[$key] = $value;
}
}
/**
* Gets information stored bound to the loop.
*
* Stored information is package private. Packages MUST NOT retrieve the stored state of other packages. Packages
* MUST use their namespace as prefix for keys. They may do so by using `SomeClass::class` as key.
*
* If packages want to expose loop bound state to consumers other than the package, they SHOULD provide a dedicated
* interface for that purpose instead of sharing the storage key.
*
* @param string $key The namespaced storage key.
*
* @return mixed The previously stored value or `null` if it doesn't exist.
*/
final public function getState(string $key)
{
return isset($this->registry[$key]) ? $this->registry[$key] : null;
}
/**
* Set a callback to be executed when an error occurs.
*
* The callback receives the error as the first and only parameter. The return value of the callback gets ignored.
* If it can't handle the error, it MUST throw the error. Errors thrown by the callback or during its invocation
* MUST be thrown into the `run` loop and stop the driver.
*
* Subsequent calls to this method will overwrite the previous handler.
*
* @param callable(\Throwable $error):void|null $callback The callback to execute. `null` will clear the
* current handler.
*
* @return callable(\Throwable $error):void|null The previous handler, `null` if there was none.
*/
public function setErrorHandler(callable $callback = null)
{
$previous = $this->errorHandler;
$this->errorHandler = $callback;
return $previous;
}
/**
* Invokes the error handler with the given exception.
*
* @param \Throwable $exception The exception thrown from a watcher callback.
*
* @return void
* @throws \Throwable If no error handler has been set.
*/
protected function error(\Throwable $exception)
{
if ($this->errorHandler === null) {
throw $exception;
}
($this->errorHandler)($exception);
}
/**
* Returns the current loop time in millisecond increments. Note this value does not necessarily correlate to
* wall-clock time, rather the value returned is meant to be used in relative comparisons to prior values returned
* by this method (intervals, expiration calculations, etc.) and is only updated once per loop tick.
*
* Extending classes should override this function to return a value cached once per loop tick.
*
* @return int
*/
public function now(): int
{
return (int) (\microtime(true) * self::MILLISEC_PER_SEC);
}
/**
* Get the underlying loop handle.
*
* Example: the `uv_loop` resource for `libuv` or the `EvLoop` object for `libev` or `null` for a native driver.
*
* Note: This function is *not* exposed in the `Loop` class. Users shall access it directly on the respective loop
* instance.
*
* @return null|object|resource The loop handle the event loop operates on. `null` if there is none.
*/
abstract public function getHandle();
/**
* Returns the same array of data as getInfo().
*
* @return array
*/
public function __debugInfo()
{
// @codeCoverageIgnoreStart
return $this->getInfo();
// @codeCoverageIgnoreEnd
}
/**
* Retrieve an associative array of information about the event loop driver.
*
* The returned array MUST contain the following data describing the driver's currently registered watchers:
*
* [
* "defer" => ["enabled" => int, "disabled" => int],
* "delay" => ["enabled" => int, "disabled" => int],
* "repeat" => ["enabled" => int, "disabled" => int],
* "on_readable" => ["enabled" => int, "disabled" => int],
* "on_writable" => ["enabled" => int, "disabled" => int],
* "on_signal" => ["enabled" => int, "disabled" => int],
* "enabled_watchers" => ["referenced" => int, "unreferenced" => int],
* "running" => bool
* ];
*
* Implementations MAY optionally add more information in the array but at minimum the above `key => value` format
* MUST always be provided.
*
* @return array Statistics about the loop in the described format.
*/
public function getInfo(): array
{
$watchers = [
"referenced" => 0,
"unreferenced" => 0,
];
$defer = $delay = $repeat = $onReadable = $onWritable = $onSignal = [
"enabled" => 0,
"disabled" => 0,
];
foreach ($this->watchers as $watcher) {
switch ($watcher->type) {
case Watcher::READABLE:
$array = &$onReadable;
break;
case Watcher::WRITABLE:
$array = &$onWritable;
break;
case Watcher::SIGNAL:
$array = &$onSignal;
break;
case Watcher::DEFER:
$array = &$defer;
break;
case Watcher::DELAY:
$array = &$delay;
break;
case Watcher::REPEAT:
$array = &$repeat;
break;
default:
// @codeCoverageIgnoreStart
throw new \Error("Unknown watcher type");
// @codeCoverageIgnoreEnd
}
if ($watcher->enabled) {
++$array["enabled"];
if ($watcher->referenced) {
++$watchers["referenced"];
} else {
++$watchers["unreferenced"];
}
} else {
++$array["disabled"];
}
}
return [
"enabled_watchers" => $watchers,
"defer" => $defer,
"delay" => $delay,
"repeat" => $repeat,
"on_readable" => $onReadable,
"on_writable" => $onWritable,
"on_signal" => $onSignal,
"running" => (bool) $this->running,
];
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace Amp\Loop;
// @codeCoverageIgnoreStart
class DriverFactory
{
/**
* Creates a new loop instance and chooses the best available driver.
*
* @return Driver
*
* @throws \Error If an invalid class has been specified via AMP_LOOP_DRIVER
*/
public function create(): Driver
{
$driver = (function () {
if ($driver = $this->createDriverFromEnv()) {
return $driver;
}
if (UvDriver::isSupported()) {
return new UvDriver;
}
if (EvDriver::isSupported()) {
return new EvDriver;
}
if (EventDriver::isSupported()) {
return new EventDriver;
}
return new NativeDriver;
})();
if (\getenv("AMP_DEBUG_TRACE_WATCHERS")) {
return new TracingDriver($driver);
}
return $driver;
}
/**
* @return Driver|null
*/
private function createDriverFromEnv()
{
$driver = \getenv("AMP_LOOP_DRIVER");
if (!$driver) {
return null;
}
if (!\class_exists($driver)) {
throw new \Error(\sprintf(
"Driver '%s' does not exist.",
$driver
));
}
if (!\is_subclass_of($driver, Driver::class)) {
throw new \Error(\sprintf(
"Driver '%s' is not a subclass of '%s'.",
$driver,
Driver::class
));
}
return new $driver;
}
}
// @codeCoverageIgnoreEnd

View File

@ -0,0 +1,317 @@
<?php /** @noinspection PhpComposerExtensionStubsInspection */
namespace Amp\Loop;
use Amp\Coroutine;
use Amp\Promise;
use React\Promise\PromiseInterface as ReactPromise;
use function Amp\Internal\getCurrentTime;
use function Amp\Promise\rethrow;
class EvDriver extends Driver
{
/** @var \EvSignal[]|null */
private static $activeSignals;
public static function isSupported(): bool
{
return \extension_loaded("ev");
}
/** @var \EvLoop */
private $handle;
/** @var \EvWatcher[] */
private $events = [];
/** @var callable */
private $ioCallback;
/** @var callable */
private $timerCallback;
/** @var callable */
private $signalCallback;
/** @var \EvSignal[] */
private $signals = [];
/** @var int Internal timestamp for now. */
private $now;
/** @var int Loop time offset */
private $nowOffset;
public function __construct()
{
$this->handle = new \EvLoop;
$this->nowOffset = getCurrentTime();
$this->now = \random_int(0, $this->nowOffset);
$this->nowOffset -= $this->now;
if (self::$activeSignals === null) {
self::$activeSignals = &$this->signals;
}
/**
* @param \EvIO $event
*
* @return void
*/
$this->ioCallback = function (\EvIO $event) {
/** @var Watcher $watcher */
$watcher = $event->data;
try {
$result = ($watcher->callback)($watcher->id, $watcher->value, $watcher->data);
if ($result === null) {
return;
}
if ($result instanceof \Generator) {
$result = new Coroutine($result);
}
if ($result instanceof Promise || $result instanceof ReactPromise) {
rethrow($result);
}
} catch (\Throwable $exception) {
$this->error($exception);
}
};
/**
* @param \EvTimer $event
*
* @return void
*/
$this->timerCallback = function (\EvTimer $event) {
/** @var Watcher $watcher */
$watcher = $event->data;
if ($watcher->type & Watcher::DELAY) {
$this->cancel($watcher->id);
} elseif ($watcher->value === 0) {
// Disable and re-enable so it's not executed repeatedly in the same tick
// See https://github.com/amphp/amp/issues/131
$this->disable($watcher->id);
$this->enable($watcher->id);
}
try {
$result = ($watcher->callback)($watcher->id, $watcher->data);
if ($result === null) {
return;
}
if ($result instanceof \Generator) {
$result = new Coroutine($result);
}
if ($result instanceof Promise || $result instanceof ReactPromise) {
rethrow($result);
}
} catch (\Throwable $exception) {
$this->error($exception);
}
};
/**
* @param \EvSignal $event
*
* @return void
*/
$this->signalCallback = function (\EvSignal $event) {
/** @var Watcher $watcher */
$watcher = $event->data;
try {
$result = ($watcher->callback)($watcher->id, $watcher->value, $watcher->data);
if ($result === null) {
return;
}
if ($result instanceof \Generator) {
$result = new Coroutine($result);
}
if ($result instanceof Promise || $result instanceof ReactPromise) {
rethrow($result);
}
} catch (\Throwable $exception) {
$this->error($exception);
}
};
}
/**
* {@inheritdoc}
*/
public function cancel(string $watcherId)
{
parent::cancel($watcherId);
unset($this->events[$watcherId]);
}
public function __destruct()
{
foreach ($this->events as $event) {
/** @psalm-suppress all */
if ($event !== null) { // Events may have been nulled in extension depending on destruct order.
$event->stop();
}
}
// We need to clear all references to events manually, see
// https://bitbucket.org/osmanov/pecl-ev/issues/31/segfault-in-ev_timer_stop
$this->events = [];
}
/**
* {@inheritdoc}
*/
public function run()
{
$active = self::$activeSignals;
\assert($active !== null);
foreach ($active as $event) {
$event->stop();
}
self::$activeSignals = &$this->signals;
foreach ($this->signals as $event) {
$event->start();
}
try {
parent::run();
} finally {
foreach ($this->signals as $event) {
$event->stop();
}
self::$activeSignals = &$active;
foreach ($active as $event) {
$event->start();
}
}
}
/**
* {@inheritdoc}
*/
public function stop()
{
$this->handle->stop();
parent::stop();
}
/**
* {@inheritdoc}
*/
public function now(): int
{
$this->now = getCurrentTime() - $this->nowOffset;
return $this->now;
}
/**
* {@inheritdoc}
*/
public function getHandle(): \EvLoop
{
return $this->handle;
}
/**
* {@inheritdoc}
*
* @return void
*/
protected function dispatch(bool $blocking)
{
$this->handle->run($blocking ? \Ev::RUN_ONCE : \Ev::RUN_ONCE | \Ev::RUN_NOWAIT);
}
/**
* {@inheritdoc}
*
* @return void
*/
protected function activate(array $watchers)
{
$this->handle->nowUpdate();
$now = $this->now();
foreach ($watchers as $watcher) {
if (!isset($this->events[$id = $watcher->id])) {
switch ($watcher->type) {
case Watcher::READABLE:
\assert(\is_resource($watcher->value));
$this->events[$id] = $this->handle->io($watcher->value, \Ev::READ, $this->ioCallback, $watcher);
break;
case Watcher::WRITABLE:
\assert(\is_resource($watcher->value));
$this->events[$id] = $this->handle->io(
$watcher->value,
\Ev::WRITE,
$this->ioCallback,
$watcher
);
break;
case Watcher::DELAY:
case Watcher::REPEAT:
\assert(\is_int($watcher->value));
$interval = $watcher->value / self::MILLISEC_PER_SEC;
$this->events[$id] = $this->handle->timer(
\max(0, ($watcher->expiration - $now) / self::MILLISEC_PER_SEC),
($watcher->type & Watcher::REPEAT) ? $interval : 0,
$this->timerCallback,
$watcher
);
break;
case Watcher::SIGNAL:
\assert(\is_int($watcher->value));
$this->events[$id] = $this->handle->signal($watcher->value, $this->signalCallback, $watcher);
break;
default:
// @codeCoverageIgnoreStart
throw new \Error("Unknown watcher type");
// @codeCoverageIgnoreEnd
}
} else {
$this->events[$id]->start();
}
if ($watcher->type === Watcher::SIGNAL) {
/** @psalm-suppress PropertyTypeCoercion */
$this->signals[$id] = $this->events[$id];
}
}
}
/**
* {@inheritdoc}
*
* @return void
*/
protected function deactivate(Watcher $watcher)
{
if (isset($this->events[$id = $watcher->id])) {
$this->events[$id]->stop();
if ($watcher->type === Watcher::SIGNAL) {
unset($this->signals[$id]);
}
}
}
}

View File

@ -0,0 +1,362 @@
<?php
namespace Amp\Loop;
use Amp\Coroutine;
use Amp\Promise;
use React\Promise\PromiseInterface as ReactPromise;
use function Amp\Internal\getCurrentTime;
use function Amp\Promise\rethrow;
class EventDriver extends Driver
{
/** @var \Event[]|null */
private static $activeSignals;
/** @var \EventBase */
private $handle;
/** @var \Event[] */
private $events = [];
/** @var callable */
private $ioCallback;
/** @var callable */
private $timerCallback;
/** @var callable */
private $signalCallback;
/** @var \Event[] */
private $signals = [];
/** @var int Internal timestamp for now. */
private $now;
/** @var int Loop time offset */
private $nowOffset;
public function __construct()
{
/** @psalm-suppress TooFewArguments https://github.com/JetBrains/phpstorm-stubs/pull/763 */
$this->handle = new \EventBase;
$this->nowOffset = getCurrentTime();
$this->now = \random_int(0, $this->nowOffset);
$this->nowOffset -= $this->now;
if (self::$activeSignals === null) {
self::$activeSignals = &$this->signals;
}
/**
* @param $resource
* @param $what
* @param Watcher $watcher
*
* @return void
*/
$this->ioCallback = function ($resource, $what, Watcher $watcher) {
\assert(\is_resource($watcher->value));
try {
$result = ($watcher->callback)($watcher->id, $watcher->value, $watcher->data);
if ($result === null) {
return;
}
if ($result instanceof \Generator) {
$result = new Coroutine($result);
}
if ($result instanceof Promise || $result instanceof ReactPromise) {
rethrow($result);
}
} catch (\Throwable $exception) {
$this->error($exception);
}
};
/**
* @param $resource
* @param $what
* @param Watcher $watcher
*
* @return void
*/
$this->timerCallback = function ($resource, $what, Watcher $watcher) {
\assert(\is_int($watcher->value));
if ($watcher->type & Watcher::DELAY) {
$this->cancel($watcher->id);
} else {
$this->events[$watcher->id]->add($watcher->value / self::MILLISEC_PER_SEC);
}
try {
$result = ($watcher->callback)($watcher->id, $watcher->data);
if ($result === null) {
return;
}
if ($result instanceof \Generator) {
$result = new Coroutine($result);
}
if ($result instanceof Promise || $result instanceof ReactPromise) {
rethrow($result);
}
} catch (\Throwable $exception) {
$this->error($exception);
}
};
/**
* @param $signum
* @param $what
* @param Watcher $watcher
*
* @return void
*/
$this->signalCallback = function ($signum, $what, Watcher $watcher) {
try {
$result = ($watcher->callback)($watcher->id, $watcher->value, $watcher->data);
if ($result === null) {
return;
}
if ($result instanceof \Generator) {
$result = new Coroutine($result);
}
if ($result instanceof Promise || $result instanceof ReactPromise) {
rethrow($result);
}
} catch (\Throwable $exception) {
$this->error($exception);
}
};
}
/**
* {@inheritdoc}
*/
public function cancel(string $watcherId)
{
parent::cancel($watcherId);
if (isset($this->events[$watcherId])) {
$this->events[$watcherId]->free();
unset($this->events[$watcherId]);
}
}
public static function isSupported(): bool
{
return \extension_loaded("event");
}
/**
* @codeCoverageIgnore
*/
public function __destruct()
{
foreach ($this->events as $event) {
if ($event !== null) { // Events may have been nulled in extension depending on destruct order.
$event->free();
}
}
// Unset here, otherwise $event->del() fails with a warning, because __destruct order isn't defined.
// See https://github.com/amphp/amp/issues/159.
$this->events = [];
// Manually free the loop handle to fully release loop resources.
// See https://github.com/amphp/amp/issues/177.
if ($this->handle !== null) {
$this->handle->free();
$this->handle = null;
}
}
/**
* {@inheritdoc}
*/
public function run()
{
$active = self::$activeSignals;
\assert($active !== null);
foreach ($active as $event) {
$event->del();
}
self::$activeSignals = &$this->signals;
foreach ($this->signals as $event) {
/** @psalm-suppress TooFewArguments https://github.com/JetBrains/phpstorm-stubs/pull/763 */
$event->add();
}
try {
parent::run();
} finally {
foreach ($this->signals as $event) {
$event->del();
}
self::$activeSignals = &$active;
foreach ($active as $event) {
/** @psalm-suppress TooFewArguments https://github.com/JetBrains/phpstorm-stubs/pull/763 */
$event->add();
}
}
}
/**
* {@inheritdoc}
*/
public function stop()
{
$this->handle->stop();
parent::stop();
}
/**
* {@inheritdoc}
*/
public function now(): int
{
$this->now = getCurrentTime() - $this->nowOffset;
return $this->now;
}
/**
* {@inheritdoc}
*/
public function getHandle(): \EventBase
{
return $this->handle;
}
/**
* {@inheritdoc}
*
* @return void
*/
protected function dispatch(bool $blocking)
{
$this->handle->loop($blocking ? \EventBase::LOOP_ONCE : \EventBase::LOOP_ONCE | \EventBase::LOOP_NONBLOCK);
}
/**
* {@inheritdoc}
*
* @return void
*/
protected function activate(array $watchers)
{
$now = $this->now();
foreach ($watchers as $watcher) {
if (!isset($this->events[$id = $watcher->id])) {
switch ($watcher->type) {
case Watcher::READABLE:
\assert(\is_resource($watcher->value));
$this->events[$id] = new \Event(
$this->handle,
$watcher->value,
\Event::READ | \Event::PERSIST,
$this->ioCallback,
$watcher
);
break;
case Watcher::WRITABLE:
\assert(\is_resource($watcher->value));
$this->events[$id] = new \Event(
$this->handle,
$watcher->value,
\Event::WRITE | \Event::PERSIST,
$this->ioCallback,
$watcher
);
break;
case Watcher::DELAY:
case Watcher::REPEAT:
\assert(\is_int($watcher->value));
$this->events[$id] = new \Event(
$this->handle,
-1,
\Event::TIMEOUT,
$this->timerCallback,
$watcher
);
break;
case Watcher::SIGNAL:
\assert(\is_int($watcher->value));
$this->events[$id] = new \Event(
$this->handle,
$watcher->value,
\Event::SIGNAL | \Event::PERSIST,
$this->signalCallback,
$watcher
);
break;
default:
// @codeCoverageIgnoreStart
throw new \Error("Unknown watcher type");
// @codeCoverageIgnoreEnd
}
}
switch ($watcher->type) {
case Watcher::DELAY:
case Watcher::REPEAT:
\assert(\is_int($watcher->value));
$interval = \max(0, $watcher->expiration - $now);
$this->events[$id]->add($interval > 0 ? $interval / self::MILLISEC_PER_SEC : 0);
break;
case Watcher::SIGNAL:
$this->signals[$id] = $this->events[$id];
// no break
default:
/** @psalm-suppress TooFewArguments https://github.com/JetBrains/phpstorm-stubs/pull/763 */
$this->events[$id]->add();
break;
}
}
}
/**
* {@inheritdoc}
*
* @return void
*/
protected function deactivate(Watcher $watcher)
{
if (isset($this->events[$id = $watcher->id])) {
$this->events[$id]->del();
if ($watcher->type === Watcher::SIGNAL) {
unset($this->signals[$id]);
}
}
}
}

View File

@ -0,0 +1,145 @@
<?php
namespace Amp\Loop\Internal;
use Amp\Loop\Watcher;
/**
* Uses a binary tree stored in an array to implement a heap.
*/
final class TimerQueue
{
/** @var TimerQueueEntry[] */
private $data = [];
/** @var int[] */
private $pointers = [];
/**
* Inserts the watcher into the queue. Time complexity: O(log(n)).
*
* @param Watcher $watcher
*
* @psalm-param Watcher<int> $watcher
*
* @return void
*/
public function insert(Watcher $watcher)
{
\assert($watcher->expiration !== null);
\assert(!isset($this->pointers[$watcher->id]));
$entry = new TimerQueueEntry($watcher, $watcher->expiration);
$node = \count($this->data);
$this->data[$node] = $entry;
$this->pointers[$watcher->id] = $node;
while ($node !== 0 && $entry->expiration < $this->data[$parent = ($node - 1) >> 1]->expiration) {
$temp = $this->data[$parent];
$this->data[$node] = $temp;
$this->pointers[$temp->watcher->id] = $node;
$this->data[$parent] = $entry;
$this->pointers[$watcher->id] = $parent;
$node = $parent;
}
}
/**
* Removes the given watcher from the queue. Time complexity: O(log(n)).
*
* @param Watcher $watcher
*
* @psalm-param Watcher<int> $watcher
*
* @return void
*/
public function remove(Watcher $watcher)
{
$id = $watcher->id;
if (!isset($this->pointers[$id])) {
return;
}
$this->removeAndRebuild($this->pointers[$id]);
}
/**
* Deletes and returns the Watcher on top of the heap if it has expired, otherwise null is returned.
* Time complexity: O(log(n)).
*
* @param int $now Current loop time.
*
* @return Watcher|null Expired watcher at the top of the heap or null if the watcher has not expired.
*
* @psalm-return Watcher<int>|null
*/
public function extract(int $now)
{
if (empty($this->data)) {
return null;
}
$data = $this->data[0];
if ($data->expiration > $now) {
return null;
}
$this->removeAndRebuild(0);
return $data->watcher;
}
/**
* Returns the expiration time value at the top of the heap. Time complexity: O(1).
*
* @return int|null Expiration time of the watcher at the top of the heap or null if the heap is empty.
*/
public function peek()
{
return isset($this->data[0]) ? $this->data[0]->expiration : null;
}
/**
* @param int $node Remove the given node and then rebuild the data array from that node downward.
*
* @return void
*/
private function removeAndRebuild(int $node)
{
$length = \count($this->data) - 1;
$id = $this->data[$node]->watcher->id;
$left = $this->data[$node] = $this->data[$length];
$this->pointers[$left->watcher->id] = $node;
unset($this->data[$length], $this->pointers[$id]);
while (($child = ($node << 1) + 1) < $length) {
if ($this->data[$child]->expiration < $this->data[$node]->expiration
&& ($child + 1 >= $length || $this->data[$child]->expiration < $this->data[$child + 1]->expiration)
) {
// Left child is less than parent and right child.
$swap = $child;
} elseif ($child + 1 < $length && $this->data[$child + 1]->expiration < $this->data[$node]->expiration) {
// Right child is less than parent and left child.
$swap = $child + 1;
} else { // Left and right child are greater than parent.
break;
}
$left = $this->data[$node];
$right = $this->data[$swap];
$this->data[$node] = $right;
$this->pointers[$right->watcher->id] = $node;
$this->data[$swap] = $left;
$this->pointers[$left->watcher->id] = $swap;
$node = $swap;
}
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Amp\Loop\Internal;
use Amp\Loop\Watcher;
use Amp\Struct;
/**
* @internal
*/
final class TimerQueueEntry
{
use Struct;
/** @var Watcher */
public $watcher;
/** @var int */
public $expiration;
/**
* @param Watcher $watcher
* @param int $expiration
*/
public function __construct(Watcher $watcher, int $expiration)
{
$this->watcher = $watcher;
$this->expiration = $expiration;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Amp\Loop;
/**
* MUST be thrown if any operation (except disable() and cancel()) is attempted with an invalid watcher identifier.
*
* An invalid watcher identifier is any identifier that is not yet emitted by the driver or cancelled by the user.
*/
class InvalidWatcherError extends \Error
{
/** @var string */
private $watcherId;
/**
* @param string $watcherId The watcher identifier.
* @param string $message The exception message.
*/
public function __construct(string $watcherId, string $message)
{
$this->watcherId = $watcherId;
parent::__construct($message);
}
/**
* @return string The watcher identifier.
*/
public function getWatcherId()
{
return $this->watcherId;
}
}

View File

@ -0,0 +1,394 @@
<?php
namespace Amp\Loop;
use Amp\CallableMaker;
use Amp\Coroutine;
use Amp\Promise;
use React\Promise\PromiseInterface as ReactPromise;
use function Amp\Internal\getCurrentTime;
use function Amp\Promise\rethrow;
class NativeDriver extends Driver
{
use CallableMaker;
/** @var resource[] */
private $readStreams = [];
/** @var Watcher[][] */
private $readWatchers = [];
/** @var resource[] */
private $writeStreams = [];
/** @var Watcher[][] */
private $writeWatchers = [];
/** @var Internal\TimerQueue */
private $timerQueue;
/** @var Watcher[][] */
private $signalWatchers = [];
/** @var int Internal timestamp for now. */
private $now;
/** @var int Loop time offset */
private $nowOffset;
/** @var bool */
private $signalHandling;
public function __construct()
{
$this->timerQueue = new Internal\TimerQueue;
$this->signalHandling = \extension_loaded("pcntl");
$this->nowOffset = getCurrentTime();
$this->now = \random_int(0, $this->nowOffset);
$this->nowOffset -= $this->now;
}
/**
* {@inheritdoc}
*
* @throws \Amp\Loop\UnsupportedFeatureException If the pcntl extension is not available.
*/
public function onSignal(int $signo, callable $callback, $data = null): string
{
if (!$this->signalHandling) {
throw new UnsupportedFeatureException("Signal handling requires the pcntl extension");
}
return parent::onSignal($signo, $callback, $data);
}
/**
* {@inheritdoc}
*/
public function now(): int
{
$this->now = getCurrentTime() - $this->nowOffset;
return $this->now;
}
/**
* {@inheritdoc}
*/
public function getHandle()
{
return null;
}
/**
* @param bool $blocking
*
* @return void
*
* @throws \Throwable
*/
protected function dispatch(bool $blocking)
{
$this->selectStreams(
$this->readStreams,
$this->writeStreams,
$blocking ? $this->getTimeout() : 0
);
$now = $this->now();
while ($watcher = $this->timerQueue->extract($now)) {
if ($watcher->type & Watcher::REPEAT) {
$watcher->enabled = false; // Trick base class into adding to enable queue when calling enable()
$this->enable($watcher->id);
} else {
$this->cancel($watcher->id);
}
try {
// Execute the timer.
$result = ($watcher->callback)($watcher->id, $watcher->data);
if ($result === null) {
continue;
}
if ($result instanceof \Generator) {
$result = new Coroutine($result);
}
if ($result instanceof Promise || $result instanceof ReactPromise) {
rethrow($result);
}
} catch (\Throwable $exception) {
$this->error($exception);
}
}
if ($this->signalHandling) {
\pcntl_signal_dispatch();
}
}
/**
* @param resource[] $read
* @param resource[] $write
* @param int $timeout
*
* @return void
*/
private function selectStreams(array $read, array $write, int $timeout)
{
$timeout /= self::MILLISEC_PER_SEC;
if (!empty($read) || !empty($write)) { // Use stream_select() if there are any streams in the loop.
if ($timeout >= 0) {
$seconds = (int) $timeout;
$microseconds = (int) (($timeout - $seconds) * self::MICROSEC_PER_SEC);
} else {
$seconds = null;
$microseconds = null;
}
$except = null;
// Error reporting suppressed since stream_select() emits an E_WARNING if it is interrupted by a signal.
if (!($result = @\stream_select($read, $write, $except, $seconds, $microseconds))) {
if ($result === 0) {
return;
}
$error = \error_get_last();
if (\strpos($error["message"] ?? '', "unable to select") !== 0) {
return;
}
$this->error(new \Exception($error["message"] ?? 'Unknown error during stream_select'));
}
foreach ($read as $stream) {
$streamId = (int) $stream;
if (!isset($this->readWatchers[$streamId])) {
continue; // All read watchers disabled.
}
foreach ($this->readWatchers[$streamId] as $watcher) {
if (!isset($this->readWatchers[$streamId][$watcher->id])) {
continue; // Watcher disabled by another IO watcher.
}
try {
$result = ($watcher->callback)($watcher->id, $stream, $watcher->data);
if ($result === null) {
continue;
}
if ($result instanceof \Generator) {
$result = new Coroutine($result);
}
if ($result instanceof Promise || $result instanceof ReactPromise) {
rethrow($result);
}
} catch (\Throwable $exception) {
$this->error($exception);
}
}
}
\assert(\is_array($write)); // See https://github.com/vimeo/psalm/issues/3036
foreach ($write as $stream) {
$streamId = (int) $stream;
if (!isset($this->writeWatchers[$streamId])) {
continue; // All write watchers disabled.
}
foreach ($this->writeWatchers[$streamId] as $watcher) {
if (!isset($this->writeWatchers[$streamId][$watcher->id])) {
continue; // Watcher disabled by another IO watcher.
}
try {
$result = ($watcher->callback)($watcher->id, $stream, $watcher->data);
if ($result === null) {
continue;
}
if ($result instanceof \Generator) {
$result = new Coroutine($result);
}
if ($result instanceof Promise || $result instanceof ReactPromise) {
rethrow($result);
}
} catch (\Throwable $exception) {
$this->error($exception);
}
}
}
return;
}
if ($timeout > 0) { // Otherwise sleep with usleep() if $timeout > 0.
\usleep((int) ($timeout * self::MICROSEC_PER_SEC));
}
}
/**
* @return int Milliseconds until next timer expires or -1 if there are no pending times.
*/
private function getTimeout(): int
{
$expiration = $this->timerQueue->peek();
if ($expiration === null) {
return -1;
}
$expiration -= getCurrentTime() - $this->nowOffset;
return $expiration > 0 ? $expiration : 0;
}
/**
* {@inheritdoc}
*
* @return void
*/
protected function activate(array $watchers)
{
foreach ($watchers as $watcher) {
switch ($watcher->type) {
case Watcher::READABLE:
\assert(\is_resource($watcher->value));
$streamId = (int) $watcher->value;
$this->readWatchers[$streamId][$watcher->id] = $watcher;
$this->readStreams[$streamId] = $watcher->value;
break;
case Watcher::WRITABLE:
\assert(\is_resource($watcher->value));
$streamId = (int) $watcher->value;
$this->writeWatchers[$streamId][$watcher->id] = $watcher;
$this->writeStreams[$streamId] = $watcher->value;
break;
case Watcher::DELAY:
case Watcher::REPEAT:
\assert(\is_int($watcher->value));
$this->timerQueue->insert($watcher);
break;
case Watcher::SIGNAL:
\assert(\is_int($watcher->value));
if (!isset($this->signalWatchers[$watcher->value])) {
if (!@\pcntl_signal($watcher->value, $this->callableFromInstanceMethod('handleSignal'))) {
$message = "Failed to register signal handler";
if ($error = \error_get_last()) {
$message .= \sprintf("; Errno: %d; %s", $error["type"], $error["message"]);
}
throw new \Error($message);
}
}
$this->signalWatchers[$watcher->value][$watcher->id] = $watcher;
break;
default:
// @codeCoverageIgnoreStart
throw new \Error("Unknown watcher type");
// @codeCoverageIgnoreEnd
}
}
}
/**
* {@inheritdoc}
*
* @return void
*/
protected function deactivate(Watcher $watcher)
{
switch ($watcher->type) {
case Watcher::READABLE:
$streamId = (int) $watcher->value;
unset($this->readWatchers[$streamId][$watcher->id]);
if (empty($this->readWatchers[$streamId])) {
unset($this->readWatchers[$streamId], $this->readStreams[$streamId]);
}
break;
case Watcher::WRITABLE:
$streamId = (int) $watcher->value;
unset($this->writeWatchers[$streamId][$watcher->id]);
if (empty($this->writeWatchers[$streamId])) {
unset($this->writeWatchers[$streamId], $this->writeStreams[$streamId]);
}
break;
case Watcher::DELAY:
case Watcher::REPEAT:
$this->timerQueue->remove($watcher);
break;
case Watcher::SIGNAL:
\assert(\is_int($watcher->value));
if (isset($this->signalWatchers[$watcher->value])) {
unset($this->signalWatchers[$watcher->value][$watcher->id]);
if (empty($this->signalWatchers[$watcher->value])) {
unset($this->signalWatchers[$watcher->value]);
@\pcntl_signal($watcher->value, \SIG_DFL);
}
}
break;
default:
// @codeCoverageIgnoreStart
throw new \Error("Unknown watcher type");
// @codeCoverageIgnoreEnd
}
}
/**
* @param int $signo
*
* @return void
*/
private function handleSignal(int $signo)
{
foreach ($this->signalWatchers[$signo] as $watcher) {
if (!isset($this->signalWatchers[$signo][$watcher->id])) {
continue;
}
try {
$result = ($watcher->callback)($watcher->id, $signo, $watcher->data);
if ($result === null) {
continue;
}
if ($result instanceof \Generator) {
$result = new Coroutine($result);
}
if ($result instanceof Promise || $result instanceof ReactPromise) {
rethrow($result);
}
} catch (\Throwable $exception) {
$this->error($exception);
}
}
}
}

View File

@ -0,0 +1,251 @@
<?php
namespace Amp\Loop;
use function Amp\Internal\formatStacktrace;
final class TracingDriver extends Driver
{
/** @var Driver */
private $driver;
/** @var true[] */
private $enabledWatchers = [];
/** @var true[] */
private $unreferencedWatchers = [];
/** @var string[] */
private $creationTraces = [];
/** @var string[] */
private $cancelTraces = [];
public function __construct(Driver $driver)
{
$this->driver = $driver;
}
public function run()
{
$this->driver->run();
}
public function stop()
{
$this->driver->stop();
}
public function defer(callable $callback, $data = null): string
{
$id = $this->driver->defer(function (...$args) use ($callback) {
$this->cancel($args[0]);
return $callback(...$args);
}, $data);
$this->creationTraces[$id] = formatStacktrace(\debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS));
$this->enabledWatchers[$id] = true;
return $id;
}
public function delay(int $delay, callable $callback, $data = null): string
{
$id = $this->driver->delay($delay, function (...$args) use ($callback) {
$this->cancel($args[0]);
return $callback(...$args);
}, $data);
$this->creationTraces[$id] = formatStacktrace(\debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS));
$this->enabledWatchers[$id] = true;
return $id;
}
public function repeat(int $interval, callable $callback, $data = null): string
{
$id = $this->driver->repeat($interval, $callback, $data);
$this->creationTraces[$id] = formatStacktrace(\debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS));
$this->enabledWatchers[$id] = true;
return $id;
}
public function onReadable($stream, callable $callback, $data = null): string
{
$id = $this->driver->onReadable($stream, $callback, $data);
$this->creationTraces[$id] = formatStacktrace(\debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS));
$this->enabledWatchers[$id] = true;
return $id;
}
public function onWritable($stream, callable $callback, $data = null): string
{
$id = $this->driver->onWritable($stream, $callback, $data);
$this->creationTraces[$id] = formatStacktrace(\debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS));
$this->enabledWatchers[$id] = true;
return $id;
}
public function onSignal(int $signo, callable $callback, $data = null): string
{
$id = $this->driver->onSignal($signo, $callback, $data);
$this->creationTraces[$id] = formatStacktrace(\debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS));
$this->enabledWatchers[$id] = true;
return $id;
}
public function enable(string $watcherId)
{
try {
$this->driver->enable($watcherId);
$this->enabledWatchers[$watcherId] = true;
} catch (InvalidWatcherError $e) {
throw new InvalidWatcherError(
$watcherId,
$e->getMessage() . "\r\n\r\n" . $this->getTraces($watcherId)
);
}
}
public function cancel(string $watcherId)
{
$this->driver->cancel($watcherId);
if (!isset($this->cancelTraces[$watcherId])) {
$this->cancelTraces[$watcherId] = formatStacktrace(\debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS));
}
unset($this->enabledWatchers[$watcherId], $this->unreferencedWatchers[$watcherId]);
}
public function disable(string $watcherId)
{
$this->driver->disable($watcherId);
unset($this->enabledWatchers[$watcherId]);
}
public function reference(string $watcherId)
{
try {
$this->driver->reference($watcherId);
unset($this->unreferencedWatchers[$watcherId]);
} catch (InvalidWatcherError $e) {
throw new InvalidWatcherError(
$watcherId,
$e->getMessage() . "\r\n\r\n" . $this->getTraces($watcherId)
);
}
}
public function unreference(string $watcherId)
{
$this->driver->unreference($watcherId);
$this->unreferencedWatchers[$watcherId] = true;
}
public function setErrorHandler(callable $callback = null)
{
return $this->driver->setErrorHandler($callback);
}
/** @inheritdoc */
public function getHandle()
{
$this->driver->getHandle();
}
public function dump(): string
{
$dump = "Enabled, referenced watchers keeping the loop running: ";
foreach ($this->enabledWatchers as $watcher => $_) {
if (isset($this->unreferencedWatchers[$watcher])) {
continue;
}
$dump .= "Watcher ID: " . $watcher . "\r\n";
$dump .= $this->getCreationTrace($watcher);
$dump .= "\r\n\r\n";
}
return \rtrim($dump);
}
public function getInfo(): array
{
return $this->driver->getInfo();
}
public function __debugInfo()
{
return $this->driver->__debugInfo();
}
public function now(): int
{
return $this->driver->now();
}
protected function error(\Throwable $exception)
{
$this->driver->error($exception);
}
/**
* @inheritdoc
*
* @return void
*/
protected function activate(array $watchers)
{
// nothing to do in a decorator
}
/**
* @inheritdoc
*
* @return void
*/
protected function dispatch(bool $blocking)
{
// nothing to do in a decorator
}
/**
* @inheritdoc
*
* @return void
*/
protected function deactivate(Watcher $watcher)
{
// nothing to do in a decorator
}
private function getTraces(string $watcherId): string
{
return "Creation Trace:\r\n" . $this->getCreationTrace($watcherId) . "\r\n\r\n" .
"Cancellation Trace:\r\n" . $this->getCancelTrace($watcherId);
}
private function getCreationTrace(string $watcher): string
{
if (!isset($this->creationTraces[$watcher])) {
return 'No creation trace, yet.';
}
return $this->creationTraces[$watcher];
}
private function getCancelTrace(string $watcher): string
{
if (!isset($this->cancelTraces[$watcher])) {
return 'No cancellation trace, yet.';
}
return $this->cancelTraces[$watcher];
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace Amp\Loop;
/**
* MUST be thrown if a feature is not supported by the system.
*
* This might happen if ext-pcntl is missing and the loop driver doesn't support another way to dispatch signals.
*/
class UnsupportedFeatureException extends \Exception
{
}

View File

@ -0,0 +1,350 @@
<?php
namespace Amp\Loop;
use Amp\Coroutine;
use Amp\Promise;
use React\Promise\PromiseInterface as ReactPromise;
use function Amp\Promise\rethrow;
class UvDriver extends Driver
{
/** @var resource A uv_loop resource created with uv_loop_new() */
private $handle;
/** @var resource[] */
private $events = [];
/** @var Watcher[][] */
private $watchers = [];
/** @var resource[] */
private $streams = [];
/** @var callable */
private $ioCallback;
/** @var callable */
private $timerCallback;
/** @var callable */
private $signalCallback;
public function __construct()
{
$this->handle = \uv_loop_new();
/**
* @param $event
* @param $status
* @param $events
* @param $resource
*
* @return void
*/
$this->ioCallback = function ($event, $status, $events, $resource) {
$watchers = $this->watchers[(int) $event];
switch ($status) {
case 0: // OK
break;
default: // Invoke the callback on errors, as this matches behavior with other loop back-ends.
// Re-enable watcher as libuv disables the watcher on non-zero status.
$flags = 0;
foreach ($watchers as $watcher) {
$flags |= $watcher->enabled ? $watcher->type : 0;
}
\uv_poll_start($event, $flags, $this->ioCallback);
break;
}
foreach ($watchers as $watcher) {
// $events is OR'ed with 4 to trigger watcher if no events are indicated (0) or on UV_DISCONNECT (4).
// http://docs.libuv.org/en/v1.x/poll.html
if (!($watcher->enabled && ($watcher->type & $events || ($events | 4) === 4))) {
continue;
}
try {
$result = ($watcher->callback)($watcher->id, $resource, $watcher->data);
if ($result === null) {
continue;
}
if ($result instanceof \Generator) {
$result = new Coroutine($result);
}
if ($result instanceof Promise || $result instanceof ReactPromise) {
rethrow($result);
}
} catch (\Throwable $exception) {
$this->error($exception);
}
}
};
/**
* @param $event
*
* @return void
*/
$this->timerCallback = function ($event) {
$watcher = $this->watchers[(int) $event][0];
if ($watcher->type & Watcher::DELAY) {
unset($this->events[$watcher->id], $this->watchers[(int) $event]); // Avoid call to uv_is_active().
$this->cancel($watcher->id); // Remove reference to watcher in parent.
} elseif ($watcher->value === 0) {
// Disable and re-enable so it's not executed repeatedly in the same tick
// See https://github.com/amphp/amp/issues/131
$this->disable($watcher->id);
$this->enable($watcher->id);
}
try {
$result = ($watcher->callback)($watcher->id, $watcher->data);
if ($result === null) {
return;
}
if ($result instanceof \Generator) {
$result = new Coroutine($result);
}
if ($result instanceof Promise || $result instanceof ReactPromise) {
rethrow($result);
}
} catch (\Throwable $exception) {
$this->error($exception);
}
};
/**
* @param $event
* @param $signo
*
* @return void
*/
$this->signalCallback = function ($event, $signo) {
$watcher = $this->watchers[(int) $event][0];
try {
$result = ($watcher->callback)($watcher->id, $signo, $watcher->data);
if ($result === null) {
return;
}
if ($result instanceof \Generator) {
$result = new Coroutine($result);
}
if ($result instanceof Promise || $result instanceof ReactPromise) {
rethrow($result);
}
} catch (\Throwable $exception) {
$this->error($exception);
}
};
}
/**
* {@inheritdoc}
*/
public function cancel(string $watcherId)
{
parent::cancel($watcherId);
if (!isset($this->events[$watcherId])) {
return;
}
$event = $this->events[$watcherId];
$eventId = (int) $event;
if (isset($this->watchers[$eventId][0])) { // All except IO watchers.
unset($this->watchers[$eventId]);
} elseif (isset($this->watchers[$eventId][$watcherId])) {
$watcher = $this->watchers[$eventId][$watcherId];
unset($this->watchers[$eventId][$watcherId]);
if (empty($this->watchers[$eventId])) {
unset($this->watchers[$eventId], $this->streams[(int) $watcher->value]);
}
}
unset($this->events[$watcherId]);
}
public static function isSupported(): bool
{
return \extension_loaded("uv");
}
/**
* {@inheritdoc}
*/
public function now(): int
{
\uv_update_time($this->handle);
/** @psalm-suppress TooManyArguments */
return \uv_now($this->handle);
}
/**
* {@inheritdoc}
*/
public function getHandle()
{
return $this->handle;
}
/**
* {@inheritdoc}
*
* @return void
*/
protected function dispatch(bool $blocking)
{
/** @psalm-suppress TooManyArguments */
\uv_run($this->handle, $blocking ? \UV::RUN_ONCE : \UV::RUN_NOWAIT);
}
/**
* {@inheritdoc}
*
* @return void
*/
protected function activate(array $watchers)
{
$now = $this->now();
foreach ($watchers as $watcher) {
$id = $watcher->id;
switch ($watcher->type) {
case Watcher::READABLE:
case Watcher::WRITABLE:
\assert(\is_resource($watcher->value));
$streamId = (int) $watcher->value;
if (isset($this->streams[$streamId])) {
$event = $this->streams[$streamId];
} elseif (isset($this->events[$id])) {
$event = $this->streams[$streamId] = $this->events[$id];
} else {
/** @psalm-suppress UndefinedFunction */
$event = $this->streams[$streamId] = \uv_poll_init_socket($this->handle, $watcher->value);
}
$eventId = (int) $event;
$this->events[$id] = $event;
$this->watchers[$eventId][$id] = $watcher;
$flags = 0;
foreach ($this->watchers[$eventId] as $w) {
$flags |= $w->enabled ? $w->type : 0;
}
\uv_poll_start($event, $flags, $this->ioCallback);
break;
case Watcher::DELAY:
case Watcher::REPEAT:
\assert(\is_int($watcher->value));
if (isset($this->events[$id])) {
$event = $this->events[$id];
} else {
$event = $this->events[$id] = \uv_timer_init($this->handle);
}
$this->watchers[(int) $event] = [$watcher];
\uv_timer_start(
$event,
\max(0, $watcher->expiration - $now),
($watcher->type & Watcher::REPEAT) ? $watcher->value : 0,
$this->timerCallback
);
break;
case Watcher::SIGNAL:
\assert(\is_int($watcher->value));
if (isset($this->events[$id])) {
$event = $this->events[$id];
} else {
/** @psalm-suppress UndefinedFunction */
$event = $this->events[$id] = \uv_signal_init($this->handle);
}
$this->watchers[(int) $event] = [$watcher];
/** @psalm-suppress UndefinedFunction */
\uv_signal_start($event, $this->signalCallback, $watcher->value);
break;
default:
// @codeCoverageIgnoreStart
throw new \Error("Unknown watcher type");
// @codeCoverageIgnoreEnd
}
}
}
/**
* {@inheritdoc}
*
* @return void
*/
protected function deactivate(Watcher $watcher)
{
$id = $watcher->id;
if (!isset($this->events[$id])) {
return;
}
$event = $this->events[$id];
if (!\uv_is_active($event)) {
return;
}
switch ($watcher->type) {
case Watcher::READABLE:
case Watcher::WRITABLE:
$flags = 0;
foreach ($this->watchers[(int) $event] as $w) {
$flags |= $w->enabled ? $w->type : 0;
}
if ($flags) {
\uv_poll_start($event, $flags, $this->ioCallback);
} else {
\uv_poll_stop($event);
}
break;
case Watcher::DELAY:
case Watcher::REPEAT:
\uv_timer_stop($event);
break;
case Watcher::SIGNAL:
\uv_signal_stop($event);
break;
default:
// @codeCoverageIgnoreStart
throw new \Error("Unknown watcher type");
// @codeCoverageIgnoreEnd
}
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace Amp\Loop;
use Amp\Struct;
/**
* @template TValue as (int|resource|null)
*
* @psalm-suppress MissingConstructor
*/
class Watcher
{
use Struct;
const IO = 0b00000011;
const READABLE = 0b00000001;
const WRITABLE = 0b00000010;
const DEFER = 0b00000100;
const TIMER = 0b00011000;
const DELAY = 0b00001000;
const REPEAT = 0b00010000;
const SIGNAL = 0b00100000;
/** @var int */
public $type;
/** @var bool */
public $enabled = true;
/** @var bool */
public $referenced = true;
/** @var string */
public $id;
/** @var callable */
public $callback;
/**
* Data provided to the watcher callback.
*
* @var mixed
*/
public $data;
/**
* Watcher-dependent value storage. Stream for IO watchers, signal number for signal watchers, interval for timers.
*
* @var resource|int|null
* @psalm-var TValue
*/
public $value;
/** @var int|null */
public $expiration;
}

View File

@ -0,0 +1,29 @@
<?php
namespace Amp;
class MultiReasonException extends \Exception
{
/** @var \Throwable[] */
private $reasons;
/**
* @param \Throwable[] $reasons Array of exceptions rejecting the promise.
* @param string|null $message
*/
public function __construct(array $reasons, string $message = null)
{
parent::__construct($message ?: "Multiple errors encountered; use "
. self::class . "::getReasons() to retrieve the array of exceptions thrown");
$this->reasons = $reasons;
}
/**
* @return \Throwable[]
*/
public function getReasons(): array
{
return $this->reasons;
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace Amp;
/**
* A NullCancellationToken can be used to avoid conditionals to check whether a token has been provided.
*
* Instead of writing
*
* ```php
* if ($token) {
* $token->throwIfRequested();
* }
* ```
*
* potentially multiple times, it allows writing
*
* ```php
* $token = $token ?? new NullCancellationToken;
*
* // ...
*
* $token->throwIfRequested();
* ```
*
* instead.
*/
final class NullCancellationToken implements CancellationToken
{
/** @inheritdoc */
public function subscribe(callable $callback): string
{
return "null-token";
}
/** @inheritdoc */
public function unsubscribe(string $id)
{
// nothing to do
}
/** @inheritdoc */
public function isRequested(): bool
{
return false;
}
/** @inheritdoc */
public function throwIfRequested()
{
// nothing to do
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace Amp;
/**
* @template-covariant TValue
* @template-implements Iterator<TValue>
*/
final class Producer implements Iterator
{
/**
* @use Internal\Producer<TValue>
*/
use CallableMaker, Internal\Producer;
/**
* @param callable(callable(TValue):Promise):\Generator $producer
*
* @throws \Error Thrown if the callable does not return a Generator.
*/
public function __construct(callable $producer)
{
$result = $producer($this->callableFromInstanceMethod("emit"));
if (!$result instanceof \Generator) {
throw new \Error("The callable did not return a Generator");
}
$coroutine = new Coroutine($result);
$coroutine->onResolve(function ($exception) {
if ($this->complete) {
return;
}
if ($exception) {
$this->fail($exception);
return;
}
$this->complete();
});
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Amp;
/**
* Representation of the future value of an asynchronous operation.
*
* @template-covariant TValue
* @psalm-yield TValue
*/
interface Promise
{
/**
* Registers a callback to be invoked when the promise is resolved.
*
* If this method is called multiple times, additional handlers will be registered instead of replacing any already
* existing handlers.
*
* If the promise is already resolved, the callback MUST be executed immediately.
*
* Exceptions MUST NOT be thrown from this method. Any exceptions thrown from invoked callbacks MUST be
* forwarded to the event-loop error handler.
*
* Note: You shouldn't implement this interface yourself. Instead, provide a method that returns a promise for the
* operation you're implementing. Objects other than pure placeholders implementing it are a very bad idea.
*
* @param callable $onResolved The first argument shall be `null` on success, while the second shall be `null` on
* failure.
*
* @psalm-param callable(\Throwable|null, mixed): (Promise|\React\Promise\PromiseInterface|\Generator<mixed,
* Promise|\React\Promise\PromiseInterface|array<array-key, Promise|\React\Promise\PromiseInterface>, mixed,
* mixed>|null) | callable(\Throwable|null, mixed): void $onResolved
*
* @return void
*/
public function onResolve(callable $onResolved);
}

View File

@ -0,0 +1,78 @@
<?php
namespace Amp;
/**
* A "safe" struct trait for public property aggregators.
*
* This trait is intended to make using public properties a little safer by throwing when
* nonexistent property names are read or written.
*/
trait Struct
{
/**
* The minimum percentage [0-100] at which to recommend a similar property
* name when generating error messages.
*/
private $__propertySuggestThreshold = 70;
/**
* @param string $property
*
* @psalm-return no-return
*/
public function __get(string $property)
{
throw new \Error(
$this->generateStructPropertyError($property)
);
}
/**
* @param string $property
* @param mixed $value
*
* @psalm-return no-return
*/
public function __set(string $property, $value)
{
throw new \Error(
$this->generateStructPropertyError($property)
);
}
private function generateStructPropertyError(string $property): string
{
$suggestion = $this->suggestPropertyName($property);
$suggestStr = ($suggestion == "") ? "" : " ... did you mean \"{$suggestion}?\"";
return \sprintf(
"%s property \"%s\" does not exist%s",
\str_replace("\0", "@", \get_class($this)), // Handle anonymous class names.
$property,
$suggestStr
);
}
private function suggestPropertyName(string $badProperty): string
{
$badProperty = \strtolower($badProperty);
$bestMatch = "";
$bestMatchPercentage = 0;
/** @psalm-suppress RawObjectIteration */
foreach ($this as $property => $value) {
// Never suggest properties that begin with an underscore
if ($property[0] === "_") {
continue;
}
\similar_text($badProperty, \strtolower($property), $byRefPercentage);
if ($byRefPercentage > $bestMatchPercentage) {
$bestMatchPercentage = $byRefPercentage;
$bestMatch = $property;
}
}
return ($bestMatchPercentage >= $this->__propertySuggestThreshold) ? $bestMatch : "";
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace Amp;
use React\Promise\PromiseInterface as ReactPromise;
/**
* Creates a successful promise using the given value (which can be any value except an object implementing
* `Amp\Promise` or `React\Promise\PromiseInterface`).
*
* @template-covariant TValue
* @template-implements Promise<TValue>
*/
final class Success implements Promise
{
/** @var mixed */
private $value;
/**
* @param mixed $value Anything other than a Promise object.
*
* @psalm-param TValue $value
*
* @throws \Error If a promise is given as the value.
*/
public function __construct($value = null)
{
if ($value instanceof Promise || $value instanceof ReactPromise) {
throw new \Error("Cannot use a promise as success value");
}
$this->value = $value;
}
/**
* {@inheritdoc}
*/
public function onResolve(callable $onResolved)
{
try {
$result = $onResolved(null, $this->value);
if ($result === null) {
return;
}
if ($result instanceof \Generator) {
$result = new Coroutine($result);
}
if ($result instanceof Promise || $result instanceof ReactPromise) {
Promise\rethrow($result);
}
} catch (\Throwable $exception) {
Loop::defer(static function () use ($exception) {
throw $exception;
});
}
}
}

View File

@ -0,0 +1,75 @@
<?php
namespace Amp;
use function Amp\Internal\formatStacktrace;
/**
* A TimeoutCancellationToken automatically requests cancellation after the timeout has elapsed.
*/
final class TimeoutCancellationToken implements CancellationToken
{
/** @var string */
private $watcher;
/** @var CancellationToken */
private $token;
/**
* @param int $timeout Milliseconds until cancellation is requested.
* @param string $message Message for TimeoutException. Default is "Operation timed out".
*/
public function __construct(int $timeout, string $message = "Operation timed out")
{
$source = new CancellationTokenSource;
$this->token = $source->getToken();
$trace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS);
$this->watcher = Loop::delay($timeout, static function () use ($source, $message, $trace) {
$trace = formatStacktrace($trace);
$source->cancel(new TimeoutException("$message\r\nTimeoutCancellationToken was created here:\r\n$trace"));
});
Loop::unreference($this->watcher);
}
/**
* Cancels the delay watcher.
*/
public function __destruct()
{
Loop::cancel($this->watcher);
}
/**
* {@inheritdoc}
*/
public function subscribe(callable $callback): string
{
return $this->token->subscribe($callback);
}
/**
* {@inheritdoc}
*/
public function unsubscribe(string $id)
{
$this->token->unsubscribe($id);
}
/**
* {@inheritdoc}
*/
public function isRequested(): bool
{
return $this->token->isRequested();
}
/**
* {@inheritdoc}
*/
public function throwIfRequested()
{
$this->token->throwIfRequested();
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Amp;
/**
* Thrown if a promise doesn't resolve within a specified timeout.
*
* @see \Amp\Promise\timeout()
*/
class TimeoutException extends \Exception
{
/**
* @param string $message Exception message.
*/
public function __construct(string $message = "Operation timed out")
{
parent::__construct($message);
}
}

View File

@ -0,0 +1,815 @@
<?php
namespace Amp
{
use React\Promise\PromiseInterface as ReactPromise;
/**
* Returns a new function that wraps $callback in a promise/coroutine-aware function that automatically runs
* Generators as coroutines. The returned function always returns a promise when invoked. Errors have to be handled
* by the callback caller or they will go unnoticed.
*
* Use this function to create a coroutine-aware callable for a promise-aware callback caller.
*
* @template TReturn
* @template TPromise
* @template TGeneratorReturn
* @template TGeneratorPromise
*
* @template TGenerator as TGeneratorReturn|Promise<TGeneratorPromise>
* @template T as TReturn|Promise<TPromise>|\Generator<mixed, mixed, mixed, TGenerator>
*
* @formatter:off
*
* @param callable(...mixed): T $callback
*
* @return callable
* @psalm-return (T is Promise ? (callable(mixed...): Promise<TPromise>) : (T is \Generator ? (TGenerator is Promise ? (callable(mixed...): Promise<TGeneratorPromise>) : (callable(mixed...): Promise<TGeneratorReturn>)) : (callable(mixed...): Promise<TReturn>)))
*
* @formatter:on
*
* @see asyncCoroutine()
*
* @psalm-suppress InvalidReturnType
*/
function coroutine(callable $callback): callable
{
/** @psalm-suppress InvalidReturnStatement */
return static function (...$args) use ($callback): Promise {
return call($callback, ...$args);
};
}
/**
* Returns a new function that wraps $callback in a promise/coroutine-aware function that automatically runs
* Generators as coroutines. The returned function always returns void when invoked. Errors are forwarded to the
* loop's error handler using `Amp\Promise\rethrow()`.
*
* Use this function to create a coroutine-aware callable for a non-promise-aware callback caller.
*
* @param callable(...mixed): mixed $callback
*
* @return callable
* @psalm-return callable(mixed...): void
*
* @see coroutine()
*/
function asyncCoroutine(callable $callback): callable
{
return static function (...$args) use ($callback) {
Promise\rethrow(call($callback, ...$args));
};
}
/**
* Calls the given function, always returning a promise. If the function returns a Generator, it will be run as a
* coroutine. If the function throws, a failed promise will be returned.
*
* @template TReturn
* @template TPromise
* @template TGeneratorReturn
* @template TGeneratorPromise
*
* @template TGenerator as TGeneratorReturn|Promise<TGeneratorPromise>
* @template T as TReturn|Promise<TPromise>|\Generator<mixed, mixed, mixed, TGenerator>
*
* @formatter:off
*
* @param callable(...mixed): T $callback
* @param mixed ...$args Arguments to pass to the function.
*
* @return Promise
* @psalm-return (T is Promise ? Promise<TPromise> : (T is \Generator ? (TGenerator is Promise ? Promise<TGeneratorPromise> : Promise<TGeneratorReturn>) : Promise<TReturn>))
*
* @formatter:on
*/
function call(callable $callback, ...$args): Promise
{
try {
$result = $callback(...$args);
} catch (\Throwable $exception) {
return new Failure($exception);
}
if ($result instanceof \Generator) {
return new Coroutine($result);
}
if ($result instanceof Promise) {
return $result;
}
if ($result instanceof ReactPromise) {
return Promise\adapt($result);
}
return new Success($result);
}
/**
* Calls the given function. If the function returns a Generator, it will be run as a coroutine. If the function
* throws or returns a failing promise, the failure is forwarded to the loop error handler.
*
* @param callable(...mixed): mixed $callback
* @param mixed ...$args Arguments to pass to the function.
*
* @return void
*/
function asyncCall(callable $callback, ...$args)
{
Promise\rethrow(call($callback, ...$args));
}
/**
* Sleeps for the specified number of milliseconds.
*
* @param int $milliseconds
*
* @return Delayed
*/
function delay(int $milliseconds): Delayed
{
return new Delayed($milliseconds);
}
/**
* Returns the current time relative to an arbitrary point in time.
*
* @return int Time in milliseconds.
*/
function getCurrentTime(): int
{
return Internal\getCurrentTime();
}
}
namespace Amp\Promise
{
use Amp\Deferred;
use Amp\Loop;
use Amp\MultiReasonException;
use Amp\Promise;
use Amp\Success;
use Amp\TimeoutException;
use React\Promise\PromiseInterface as ReactPromise;
use function Amp\call;
use function Amp\Internal\createTypeError;
/**
* Registers a callback that will forward the failure reason to the event loop's error handler if the promise fails.
*
* Use this function if you neither return the promise nor handle a possible error yourself to prevent errors from
* going entirely unnoticed.
*
* @param Promise|ReactPromise $promise Promise to register the handler on.
*
* @return void
* @throws \TypeError If $promise is not an instance of \Amp\Promise or \React\Promise\PromiseInterface.
*
*/
function rethrow($promise)
{
if (!$promise instanceof Promise) {
if ($promise instanceof ReactPromise) {
$promise = adapt($promise);
} else {
throw createTypeError([Promise::class, ReactPromise::class], $promise);
}
}
$promise->onResolve(static function ($exception) {
if ($exception) {
throw $exception;
}
});
}
/**
* Runs the event loop until the promise is resolved. Should not be called within a running event loop.
*
* Use this function only in synchronous contexts to wait for an asynchronous operation. Use coroutines and yield to
* await promise resolution in a fully asynchronous application instead.
*
* @template TPromise
* @template T as Promise<TPromise>|ReactPromise
*
* @param Promise|ReactPromise $promise Promise to wait for.
*
* @return mixed Promise success value.
*
* @psalm-param T $promise
* @psalm-return (T is Promise ? TPromise : mixed)
*
* @throws \TypeError If $promise is not an instance of \Amp\Promise or \React\Promise\PromiseInterface.
* @throws \Error If the event loop stopped without the $promise being resolved.
* @throws \Throwable Promise failure reason.
*/
function wait($promise)
{
if (!$promise instanceof Promise) {
if ($promise instanceof ReactPromise) {
$promise = adapt($promise);
} else {
throw createTypeError([Promise::class, ReactPromise::class], $promise);
}
}
$resolved = false;
try {
Loop::run(function () use (&$resolved, &$value, &$exception, $promise) {
$promise->onResolve(function ($e, $v) use (&$resolved, &$value, &$exception) {
Loop::stop();
$resolved = true;
$exception = $e;
$value = $v;
});
});
} catch (\Throwable $throwable) {
throw new \Error("Loop exceptionally stopped without resolving the promise", 0, $throwable);
}
if (!$resolved) {
throw new \Error("Loop stopped without resolving the promise");
}
if ($exception) {
throw $exception;
}
return $value;
}
/**
* Creates an artificial timeout for any `Promise`.
*
* If the timeout expires before the promise is resolved, the returned promise fails with an instance of
* `Amp\TimeoutException`.
*
* @template TReturn
*
* @param Promise<TReturn>|ReactPromise $promise Promise to which the timeout is applied.
* @param int $timeout Timeout in milliseconds.
*
* @return Promise<TReturn>
*
* @throws \TypeError If $promise is not an instance of \Amp\Promise or \React\Promise\PromiseInterface.
*/
function timeout($promise, int $timeout): Promise
{
if (!$promise instanceof Promise) {
if ($promise instanceof ReactPromise) {
$promise = adapt($promise);
} else {
throw createTypeError([Promise::class, ReactPromise::class], $promise);
}
}
$deferred = new Deferred;
$watcher = Loop::delay($timeout, static function () use (&$deferred) {
$temp = $deferred; // prevent double resolve
$deferred = null;
$temp->fail(new TimeoutException);
});
Loop::unreference($watcher);
$promise->onResolve(function () use (&$deferred, $promise, $watcher) {
if ($deferred !== null) {
Loop::cancel($watcher);
$deferred->resolve($promise);
}
});
return $deferred->promise();
}
/**
* Creates an artificial timeout for any `Promise`.
*
* If the promise is resolved before the timeout expires, the result is returned
*
* If the timeout expires before the promise is resolved, a default value is returned
*
* @template TReturn
*
* @param Promise<TReturn>|ReactPromise $promise Promise to which the timeout is applied.
* @param int $timeout Timeout in milliseconds.
* @param TReturn $default
*
* @return Promise<TReturn>
*
* @throws \TypeError If $promise is not an instance of \Amp\Promise or \React\Promise\PromiseInterface.
*/
function timeoutWithDefault($promise, int $timeout, $default = null): Promise
{
$promise = timeout($promise, $timeout);
return call(static function () use ($promise, $default) {
try {
return yield $promise;
} catch (TimeoutException $exception) {
return $default;
}
});
}
/**
* Adapts any object with a done(callable $onFulfilled, callable $onRejected) or then(callable $onFulfilled,
* callable $onRejected) method to a promise usable by components depending on placeholders implementing
* \AsyncInterop\Promise.
*
* @param object $promise Object with a done() or then() method.
*
* @return Promise Promise resolved by the $thenable object.
*
* @throws \Error If the provided object does not have a then() method.
*/
function adapt($promise): Promise
{
$deferred = new Deferred;
if (\method_exists($promise, 'done')) {
$promise->done([$deferred, 'resolve'], [$deferred, 'fail']);
} elseif (\method_exists($promise, 'then')) {
$promise->then([$deferred, 'resolve'], [$deferred, 'fail']);
} else {
throw new \Error("Object must have a 'then' or 'done' method");
}
return $deferred->promise();
}
/**
* Returns a promise that is resolved when all promises are resolved. The returned promise will not fail.
* Returned promise succeeds with a two-item array delineating successful and failed promise results,
* with keys identical and corresponding to the original given array.
*
* This function is the same as some() with the notable exception that it will never fail even
* if all promises in the array resolve unsuccessfully.
*
* @param Promise[]|ReactPromise[] $promises
*
* @return Promise
*
* @throws \Error If a non-Promise is in the array.
*/
function any(array $promises): Promise
{
return some($promises, 0);
}
/**
* Returns a promise that succeeds when all promises succeed, and fails if any promise fails. Returned
* promise succeeds with an array of values used to succeed each contained promise, with keys corresponding to
* the array of promises.
*
* @param Promise[]|ReactPromise[] $promises Array of only promises.
*
* @return Promise
*
* @throws \Error If a non-Promise is in the array.
*
* @template TValue
*
* @psalm-param array<array-key, Promise<TValue>|ReactPromise> $promises
* @psalm-assert array<array-key, Promise<TValue>|ReactPromise> $promises $promises
* @psalm-return Promise<array<array-key, TValue>>
*/
function all(array $promises): Promise
{
if (empty($promises)) {
return new Success([]);
}
$deferred = new Deferred;
$result = $deferred->promise();
$pending = \count($promises);
$values = [];
foreach ($promises as $key => $promise) {
if ($promise instanceof ReactPromise) {
$promise = adapt($promise);
} elseif (!$promise instanceof Promise) {
throw createTypeError([Promise::class, ReactPromise::class], $promise);
}
$values[$key] = null; // add entry to array to preserve order
$promise->onResolve(function ($exception, $value) use (&$deferred, &$values, &$pending, $key) {
if ($pending === 0) {
return;
}
if ($exception) {
$pending = 0;
$deferred->fail($exception);
$deferred = null;
return;
}
$values[$key] = $value;
if (0 === --$pending) {
$deferred->resolve($values);
}
});
}
return $result;
}
/**
* Returns a promise that succeeds when the first promise succeeds, and fails only if all promises fail.
*
* @param Promise[]|ReactPromise[] $promises Array of only promises.
*
* @return Promise
*
* @throws \Error If the array is empty or a non-Promise is in the array.
*/
function first(array $promises): Promise
{
if (empty($promises)) {
throw new \Error("No promises provided");
}
$deferred = new Deferred;
$result = $deferred->promise();
$pending = \count($promises);
$exceptions = [];
foreach ($promises as $key => $promise) {
if ($promise instanceof ReactPromise) {
$promise = adapt($promise);
} elseif (!$promise instanceof Promise) {
throw createTypeError([Promise::class, ReactPromise::class], $promise);
}
$exceptions[$key] = null; // add entry to array to preserve order
$promise->onResolve(function ($error, $value) use (&$deferred, &$exceptions, &$pending, $key) {
if ($pending === 0) {
return;
}
if (!$error) {
$pending = 0;
$deferred->resolve($value);
$deferred = null;
return;
}
$exceptions[$key] = $error;
if (0 === --$pending) {
$deferred->fail(new MultiReasonException($exceptions));
}
});
}
return $result;
}
/**
* Resolves with a two-item array delineating successful and failed Promise results.
*
* The returned promise will only fail if the given number of required promises fail.
*
* @param Promise[]|ReactPromise[] $promises Array of only promises.
* @param int $required Number of promises that must succeed for the
* returned promise to succeed.
*
* @return Promise
*
* @throws \Error If a non-Promise is in the array.
*/
function some(array $promises, int $required = 1): Promise
{
if ($required < 0) {
throw new \Error("Number of promises required must be non-negative");
}
$pending = \count($promises);
if ($required > $pending) {
throw new \Error("Too few promises provided");
}
if (empty($promises)) {
return new Success([[], []]);
}
$deferred = new Deferred;
$result = $deferred->promise();
$values = [];
$exceptions = [];
foreach ($promises as $key => $promise) {
if ($promise instanceof ReactPromise) {
$promise = adapt($promise);
} elseif (!$promise instanceof Promise) {
throw createTypeError([Promise::class, ReactPromise::class], $promise);
}
$values[$key] = $exceptions[$key] = null; // add entry to arrays to preserve order
$promise->onResolve(static function ($exception, $value) use (
&$values,
&$exceptions,
&$pending,
$key,
$required,
$deferred
) {
if ($exception) {
$exceptions[$key] = $exception;
unset($values[$key]);
} else {
$values[$key] = $value;
unset($exceptions[$key]);
}
if (0 === --$pending) {
if (\count($values) < $required) {
$deferred->fail(new MultiReasonException($exceptions));
} else {
$deferred->resolve([$exceptions, $values]);
}
}
});
}
return $result;
}
/**
* Wraps a promise into another promise, altering the exception or result.
*
* @param Promise|ReactPromise $promise
* @param callable $callback
*
* @return Promise
*/
function wrap($promise, callable $callback): Promise
{
if ($promise instanceof ReactPromise) {
$promise = adapt($promise);
} elseif (!$promise instanceof Promise) {
throw createTypeError([Promise::class, ReactPromise::class], $promise);
}
$deferred = new Deferred();
$promise->onResolve(static function (\Throwable $exception = null, $result) use ($deferred, $callback) {
try {
$result = $callback($exception, $result);
} catch (\Throwable $exception) {
$deferred->fail($exception);
return;
}
$deferred->resolve($result);
});
return $deferred->promise();
}
}
namespace Amp\Iterator
{
use Amp\Delayed;
use Amp\Emitter;
use Amp\Iterator;
use Amp\Producer;
use Amp\Promise;
use function Amp\call;
use function Amp\coroutine;
use function Amp\Internal\createTypeError;
/**
* Creates an iterator from the given iterable, emitting the each value. The iterable may contain promises. If any
* promise fails, the iterator will fail with the same reason.
*
* @param array|\Traversable $iterable Elements to emit.
* @param int $delay Delay between element emissions in milliseconds.
*
* @return Iterator
*
* @throws \TypeError If the argument is not an array or instance of \Traversable.
*/
function fromIterable(/* iterable */
$iterable,
int $delay = 0
): Iterator {
if (!$iterable instanceof \Traversable && !\is_array($iterable)) {
throw createTypeError(["array", "Traversable"], $iterable);
}
if ($delay) {
return new Producer(static function (callable $emit) use ($iterable, $delay) {
foreach ($iterable as $value) {
yield new Delayed($delay);
yield $emit($value);
}
});
}
return new Producer(static function (callable $emit) use ($iterable) {
foreach ($iterable as $value) {
yield $emit($value);
}
});
}
/**
* @template TValue
* @template TReturn
*
* @param Iterator<TValue> $iterator
* @param callable (TValue $value): TReturn $onEmit
*
* @return Iterator<TReturn>
*/
function map(Iterator $iterator, callable $onEmit): Iterator
{
return new Producer(static function (callable $emit) use ($iterator, $onEmit) {
while (yield $iterator->advance()) {
yield $emit($onEmit($iterator->getCurrent()));
}
});
}
/**
* @template TValue
*
* @param Iterator<TValue> $iterator
* @param callable(TValue $value):bool $filter
*
* @return Iterator<TValue>
*/
function filter(Iterator $iterator, callable $filter): Iterator
{
return new Producer(static function (callable $emit) use ($iterator, $filter) {
while (yield $iterator->advance()) {
if ($filter($iterator->getCurrent())) {
yield $emit($iterator->getCurrent());
}
}
});
}
/**
* Creates an iterator that emits values emitted from any iterator in the array of iterators.
*
* @param Iterator[] $iterators
*
* @return Iterator
*/
function merge(array $iterators): Iterator
{
$emitter = new Emitter;
$result = $emitter->iterate();
$coroutine = coroutine(static function (Iterator $iterator) use (&$emitter) {
while ((yield $iterator->advance()) && $emitter !== null) {
yield $emitter->emit($iterator->getCurrent());
}
});
$coroutines = [];
foreach ($iterators as $iterator) {
if (!$iterator instanceof Iterator) {
throw createTypeError([Iterator::class], $iterator);
}
$coroutines[] = $coroutine($iterator);
}
Promise\all($coroutines)->onResolve(static function ($exception) use (&$emitter) {
if ($exception) {
$emitter->fail($exception);
$emitter = null;
} else {
$emitter->complete();
}
});
return $result;
}
/**
* Concatenates the given iterators into a single iterator, emitting values from a single iterator at a time. The
* prior iterator must complete before values are emitted from any subsequent iterators. Iterators are concatenated
* in the order given (iteration order of the array).
*
* @param Iterator[] $iterators
*
* @return Iterator
*/
function concat(array $iterators): Iterator
{
foreach ($iterators as $iterator) {
if (!$iterator instanceof Iterator) {
throw createTypeError([Iterator::class], $iterator);
}
}
$emitter = new Emitter;
$previous = [];
$promise = Promise\all($previous);
$coroutine = coroutine(static function (Iterator $iterator, callable $emit) {
while (yield $iterator->advance()) {
yield $emit($iterator->getCurrent());
}
});
foreach ($iterators as $iterator) {
$emit = coroutine(static function ($value) use ($emitter, $promise) {
static $pending = true, $failed = false;
if ($failed) {
return;
}
if ($pending) {
try {
yield $promise;
$pending = false;
} catch (\Throwable $exception) {
$failed = true;
return; // Prior iterator failed.
}
}
yield $emitter->emit($value);
});
$previous[] = $coroutine($iterator, $emit);
$promise = Promise\all($previous);
}
$promise->onResolve(static function ($exception) use ($emitter) {
if ($exception) {
$emitter->fail($exception);
return;
}
$emitter->complete();
});
return $emitter->iterate();
}
/**
* Discards all remaining items and returns the number of discarded items.
*
* @template TValue
*
* @param Iterator $iterator
*
* @return Promise
*
* @psalm-param Iterator<TValue> $iterator
* @psalm-return Promise<int>
*/
function discard(Iterator $iterator): Promise
{
return call(static function () use ($iterator): \Generator {
$count = 0;
while (yield $iterator->advance()) {
$count++;
}
return $count;
});
}
/**
* Collects all items from an iterator into an array.
*
* @template TValue
*
* @param Iterator $iterator
*
* @psalm-param Iterator<TValue> $iterator
*
* @return Promise
* @psalm-return Promise<array<array-key, TValue>>
*/
function toArray(Iterator $iterator): Promise
{
return call(static function () use ($iterator) {
/** @psalm-var list $array */
$array = [];
while (yield $iterator->advance()) {
$array[] = $iterator->getCurrent();
}
return $array;
});
}
}

View File

@ -0,0 +1,16 @@
<?xml version="1.0"?>
<psalm
totallyTyped="true"
phpVersion="7.0"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="examples/psalm"/>
<ignoreFiles>
<directory name="vendor"/>
</ignoreFiles>
</projectFiles>
</psalm>

View File

@ -0,0 +1,69 @@
<?xml version="1.0"?>
<psalm
totallyTyped="false"
errorLevel="2"
phpVersion="7.0"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="examples"/>
<ignoreFiles>
<directory name="examples/psalm"/>
</ignoreFiles>
<directory name="lib"/>
<ignoreFiles>
<directory name="vendor"/>
</ignoreFiles>
</projectFiles>
<issueHandlers>
<DuplicateClass>
<errorLevel type="suppress">
<file name="lib/CallableMaker.php"/>
</errorLevel>
</DuplicateClass>
<StringIncrement>
<errorLevel type="suppress">
<directory name="examples"/>
<directory name="lib"/>
</errorLevel>
</StringIncrement>
<RedundantConditionGivenDocblockType>
<errorLevel type="suppress">
<directory name="lib"/>
</errorLevel>
</RedundantConditionGivenDocblockType>
<DocblockTypeContradiction>
<errorLevel type="suppress">
<directory name="lib"/>
</errorLevel>
</DocblockTypeContradiction>
<MissingClosureParamType>
<errorLevel type="suppress">
<directory name="examples"/>
<directory name="lib"/>
</errorLevel>
</MissingClosureParamType>
<MissingClosureReturnType>
<errorLevel type="suppress">
<directory name="examples"/>
<directory name="lib"/>
</errorLevel>
</MissingClosureReturnType>
</issueHandlers>
<stubs>
<file name="vendor/jetbrains/phpstorm-stubs/Ev/Ev.php"/>
<file name="vendor/jetbrains/phpstorm-stubs/event/event.php"/>
<file name="vendor/jetbrains/phpstorm-stubs/uv/UV.php"/>
<file name="vendor/jetbrains/phpstorm-stubs/uv/uv_functions.php"/>
</stubs>
</psalm>

View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2016-2020 amphp
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

@ -0,0 +1,58 @@
{
"name": "amphp/byte-stream",
"homepage": "http://amphp.org/byte-stream",
"description": "A stream abstraction to make working with non-blocking I/O simple.",
"support": {
"issues": "https://github.com/amphp/byte-stream/issues",
"irc": "irc://irc.freenode.org/amphp"
},
"keywords": [
"stream",
"async",
"non-blocking",
"amp",
"amphp",
"io"
],
"license": "MIT",
"authors": [
{
"name": "Aaron Piotrowski",
"email": "aaron@trowski.com"
},
{
"name": "Niklas Keller",
"email": "me@kelunik.com"
}
],
"require": {
"php": ">=7.1",
"amphp/amp": "^2"
},
"require-dev": {
"amphp/phpunit-util": "^1.4",
"phpunit/phpunit": "^6 || ^7 || ^8",
"friendsofphp/php-cs-fixer": "^2.3",
"amphp/php-cs-fixer-config": "dev-master",
"psalm/phar": "^3.11.4",
"jetbrains/phpstorm-stubs": "^2019.3"
},
"autoload": {
"psr-4": {
"Amp\\ByteStream\\": "lib"
},
"files": [
"lib/functions.php"
]
},
"autoload-dev": {
"psr-4": {
"Amp\\ByteStream\\Test\\": "test"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
}
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace Amp\ByteStream\Base64;
use Amp\ByteStream\InputStream;
use Amp\ByteStream\StreamException;
use Amp\Promise;
use function Amp\call;
final class Base64DecodingInputStream implements InputStream
{
/** @var InputStream|null */
private $source;
/** @var string|null */
private $buffer = '';
public function __construct(InputStream $source)
{
$this->source = $source;
}
public function read(): Promise
{
return call(function () {
if ($this->source === null) {
throw new StreamException('Failed to read stream chunk due to invalid base64 data');
}
$chunk = yield $this->source->read();
if ($chunk === null) {
if ($this->buffer === null) {
return null;
}
$chunk = \base64_decode($this->buffer, true);
if ($chunk === false) {
$this->source = null;
$this->buffer = null;
throw new StreamException('Failed to read stream chunk due to invalid base64 data');
}
$this->buffer = null;
return $chunk;
}
$this->buffer .= $chunk;
$length = \strlen($this->buffer);
$chunk = \base64_decode(\substr($this->buffer, 0, $length - $length % 4), true);
if ($chunk === false) {
$this->source = null;
$this->buffer = null;
throw new StreamException('Failed to read stream chunk due to invalid base64 data');
}
$this->buffer = \substr($this->buffer, $length - $length % 4);
return $chunk;
});
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace Amp\ByteStream\Base64;
use Amp\ByteStream\OutputStream;
use Amp\ByteStream\StreamException;
use Amp\Failure;
use Amp\Promise;
final class Base64DecodingOutputStream implements OutputStream
{
/** @var OutputStream */
private $destination;
/** @var string */
private $buffer = '';
/** @var int */
private $offset = 0;
public function __construct(OutputStream $destination)
{
$this->destination = $destination;
}
public function write(string $data): Promise
{
$this->buffer .= $data;
$length = \strlen($this->buffer);
$chunk = \base64_decode(\substr($this->buffer, 0, $length - $length % 4), true);
if ($chunk === false) {
return new Failure(new StreamException('Invalid base64 near offset ' . $this->offset));
}
$this->offset += $length - $length % 4;
$this->buffer = \substr($this->buffer, $length - $length % 4);
return $this->destination->write($chunk);
}
public function end(string $finalData = ""): Promise
{
$this->offset += \strlen($this->buffer);
$chunk = \base64_decode($this->buffer . $finalData, true);
if ($chunk === false) {
return new Failure(new StreamException('Invalid base64 near offset ' . $this->offset));
}
$this->buffer = '';
return $this->destination->end($chunk);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace Amp\ByteStream\Base64;
use Amp\ByteStream\InputStream;
use Amp\Promise;
use function Amp\call;
final class Base64EncodingInputStream implements InputStream
{
/** @var InputStream */
private $source;
/** @var string|null */
private $buffer = '';
public function __construct(InputStream $source)
{
$this->source = $source;
}
public function read(): Promise
{
return call(function () {
$chunk = yield $this->source->read();
if ($chunk === null) {
if ($this->buffer === null) {
return null;
}
$chunk = \base64_encode($this->buffer);
$this->buffer = null;
return $chunk;
}
$this->buffer .= $chunk;
$length = \strlen($this->buffer);
$chunk = \base64_encode(\substr($this->buffer, 0, $length - $length % 3));
$this->buffer = \substr($this->buffer, $length - $length % 3);
return $chunk;
});
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace Amp\ByteStream\Base64;
use Amp\ByteStream\OutputStream;
use Amp\Promise;
final class Base64EncodingOutputStream implements OutputStream
{
/** @var OutputStream */
private $destination;
/** @var string */
private $buffer = '';
public function __construct(OutputStream $destination)
{
$this->destination = $destination;
}
public function write(string $data): Promise
{
$this->buffer .= $data;
$length = \strlen($this->buffer);
$chunk = \base64_encode(\substr($this->buffer, 0, $length - $length % 3));
$this->buffer = \substr($this->buffer, $length - $length % 3);
return $this->destination->write($chunk);
}
public function end(string $finalData = ""): Promise
{
$chunk = \base64_encode($this->buffer . $finalData);
$this->buffer = '';
return $this->destination->end($chunk);
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace Amp\ByteStream;
final class ClosedException extends StreamException
{
}

View File

@ -0,0 +1,39 @@
<?php
namespace Amp\ByteStream;
use Amp\Promise;
use Amp\Success;
/**
* Input stream with a single already known data chunk.
*/
final class InMemoryStream implements InputStream
{
private $contents;
/**
* @param string|null $contents Data chunk or `null` for no data chunk.
*/
public function __construct(string $contents = null)
{
$this->contents = $contents;
}
/**
* Reads data from the stream.
*
* @return Promise<string|null> Resolves with the full contents or `null` if the stream has closed / already been consumed.
*/
public function read(): Promise
{
if ($this->contents === null) {
return new Success;
}
$promise = new Success($this->contents);
$this->contents = null;
return $promise;
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Amp\ByteStream;
use Amp\Promise;
/**
* An `InputStream` allows reading byte streams in chunks.
*
* **Example**
*
* ```php
* function readAll(InputStream $in): Promise {
* return Amp\call(function () use ($in) {
* $buffer = "";
*
* while (($chunk = yield $in->read()) !== null) {
* $buffer .= $chunk;
* }
*
* return $buffer;
* });
* }
* ```
*/
interface InputStream
{
/**
* Reads data from the stream.
*
* @return Promise Resolves with a string when new data is available or `null` if the stream has closed.
*
* @psalm-return Promise<string|null>
*
* @throws PendingReadError Thrown if another read operation is still pending.
*/
public function read(): Promise;
}

View File

@ -0,0 +1,52 @@
<?php
namespace Amp\ByteStream;
use Amp\Promise;
use Amp\Success;
use function Amp\call;
final class InputStreamChain implements InputStream
{
/** @var InputStream[] */
private $streams;
/** @var bool */
private $reading = false;
public function __construct(InputStream ...$streams)
{
$this->streams = $streams;
}
/** @inheritDoc */
public function read(): Promise
{
if ($this->reading) {
throw new PendingReadError;
}
if (!$this->streams) {
return new Success(null);
}
return call(function () {
$this->reading = true;
try {
while ($this->streams) {
$chunk = yield $this->streams[0]->read();
if ($chunk === null) {
\array_shift($this->streams);
continue;
}
return $chunk;
}
return null;
} finally {
$this->reading = false;
}
});
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace Amp\ByteStream;
use Amp\Deferred;
use Amp\Failure;
use Amp\Iterator;
use Amp\Promise;
final class IteratorStream implements InputStream
{
/** @var Iterator<string> */
private $iterator;
/** @var \Throwable|null */
private $exception;
/** @var bool */
private $pending = false;
/**
* @psam-param Iterator<string> $iterator
*/
public function __construct(Iterator $iterator)
{
$this->iterator = $iterator;
}
/** @inheritdoc */
public function read(): Promise
{
if ($this->exception) {
return new Failure($this->exception);
}
if ($this->pending) {
throw new PendingReadError;
}
$this->pending = true;
/** @var Deferred<string|null> $deferred */
$deferred = new Deferred;
$this->iterator->advance()->onResolve(function ($error, $hasNextElement) use ($deferred) {
$this->pending = false;
if ($error) {
$this->exception = $error;
$deferred->fail($error);
} elseif ($hasNextElement) {
$chunk = $this->iterator->getCurrent();
if (!\is_string($chunk)) {
$this->exception = new StreamException(\sprintf(
"Unexpected iterator value of type '%s', expected string",
\is_object($chunk) ? \get_class($chunk) : \gettype($chunk)
));
$deferred->fail($this->exception);
return;
}
$deferred->resolve($chunk);
} else {
$deferred->resolve();
}
});
return $deferred->promise();
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace Amp\ByteStream;
use Amp\Promise;
use function Amp\call;
final class LineReader
{
/** @var string */
private $delimiter;
/** @var bool */
private $lineMode;
/** @var string */
private $buffer = "";
/** @var InputStream */
private $source;
public function __construct(InputStream $inputStream, string $delimiter = null)
{
$this->source = $inputStream;
$this->delimiter = $delimiter === null ? "\n" : $delimiter;
$this->lineMode = $delimiter === null;
}
/**
* @return Promise<string|null>
*/
public function readLine(): Promise
{
return call(function () {
if (false !== \strpos($this->buffer, $this->delimiter)) {
list($line, $this->buffer) = \explode($this->delimiter, $this->buffer, 2);
return $this->lineMode ? \rtrim($line, "\r") : $line;
}
while (null !== $chunk = yield $this->source->read()) {
$this->buffer .= $chunk;
if (false !== \strpos($this->buffer, $this->delimiter)) {
list($line, $this->buffer) = \explode($this->delimiter, $this->buffer, 2);
return $this->lineMode ? \rtrim($line, "\r") : $line;
}
}
if ($this->buffer === "") {
return null;
}
$line = $this->buffer;
$this->buffer = "";
return $this->lineMode ? \rtrim($line, "\r") : $line;
});
}
public function getBuffer(): string
{
return $this->buffer;
}
/**
* @return void
*/
public function clearBuffer()
{
$this->buffer = "";
}
}

View File

@ -0,0 +1,176 @@
<?php
namespace Amp\ByteStream;
use Amp\Coroutine;
use Amp\Deferred;
use Amp\Failure;
use Amp\Promise;
use Amp\Success;
/**
* Creates a buffered message from an InputStream. The message can be consumed in chunks using the read() API or it may
* be buffered and accessed in its entirety by waiting for the promise to resolve.
*
* Other implementations may extend this class to add custom properties such as a `isBinary()` flag for WebSocket
* messages.
*
* Buffering Example:
*
* $stream = new Message($inputStream);
* $content = yield $stream;
*
* Streaming Example:
*
* $stream = new Message($inputStream);
*
* while (($chunk = yield $stream->read()) !== null) {
* // Immediately use $chunk, reducing memory consumption since the entire message is never buffered.
* }
*
* @deprecated Use Amp\ByteStream\Payload instead.
*/
class Message implements InputStream, Promise
{
/** @var InputStream */
private $source;
/** @var string */
private $buffer = "";
/** @var Deferred|null */
private $pendingRead;
/** @var Coroutine|null */
private $coroutine;
/** @var bool True if onResolve() has been called. */
private $buffering = false;
/** @var Deferred|null */
private $backpressure;
/** @var bool True if the iterator has completed. */
private $complete = false;
/** @var \Throwable|null Used to fail future reads on failure. */
private $error;
/**
* @param InputStream $source An iterator that only emits strings.
*/
public function __construct(InputStream $source)
{
$this->source = $source;
}
private function consume(): \Generator
{
while (($chunk = yield $this->source->read()) !== null) {
$buffer = $this->buffer .= $chunk;
if ($buffer === "") {
continue; // Do not succeed reads with empty string.
} elseif ($this->pendingRead) {
$deferred = $this->pendingRead;
$this->pendingRead = null;
$this->buffer = "";
$deferred->resolve($buffer);
$buffer = ""; // Destroy last emitted chunk to free memory.
} elseif (!$this->buffering) {
$buffer = ""; // Destroy last emitted chunk to free memory.
$this->backpressure = new Deferred;
yield $this->backpressure->promise();
}
}
$this->complete = true;
if ($this->pendingRead) {
$deferred = $this->pendingRead;
$this->pendingRead = null;
$deferred->resolve($this->buffer !== "" ? $this->buffer : null);
$this->buffer = "";
}
return $this->buffer;
}
/** @inheritdoc */
final public function read(): Promise
{
if ($this->pendingRead) {
throw new PendingReadError;
}
if ($this->coroutine === null) {
$this->coroutine = new Coroutine($this->consume());
$this->coroutine->onResolve(function ($error) {
if ($error) {
$this->error = $error;
}
if ($this->pendingRead) {
$deferred = $this->pendingRead;
$this->pendingRead = null;
$deferred->fail($error);
}
});
}
if ($this->error) {
return new Failure($this->error);
}
if ($this->buffer !== "") {
$buffer = $this->buffer;
$this->buffer = "";
if ($this->backpressure) {
$backpressure = $this->backpressure;
$this->backpressure = null;
$backpressure->resolve();
}
return new Success($buffer);
}
if ($this->complete) {
return new Success;
}
$this->pendingRead = new Deferred;
return $this->pendingRead->promise();
}
/** @inheritdoc */
final public function onResolve(callable $onResolved)
{
$this->buffering = true;
if ($this->coroutine === null) {
$this->coroutine = new Coroutine($this->consume());
}
if ($this->backpressure) {
$backpressure = $this->backpressure;
$this->backpressure = null;
$backpressure->resolve();
}
$this->coroutine->onResolve($onResolved);
}
/**
* Exposes the source input stream.
*
* This might be required to resolve a promise with an InputStream, because promises in Amp can't be resolved with
* other promises.
*
* @return InputStream
*/
final public function getInputStream(): InputStream
{
return $this->source;
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace Amp\ByteStream;
use Amp\Deferred;
use Amp\Promise;
use Amp\Success;
class OutputBuffer implements OutputStream, Promise
{
/** @var Deferred */
private $deferred;
/** @var string */
private $contents = '';
/** @var bool */
private $closed = false;
public function __construct()
{
$this->deferred = new Deferred;
}
public function write(string $data): Promise
{
if ($this->closed) {
throw new ClosedException("The stream has already been closed.");
}
$this->contents .= $data;
return new Success(\strlen($data));
}
public function end(string $finalData = ""): Promise
{
if ($this->closed) {
throw new ClosedException("The stream has already been closed.");
}
$this->contents .= $finalData;
$this->closed = true;
$this->deferred->resolve($this->contents);
$this->contents = "";
return new Success(\strlen($finalData));
}
public function onResolve(callable $onResolved)
{
$this->deferred->promise()->onResolve($onResolved);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Amp\ByteStream;
use Amp\Promise;
/**
* An `OutputStream` allows writing data in chunks. Writers can wait on the returned promises to feel the backpressure.
*/
interface OutputStream
{
/**
* Writes data to the stream.
*
* @param string $data Bytes to write.
*
* @return Promise Succeeds once the data has been successfully written to the stream.
*
* @throws ClosedException If the stream has already been closed.
* @throws StreamException If writing to the stream fails.
*/
public function write(string $data): Promise;
/**
* Marks the stream as no longer writable. Optionally writes a final data chunk before. Note that this is not the
* same as forcefully closing the stream. This method waits for all pending writes to complete before closing the
* stream. Socket streams implementing this interface should only close the writable side of the stream.
*
* @param string $finalData Bytes to write.
*
* @return Promise Succeeds once the data has been successfully written to the stream.
*
* @throws ClosedException If the stream has already been closed.
* @throws StreamException If writing to the stream fails.
*/
public function end(string $finalData = ""): Promise;
}

View File

@ -0,0 +1,92 @@
<?php
namespace Amp\ByteStream;
use Amp\Coroutine;
use Amp\Promise;
use function Amp\call;
/**
* Creates a buffered message from an InputStream. The message can be consumed in chunks using the read() API or it may
* be buffered and accessed in its entirety by calling buffer(). Once buffering is requested through buffer(), the
* stream cannot be read in chunks. On destruct any remaining data is read from the InputStream given to this class.
*/
class Payload implements InputStream
{
/** @var InputStream */
private $stream;
/** @var \Amp\Promise|null */
private $promise;
/** @var \Amp\Promise|null */
private $lastRead;
/**
* @param \Amp\ByteStream\InputStream $stream
*/
public function __construct(InputStream $stream)
{
$this->stream = $stream;
}
public function __destruct()
{
if (!$this->promise) {
Promise\rethrow(new Coroutine($this->consume()));
}
}
private function consume(): \Generator
{
try {
if ($this->lastRead && null === yield $this->lastRead) {
return;
}
while (null !== yield $this->stream->read()) {
// Discard unread bytes from message.
}
} catch (\Throwable $exception) {
// If exception is thrown here the connection closed anyway.
}
}
/**
* @inheritdoc
*
* @throws \Error If a buffered message was requested by calling buffer().
*/
final public function read(): Promise
{
if ($this->promise) {
throw new \Error("Cannot stream message data once a buffered message has been requested");
}
return $this->lastRead = $this->stream->read();
}
/**
* Buffers the entire message and resolves the returned promise then.
*
* @return Promise<string> Resolves with the entire message contents.
*/
final public function buffer(): Promise
{
if ($this->promise) {
return $this->promise;
}
return $this->promise = call(function () {
$buffer = '';
if ($this->lastRead && null === yield $this->lastRead) {
return $buffer;
}
while (null !== $chunk = yield $this->stream->read()) {
$buffer .= $chunk;
}
return $buffer;
});
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Amp\ByteStream;
/**
* Thrown in case a second read operation is attempted while another read operation is still pending.
*/
final class PendingReadError extends \Error
{
public function __construct(
string $message = "The previous read operation must complete before read can be called again",
int $code = 0,
\Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,262 @@
<?php
namespace Amp\ByteStream;
use Amp\Deferred;
use Amp\Loop;
use Amp\Promise;
use Amp\Success;
/**
* Input stream abstraction for PHP's stream resources.
*/
final class ResourceInputStream implements InputStream
{
const DEFAULT_CHUNK_SIZE = 8192;
/** @var resource|null */
private $resource;
/** @var string */
private $watcher;
/** @var Deferred|null */
private $deferred;
/** @var bool */
private $readable = true;
/** @var int */
private $chunkSize;
/** @var bool */
private $useSingleRead;
/** @var callable */
private $immediateCallable;
/** @var string|null */
private $immediateWatcher;
/**
* @param resource $stream Stream resource.
* @param int $chunkSize Chunk size per read operation.
*
* @throws \Error If an invalid stream or parameter has been passed.
*/
public function __construct($stream, int $chunkSize = self::DEFAULT_CHUNK_SIZE)
{
if (!\is_resource($stream) || \get_resource_type($stream) !== 'stream') {
throw new \Error("Expected a valid stream");
}
$meta = \stream_get_meta_data($stream);
$useSingleRead = $meta["stream_type"] === "udp_socket" || $meta["stream_type"] === "STDIO";
$this->useSingleRead = $useSingleRead;
if (\strpos($meta["mode"], "r") === false && \strpos($meta["mode"], "+") === false) {
throw new \Error("Expected a readable stream");
}
\stream_set_blocking($stream, false);
\stream_set_read_buffer($stream, 0);
$this->resource = &$stream;
$this->chunkSize = &$chunkSize;
$deferred = &$this->deferred;
$readable = &$this->readable;
$this->watcher = Loop::onReadable($this->resource, static function ($watcher) use (
&$deferred,
&$readable,
&$stream,
&$chunkSize,
$useSingleRead
) {
if ($useSingleRead) {
$data = @\fread($stream, $chunkSize);
} else {
$data = @\stream_get_contents($stream, $chunkSize);
}
\assert($data !== false, "Trying to read from a previously fclose()'d resource. Do NOT manually fclose() resources the loop still has a reference to.");
// Error suppression, because pthreads does crazy things with resources,
// which might be closed during two operations.
// See https://github.com/amphp/byte-stream/issues/32
if ($data === '' && @\feof($stream)) {
$readable = false;
$stream = null;
$data = null; // Stream closed, resolve read with null.
Loop::cancel($watcher);
} else {
Loop::disable($watcher);
}
$temp = $deferred;
$deferred = null;
\assert($temp instanceof Deferred);
$temp->resolve($data);
});
$this->immediateCallable = static function ($watcherId, $data) use (&$deferred) {
$temp = $deferred;
$deferred = null;
\assert($temp instanceof Deferred);
$temp->resolve($data);
};
Loop::disable($this->watcher);
}
/** @inheritdoc */
public function read(): Promise
{
if ($this->deferred !== null) {
throw new PendingReadError;
}
if (!$this->readable) {
return new Success; // Resolve with null on closed stream.
}
\assert($this->resource !== null);
// Attempt a direct read, because Windows suffers from slow I/O on STDIN otherwise.
if ($this->useSingleRead) {
$data = @\fread($this->resource, $this->chunkSize);
} else {
$data = @\stream_get_contents($this->resource, $this->chunkSize);
}
\assert($data !== false, "Trying to read from a previously fclose()'d resource. Do NOT manually fclose() resources the loop still has a reference to.");
if ($data === '') {
// Error suppression, because pthreads does crazy things with resources,
// which might be closed during two operations.
// See https://github.com/amphp/byte-stream/issues/32
if (@\feof($this->resource)) {
$this->readable = false;
$this->resource = null;
Loop::cancel($this->watcher);
return new Success; // Stream closed, resolve read with null.
}
$this->deferred = new Deferred;
Loop::enable($this->watcher);
return $this->deferred->promise();
}
// Prevent an immediate read → write loop from blocking everything
// See e.g. examples/benchmark-throughput.php
$this->deferred = new Deferred;
$this->immediateWatcher = Loop::defer($this->immediateCallable, $data);
return $this->deferred->promise();
}
/**
* Closes the stream forcefully. Multiple `close()` calls are ignored.
*
* @return void
*/
public function close()
{
if ($this->resource) {
// Error suppression, as resource might already be closed
$meta = @\stream_get_meta_data($this->resource);
if ($meta && \strpos($meta["mode"], "+") !== false) {
@\stream_socket_shutdown($this->resource, \STREAM_SHUT_RD);
} else {
/** @psalm-suppress InvalidPropertyAssignmentValue */
@\fclose($this->resource);
}
}
$this->free();
}
/**
* Nulls reference to resource, marks stream unreadable, and succeeds any pending read with null.
*
* @return void
*/
private function free()
{
$this->readable = false;
$this->resource = null;
if ($this->deferred !== null) {
$deferred = $this->deferred;
$this->deferred = null;
$deferred->resolve();
}
Loop::cancel($this->watcher);
if ($this->immediateWatcher !== null) {
Loop::cancel($this->immediateWatcher);
}
}
/**
* @return resource|null The stream resource or null if the stream has closed.
*/
public function getResource()
{
return $this->resource;
}
/**
* @return void
*/
public function setChunkSize(int $chunkSize)
{
$this->chunkSize = $chunkSize;
}
/**
* References the read watcher, so the loop keeps running in case there's an active read.
*
* @return void
*
* @see Loop::reference()
*/
public function reference()
{
if (!$this->resource) {
throw new \Error("Resource has already been freed");
}
Loop::reference($this->watcher);
}
/**
* Unreferences the read watcher, so the loop doesn't keep running even if there are active reads.
*
* @return void
*
* @see Loop::unreference()
*/
public function unreference()
{
if (!$this->resource) {
throw new \Error("Resource has already been freed");
}
Loop::unreference($this->watcher);
}
public function __destruct()
{
if ($this->resource !== null) {
$this->free();
}
}
}

View File

@ -0,0 +1,321 @@
<?php
namespace Amp\ByteStream;
use Amp\Deferred;
use Amp\Failure;
use Amp\Loop;
use Amp\Promise;
use Amp\Success;
/**
* Output stream abstraction for PHP's stream resources.
*/
final class ResourceOutputStream implements OutputStream
{
const MAX_CONSECUTIVE_EMPTY_WRITES = 3;
const LARGE_CHUNK_SIZE = 128 * 1024;
/** @var resource|null */
private $resource;
/** @var string */
private $watcher;
/** @var \SplQueue<array> */
private $writes;
/** @var bool */
private $writable = true;
/** @var int|null */
private $chunkSize;
/**
* @param resource $stream Stream resource.
* @param int|null $chunkSize Chunk size per `fwrite()` operation.
*/
public function __construct($stream, int $chunkSize = null)
{
if (!\is_resource($stream) || \get_resource_type($stream) !== 'stream') {
throw new \Error("Expected a valid stream");
}
$meta = \stream_get_meta_data($stream);
if (\strpos($meta["mode"], "r") !== false && \strpos($meta["mode"], "+") === false) {
throw new \Error("Expected a writable stream");
}
\stream_set_blocking($stream, false);
\stream_set_write_buffer($stream, 0);
$this->resource = $stream;
$this->chunkSize = &$chunkSize;
$writes = $this->writes = new \SplQueue;
$writable = &$this->writable;
$resource = &$this->resource;
$this->watcher = Loop::onWritable($stream, static function ($watcher, $stream) use ($writes, &$chunkSize, &$writable, &$resource) {
static $emptyWrites = 0;
try {
while (!$writes->isEmpty()) {
/** @var Deferred $deferred */
list($data, $previous, $deferred) = $writes->shift();
$length = \strlen($data);
if ($length === 0) {
$deferred->resolve(0);
continue;
}
if (!\is_resource($stream) || (($metaData = @\stream_get_meta_data($stream)) && $metaData['eof'])) {
throw new ClosedException("The stream was closed by the peer");
}
// Error reporting suppressed since fwrite() emits E_WARNING if the pipe is broken or the buffer is full.
// Use conditional, because PHP doesn't like getting null passed
if ($chunkSize) {
$written = @\fwrite($stream, $data, $chunkSize);
} else {
$written = @\fwrite($stream, $data);
}
\assert(
$written !== false || \PHP_VERSION_ID >= 70400, // PHP 7.4+ returns false on EPIPE.
"Trying to write on a previously fclose()'d resource. Do NOT manually fclose() resources the still referenced in the loop."
);
// PHP 7.4.0 and 7.4.1 may return false on EAGAIN.
if ($written === false && \PHP_VERSION_ID >= 70402) {
$message = "Failed to write to stream";
if ($error = \error_get_last()) {
$message .= \sprintf("; %s", $error["message"]);
}
throw new StreamException($message);
}
// Broken pipes between processes on macOS/FreeBSD do not detect EOF properly.
if ($written === 0 || $written === false) {
if ($emptyWrites++ > self::MAX_CONSECUTIVE_EMPTY_WRITES) {
$message = "Failed to write to stream after multiple attempts";
if ($error = \error_get_last()) {
$message .= \sprintf("; %s", $error["message"]);
}
throw new StreamException($message);
}
$writes->unshift([$data, $previous, $deferred]);
return;
}
$emptyWrites = 0;
if ($length > $written) {
$data = \substr($data, $written);
$writes->unshift([$data, $written + $previous, $deferred]);
return;
}
$deferred->resolve($written + $previous);
}
} catch (\Throwable $exception) {
$resource = null;
$writable = false;
/** @psalm-suppress PossiblyUndefinedVariable */
$deferred->fail($exception);
while (!$writes->isEmpty()) {
list(, , $deferred) = $writes->shift();
$deferred->fail($exception);
}
Loop::cancel($watcher);
} finally {
if ($writes->isEmpty()) {
Loop::disable($watcher);
}
}
});
Loop::disable($this->watcher);
}
/**
* Writes data to the stream.
*
* @param string $data Bytes to write.
*
* @return Promise Succeeds once the data has been successfully written to the stream.
*
* @throws ClosedException If the stream has already been closed.
*/
public function write(string $data): Promise
{
return $this->send($data, false);
}
/**
* Closes the stream after all pending writes have been completed. Optionally writes a final data chunk before.
*
* @param string $finalData Bytes to write.
*
* @return Promise Succeeds once the data has been successfully written to the stream.
*
* @throws ClosedException If the stream has already been closed.
*/
public function end(string $finalData = ""): Promise
{
return $this->send($finalData, true);
}
private function send(string $data, bool $end = false): Promise
{
if (!$this->writable) {
return new Failure(new ClosedException("The stream is not writable"));
}
$length = \strlen($data);
$written = 0;
if ($end) {
$this->writable = false;
}
if ($this->writes->isEmpty()) {
if ($length === 0) {
if ($end) {
$this->close();
}
return new Success(0);
}
if (!\is_resource($this->resource) || (($metaData = @\stream_get_meta_data($this->resource)) && $metaData['eof'])) {
return new Failure(new ClosedException("The stream was closed by the peer"));
}
// Error reporting suppressed since fwrite() emits E_WARNING if the pipe is broken or the buffer is full.
// Use conditional, because PHP doesn't like getting null passed.
if ($this->chunkSize) {
$written = @\fwrite($this->resource, $data, $this->chunkSize);
} else {
$written = @\fwrite($this->resource, $data);
}
\assert(
$written !== false || \PHP_VERSION_ID >= 70400, // PHP 7.4+ returns false on EPIPE.
"Trying to write on a previously fclose()'d resource. Do NOT manually fclose() resources the still referenced in the loop."
);
// PHP 7.4.0 and 7.4.1 may return false on EAGAIN.
if ($written === false && \PHP_VERSION_ID >= 70402) {
$message = "Failed to write to stream";
if ($error = \error_get_last()) {
$message .= \sprintf("; %s", $error["message"]);
}
return new Failure(new StreamException($message));
}
$written = (int) $written; // Cast potential false to 0.
if ($length === $written) {
if ($end) {
$this->close();
}
return new Success($written);
}
$data = \substr($data, $written);
}
$deferred = new Deferred;
if ($length - $written > self::LARGE_CHUNK_SIZE) {
$chunks = \str_split($data, self::LARGE_CHUNK_SIZE);
$data = \array_pop($chunks);
foreach ($chunks as $chunk) {
$this->writes->push([$chunk, $written, new Deferred]);
$written += self::LARGE_CHUNK_SIZE;
}
}
$this->writes->push([$data, $written, $deferred]);
Loop::enable($this->watcher);
$promise = $deferred->promise();
if ($end) {
$promise->onResolve([$this, "close"]);
}
return $promise;
}
/**
* Closes the stream forcefully. Multiple `close()` calls are ignored.
*
* @return void
*/
public function close()
{
if ($this->resource) {
// Error suppression, as resource might already be closed
$meta = @\stream_get_meta_data($this->resource);
if ($meta && \strpos($meta["mode"], "+") !== false) {
@\stream_socket_shutdown($this->resource, \STREAM_SHUT_WR);
} else {
/** @psalm-suppress InvalidPropertyAssignmentValue psalm reports this as closed-resource */
@\fclose($this->resource);
}
}
$this->free();
}
/**
* Nulls reference to resource, marks stream unwritable, and fails any pending write.
*
* @return void
*/
private function free()
{
$this->resource = null;
$this->writable = false;
if (!$this->writes->isEmpty()) {
$exception = new ClosedException("The socket was closed before writing completed");
do {
/** @var Deferred $deferred */
list(, , $deferred) = $this->writes->shift();
$deferred->fail($exception);
} while (!$this->writes->isEmpty());
}
Loop::cancel($this->watcher);
}
/**
* @return resource|null Stream resource or null if end() has been called or the stream closed.
*/
public function getResource()
{
return $this->resource;
}
/**
* @return void
*/
public function setChunkSize(int $chunkSize)
{
$this->chunkSize = $chunkSize;
}
public function __destruct()
{
if ($this->resource !== null) {
$this->free();
}
}
}

View File

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

View File

@ -0,0 +1,112 @@
<?php
namespace Amp\ByteStream;
use Amp\Promise;
use function Amp\call;
/**
* Allows decompression of input streams using Zlib.
*/
final class ZlibInputStream implements InputStream
{
/** @var InputStream|null */
private $source;
/** @var int */
private $encoding;
/** @var array */
private $options;
/** @var resource|null */
private $resource;
/**
* @param InputStream $source Input stream to read compressed data from.
* @param int $encoding Compression algorithm used, see `inflate_init()`.
* @param array $options Algorithm options, see `inflate_init()`.
*
* @throws StreamException
* @throws \Error
*
* @see http://php.net/manual/en/function.inflate-init.php
*/
public function __construct(InputStream $source, int $encoding, array $options = [])
{
$this->source = $source;
$this->encoding = $encoding;
$this->options = $options;
$this->resource = @\inflate_init($encoding, $options);
if ($this->resource === false) {
throw new StreamException("Failed initializing deflate context");
}
}
/** @inheritdoc */
public function read(): Promise
{
return call(function () {
if ($this->resource === null) {
return null;
}
\assert($this->source !== null);
$data = yield $this->source->read();
// Needs a double guard, as stream might have been closed while reading
/** @psalm-suppress ParadoxicalCondition */
if ($this->resource === null) {
return null;
}
if ($data === null) {
$decompressed = @\inflate_add($this->resource, "", \ZLIB_FINISH);
if ($decompressed === false) {
throw new StreamException("Failed adding data to deflate context");
}
$this->close();
return $decompressed;
}
$decompressed = @\inflate_add($this->resource, $data, \ZLIB_SYNC_FLUSH);
if ($decompressed === false) {
throw new StreamException("Failed adding data to deflate context");
}
return $decompressed;
});
}
/**
* @internal
* @return void
*/
private function close()
{
$this->resource = null;
$this->source = null;
}
/**
* Gets the used compression encoding.
*
* @return int Encoding specified on construction time.
*/
public function getEncoding(): int
{
return $this->encoding;
}
/**
* Gets the used compression options.
*
* @return array Options array passed on construction time.
*/
public function getOptions(): array
{
return $this->options;
}
}

View File

@ -0,0 +1,119 @@
<?php
namespace Amp\ByteStream;
use Amp\Promise;
/**
* Allows compression of output streams using Zlib.
*/
final class ZlibOutputStream implements OutputStream
{
/** @var OutputStream|null */
private $destination;
/** @var int */
private $encoding;
/** @var array */
private $options;
/** @var resource|null */
private $resource;
/**
* @param OutputStream $destination Output stream to write the compressed data to.
* @param int $encoding Compression encoding to use, see `deflate_init()`.
* @param array $options Compression options to use, see `deflate_init()`.
*
* @throws StreamException If an invalid encoding or invalid options have been passed.
*
* @see http://php.net/manual/en/function.deflate-init.php
*/
public function __construct(OutputStream $destination, int $encoding, array $options = [])
{
$this->destination = $destination;
$this->encoding = $encoding;
$this->options = $options;
$this->resource = @\deflate_init($encoding, $options);
if ($this->resource === false) {
throw new StreamException("Failed initializing deflate context");
}
}
/** @inheritdoc */
public function write(string $data): Promise
{
if ($this->resource === null) {
throw new ClosedException("The stream has already been closed");
}
\assert($this->destination !== null);
$compressed = \deflate_add($this->resource, $data, \ZLIB_SYNC_FLUSH);
if ($compressed === false) {
throw new StreamException("Failed adding data to deflate context");
}
$promise = $this->destination->write($compressed);
$promise->onResolve(function ($error) {
if ($error) {
$this->close();
}
});
return $promise;
}
/** @inheritdoc */
public function end(string $finalData = ""): Promise
{
if ($this->resource === null) {
throw new ClosedException("The stream has already been closed");
}
\assert($this->destination !== null);
$compressed = \deflate_add($this->resource, $finalData, \ZLIB_FINISH);
if ($compressed === false) {
throw new StreamException("Failed adding data to deflate context");
}
$promise = $this->destination->end($compressed);
$promise->onResolve(function () {
$this->close();
});
return $promise;
}
/**
* @internal
* @return void
*/
private function close()
{
$this->resource = null;
$this->destination = null;
}
/**
* Gets the used compression encoding.
*
* @return int Encoding specified on construction time.
*/
public function getEncoding(): int
{
return $this->encoding;
}
/**
* Gets the used compression options.
*
* @return array Options array passed on construction time.
*/
public function getOptions(): array
{
return $this->options;
}
}

View File

@ -0,0 +1,188 @@
<?php
namespace Amp\ByteStream;
use Amp\Iterator;
use Amp\Loop;
use Amp\Producer;
use Amp\Promise;
use function Amp\call;
// @codeCoverageIgnoreStart
if (\strlen('…') !== 3) {
throw new \Error(
'The mbstring.func_overload ini setting is enabled. It must be disabled to use the stream package.'
);
} // @codeCoverageIgnoreEnd
if (!\defined('STDOUT')) {
\define('STDOUT', \fopen('php://stdout', 'w'));
}
if (!\defined('STDERR')) {
\define('STDERR', \fopen('php://stderr', 'w'));
}
/**
* @param \Amp\ByteStream\InputStream $source
* @param \Amp\ByteStream\OutputStream $destination
*
* @return \Amp\Promise
*/
function pipe(InputStream $source, OutputStream $destination): Promise
{
return call(function () use ($source, $destination): \Generator {
$written = 0;
while (($chunk = yield $source->read()) !== null) {
$written += \strlen($chunk);
$writePromise = $destination->write($chunk);
$chunk = null; // free memory
yield $writePromise;
}
return $written;
});
}
/**
* @param \Amp\ByteStream\InputStream $source
*
* @return \Amp\Promise
*/
function buffer(InputStream $source): Promise
{
return call(function () use ($source): \Generator {
$buffer = "";
while (($chunk = yield $source->read()) !== null) {
$buffer .= $chunk;
$chunk = null; // free memory
}
return $buffer;
});
}
/**
* The php://input input buffer stream for the process associated with the currently active event loop.
*
* @return ResourceInputStream
*/
function getInputBufferStream(): ResourceInputStream
{
static $key = InputStream::class . '\\input';
$stream = Loop::getState($key);
if (!$stream) {
$stream = new ResourceInputStream(\fopen('php://input', 'rb'));
Loop::setState($key, $stream);
}
return $stream;
}
/**
* The php://output output buffer stream for the process associated with the currently active event loop.
*
* @return ResourceOutputStream
*/
function getOutputBufferStream(): ResourceOutputStream
{
static $key = OutputStream::class . '\\output';
$stream = Loop::getState($key);
if (!$stream) {
$stream = new ResourceOutputStream(\fopen('php://output', 'wb'));
Loop::setState($key, $stream);
}
return $stream;
}
/**
* The STDIN stream for the process associated with the currently active event loop.
*
* @return ResourceInputStream
*/
function getStdin(): ResourceInputStream
{
static $key = InputStream::class . '\\stdin';
$stream = Loop::getState($key);
if (!$stream) {
$stream = new ResourceInputStream(\STDIN);
Loop::setState($key, $stream);
}
return $stream;
}
/**
* The STDOUT stream for the process associated with the currently active event loop.
*
* @return ResourceOutputStream
*/
function getStdout(): ResourceOutputStream
{
static $key = OutputStream::class . '\\stdout';
$stream = Loop::getState($key);
if (!$stream) {
$stream = new ResourceOutputStream(\STDOUT);
Loop::setState($key, $stream);
}
return $stream;
}
/**
* The STDERR stream for the process associated with the currently active event loop.
*
* @return ResourceOutputStream
*/
function getStderr(): ResourceOutputStream
{
static $key = OutputStream::class . '\\stderr';
$stream = Loop::getState($key);
if (!$stream) {
$stream = new ResourceOutputStream(\STDERR);
Loop::setState($key, $stream);
}
return $stream;
}
function parseLineDelimitedJson(InputStream $stream, bool $assoc = false, int $depth = 512, int $options = 0): Iterator
{
return new Producer(static function (callable $emit) use ($stream, $assoc, $depth, $options) {
$reader = new LineReader($stream);
while (null !== $line = yield $reader->readLine()) {
$line = \trim($line);
if ($line === '') {
continue;
}
/** @noinspection PhpComposerExtensionStubsInspection */
$data = \json_decode($line, $assoc, $depth, $options);
/** @noinspection PhpComposerExtensionStubsInspection */
$error = \json_last_error();
/** @noinspection PhpComposerExtensionStubsInspection */
if ($error !== \JSON_ERROR_NONE) {
/** @noinspection PhpComposerExtensionStubsInspection */
throw new StreamException('Failed to parse JSON: ' . \json_last_error_msg(), $error);
}
yield $emit($data);
}
});
}

View File

@ -0,0 +1,53 @@
<?xml version="1.0"?>
<psalm
totallyTyped="false"
errorLevel="2"
phpVersion="7.0"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="examples"/>
<directory name="lib"/>
<ignoreFiles>
<directory name="vendor"/>
</ignoreFiles>
</projectFiles>
<issueHandlers>
<StringIncrement>
<errorLevel type="suppress">
<directory name="examples"/>
<directory name="lib"/>
</errorLevel>
</StringIncrement>
<RedundantConditionGivenDocblockType>
<errorLevel type="suppress">
<directory name="lib"/>
</errorLevel>
</RedundantConditionGivenDocblockType>
<DocblockTypeContradiction>
<errorLevel type="suppress">
<directory name="lib"/>
</errorLevel>
</DocblockTypeContradiction>
<MissingClosureParamType>
<errorLevel type="suppress">
<directory name="examples"/>
<directory name="lib"/>
</errorLevel>
</MissingClosureParamType>
<MissingClosureReturnType>
<errorLevel type="suppress">
<directory name="examples"/>
<directory name="lib"/>
</errorLevel>
</MissingClosureReturnType>
</issueHandlers>
</psalm>

View File

@ -0,0 +1 @@
../friendsofphp/php-cs-fixer/php-cs-fixer

1
lib/composer/bin/php-parse Symbolic link
View File

@ -0,0 +1 @@
../nikic/php-parser/bin/php-parse

1
lib/composer/bin/psalm Symbolic link
View File

@ -0,0 +1 @@
../vimeo/psalm/psalm

View File

@ -0,0 +1 @@
../vimeo/psalm/psalm-language-server

View File

@ -0,0 +1 @@
../vimeo/psalm/psalm-plugin

View File

@ -0,0 +1 @@
../vimeo/psalm/psalm-refactor

1
lib/composer/bin/psalter Symbolic link
View File

@ -0,0 +1 @@
../vimeo/psalm/psalter

View File

@ -37,8 +37,8 @@ namespace Composer\Autoload;
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see http://www.php-fig.org/psr/psr-0/
* @see http://www.php-fig.org/psr/psr-4/
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{

View File

@ -0,0 +1,667 @@
<?php
namespace Composer;
use Composer\Semver\VersionParser;
class InstalledVersions
{
private static $installed = array (
'root' =>
array (
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'aliases' =>
array (
),
'reference' => '11fca45e4c9ed5bc53436b6232a656a51f4984fa',
'name' => '__root__',
),
'versions' =>
array (
'__root__' =>
array (
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'aliases' =>
array (
),
'reference' => '11fca45e4c9ed5bc53436b6232a656a51f4984fa',
),
'amphp/amp' =>
array (
'pretty_version' => 'v2.5.0',
'version' => '2.5.0.0',
'aliases' =>
array (
),
'reference' => 'f220a51458bf4dd0dedebb171ac3457813c72bbc',
),
'amphp/byte-stream' =>
array (
'pretty_version' => 'v1.8.0',
'version' => '1.8.0.0',
'aliases' =>
array (
),
'reference' => 'f0c20cf598a958ba2aa8c6e5a71c697d652c7088',
),
'composer/package-versions-deprecated' =>
array (
'pretty_version' => '1.11.99',
'version' => '1.11.99.0',
'aliases' =>
array (
),
'reference' => 'c8c9aa8a14cc3d3bec86d0a8c3fa52ea79936855',
),
'composer/semver' =>
array (
'pretty_version' => '1.7.1',
'version' => '1.7.1.0',
'aliases' =>
array (
),
'reference' => '38276325bd896f90dfcfe30029aa5db40df387a7',
),
'composer/xdebug-handler' =>
array (
'pretty_version' => '1.4.3',
'version' => '1.4.3.0',
'aliases' =>
array (
),
'reference' => 'ebd27a9866ae8254e873866f795491f02418c5a5',
),
'dnoegel/php-xdg-base-dir' =>
array (
'pretty_version' => 'v0.1.1',
'version' => '0.1.1.0',
'aliases' =>
array (
),
'reference' => '8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd',
),
'doctrine/annotations' =>
array (
'pretty_version' => '1.10.3',
'version' => '1.10.3.0',
'aliases' =>
array (
),
'reference' => '5db60a4969eba0e0c197a19c077780aadbc43c5d',
),
'doctrine/lexer' =>
array (
'pretty_version' => '1.2.1',
'version' => '1.2.1.0',
'aliases' =>
array (
),
'reference' => 'e864bbf5904cb8f5bb334f99209b48018522f042',
),
'felixfbecker/advanced-json-rpc' =>
array (
'pretty_version' => 'v3.1.1',
'version' => '3.1.1.0',
'aliases' =>
array (
),
'reference' => '0ed363f8de17d284d479ec813c9ad3f6834b5c40',
),
'felixfbecker/language-server-protocol' =>
array (
'pretty_version' => 'v1.4.0',
'version' => '1.4.0.0',
'aliases' =>
array (
),
'reference' => '378801f6139bb74ac215d81cca1272af61df9a9f',
),
'friendsofphp/php-cs-fixer' =>
array (
'pretty_version' => 'v2.16.3',
'version' => '2.16.3.0',
'aliases' =>
array (
),
'reference' => '83baf823a33a1cbd5416c8626935cf3f843c10b0',
),
'netresearch/jsonmapper' =>
array (
'pretty_version' => 'v2.1.0',
'version' => '2.1.0.0',
'aliases' =>
array (
),
'reference' => 'e0f1e33a71587aca81be5cffbb9746510e1fe04e',
),
'nextcloud/coding-standard' =>
array (
'pretty_version' => 'v0.3.0',
'version' => '0.3.0.0',
'aliases' =>
array (
),
'reference' => '4f5cd012760f8293e19e602651a0ecaa265e4db9',
),
'nikic/php-parser' =>
array (
'pretty_version' => 'v4.10.2',
'version' => '4.10.2.0',
'aliases' =>
array (
),
'reference' => '658f1be311a230e0907f5dfe0213742aff0596de',
),
'ocramius/package-versions' =>
array (
'replaced' =>
array (
0 => '1.11.99',
),
),
'openlss/lib-array2xml' =>
array (
'pretty_version' => '1.0.0',
'version' => '1.0.0.0',
'aliases' =>
array (
),
'reference' => 'a91f18a8dfc69ffabe5f9b068bc39bb202c81d90',
),
'paragonie/random_compat' =>
array (
'pretty_version' => 'v9.99.99',
'version' => '9.99.99.0',
'aliases' =>
array (
),
'reference' => '84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95',
),
'php-cs-fixer/diff' =>
array (
'pretty_version' => 'v1.3.0',
'version' => '1.3.0.0',
'aliases' =>
array (
),
'reference' => '78bb099e9c16361126c86ce82ec4405ebab8e756',
),
'phpdocumentor/reflection-common' =>
array (
'pretty_version' => '2.2.0',
'version' => '2.2.0.0',
'aliases' =>
array (
),
'reference' => '1d01c49d4ed62f25aa84a747ad35d5a16924662b',
),
'phpdocumentor/reflection-docblock' =>
array (
'pretty_version' => '5.2.2',
'version' => '5.2.2.0',
'aliases' =>
array (
),
'reference' => '069a785b2141f5bcf49f3e353548dc1cce6df556',
),
'phpdocumentor/type-resolver' =>
array (
'pretty_version' => '1.4.0',
'version' => '1.4.0.0',
'aliases' =>
array (
),
'reference' => '6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0',
),
'psalm/psalm' =>
array (
'provided' =>
array (
0 => '4.0.1',
),
),
'psr/container' =>
array (
'pretty_version' => '1.0.0',
'version' => '1.0.0.0',
'aliases' =>
array (
),
'reference' => 'b7ce3b176482dbbc1245ebf52b181af44c2cf55f',
),
'psr/event-dispatcher' =>
array (
'pretty_version' => '1.0.0',
'version' => '1.0.0.0',
'aliases' =>
array (
),
'reference' => 'dbefd12671e8a14ec7f180cab83036ed26714bb0',
),
'psr/event-dispatcher-implementation' =>
array (
'provided' =>
array (
0 => '1.0',
),
),
'psr/log' =>
array (
'pretty_version' => '1.1.3',
'version' => '1.1.3.0',
'aliases' =>
array (
),
'reference' => '0f73288fd15629204f9d42b7055f72dacbe811fc',
),
'psr/log-implementation' =>
array (
'provided' =>
array (
0 => '1.0',
),
),
'sebastian/diff' =>
array (
'pretty_version' => '4.0.3',
'version' => '4.0.3.0',
'aliases' =>
array (
),
'reference' => 'ffc949a1a2aae270ea064453d7535b82e4c32092',
),
'symfony/console' =>
array (
'pretty_version' => 'v5.1.7',
'version' => '5.1.7.0',
'aliases' =>
array (
),
'reference' => 'ae789a8a2ad189ce7e8216942cdb9b77319f5eb8',
),
'symfony/deprecation-contracts' =>
array (
'pretty_version' => 'v2.1.2',
'version' => '2.1.2.0',
'aliases' =>
array (
),
'reference' => 'dd99cb3a0aff6cadd2a8d7d7ed72c2161e218337',
),
'symfony/event-dispatcher' =>
array (
'pretty_version' => 'v5.1.2',
'version' => '5.1.2.0',
'aliases' =>
array (
),
'reference' => 'cc0d059e2e997e79ca34125a52f3e33de4424ac7',
),
'symfony/event-dispatcher-contracts' =>
array (
'pretty_version' => 'v2.1.2',
'version' => '2.1.2.0',
'aliases' =>
array (
),
'reference' => '405952c4e90941a17e52ef7489a2bd94870bb290',
),
'symfony/event-dispatcher-implementation' =>
array (
'provided' =>
array (
0 => '2.0',
),
),
'symfony/filesystem' =>
array (
'pretty_version' => 'v5.1.2',
'version' => '5.1.2.0',
'aliases' =>
array (
),
'reference' => '6e4320f06d5f2cce0d96530162491f4465179157',
),
'symfony/finder' =>
array (
'pretty_version' => 'v5.1.2',
'version' => '5.1.2.0',
'aliases' =>
array (
),
'reference' => '4298870062bfc667cb78d2b379be4bf5dec5f187',
),
'symfony/options-resolver' =>
array (
'pretty_version' => 'v5.1.2',
'version' => '5.1.2.0',
'aliases' =>
array (
),
'reference' => '663f5dd5e14057d1954fe721f9709d35837f2447',
),
'symfony/polyfill-ctype' =>
array (
'pretty_version' => 'v1.18.1',
'version' => '1.18.1.0',
'aliases' =>
array (
),
'reference' => '1c302646f6efc070cd46856e600e5e0684d6b454',
),
'symfony/polyfill-intl-grapheme' =>
array (
'pretty_version' => 'v1.18.1',
'version' => '1.18.1.0',
'aliases' =>
array (
),
'reference' => 'b740103edbdcc39602239ee8860f0f45a8eb9aa5',
),
'symfony/polyfill-intl-normalizer' =>
array (
'pretty_version' => 'v1.18.1',
'version' => '1.18.1.0',
'aliases' =>
array (
),
'reference' => '37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e',
),
'symfony/polyfill-mbstring' =>
array (
'pretty_version' => 'v1.18.1',
'version' => '1.18.1.0',
'aliases' =>
array (
),
'reference' => 'a6977d63bf9a0ad4c65cd352709e230876f9904a',
),
'symfony/polyfill-php70' =>
array (
'pretty_version' => 'v1.17.1',
'version' => '1.17.1.0',
'aliases' =>
array (
),
'reference' => '471b096aede7025bace8eb356b9ac801aaba7e2d',
),
'symfony/polyfill-php72' =>
array (
'pretty_version' => 'v1.17.0',
'version' => '1.17.0.0',
'aliases' =>
array (
),
'reference' => 'f048e612a3905f34931127360bdd2def19a5e582',
),
'symfony/polyfill-php73' =>
array (
'pretty_version' => 'v1.18.1',
'version' => '1.18.1.0',
'aliases' =>
array (
),
'reference' => 'fffa1a52a023e782cdcc221d781fe1ec8f87fcca',
),
'symfony/polyfill-php80' =>
array (
'pretty_version' => 'v1.18.1',
'version' => '1.18.1.0',
'aliases' =>
array (
),
'reference' => 'd87d5766cbf48d72388a9f6b85f280c8ad51f981',
),
'symfony/process' =>
array (
'pretty_version' => 'v5.1.2',
'version' => '5.1.2.0',
'aliases' =>
array (
),
'reference' => '7f6378c1fa2147eeb1b4c385856ce9de0d46ebd1',
),
'symfony/service-contracts' =>
array (
'pretty_version' => 'v2.2.0',
'version' => '2.2.0.0',
'aliases' =>
array (
),
'reference' => 'd15da7ba4957ffb8f1747218be9e1a121fd298a1',
),
'symfony/stopwatch' =>
array (
'pretty_version' => 'v5.1.2',
'version' => '5.1.2.0',
'aliases' =>
array (
),
'reference' => '0f7c58cf81dbb5dd67d423a89d577524a2ec0323',
),
'symfony/string' =>
array (
'pretty_version' => 'v5.1.7',
'version' => '5.1.7.0',
'aliases' =>
array (
),
'reference' => '4a9afe9d07bac506f75bcee8ed3ce76da5a9343e',
),
'vimeo/psalm' =>
array (
'pretty_version' => '4.0.1',
'version' => '4.0.1.0',
'aliases' =>
array (
),
'reference' => 'b1e2e30026936ef8d5bf6a354d1c3959b6231f44',
),
'webmozart/assert' =>
array (
'pretty_version' => '1.9.1',
'version' => '1.9.1.0',
'aliases' =>
array (
),
'reference' => 'bafc69caeb4d49c39fd0779086c03a3738cbb389',
),
'webmozart/glob' =>
array (
'pretty_version' => '4.1.0',
'version' => '4.1.0.0',
'aliases' =>
array (
),
'reference' => '3cbf63d4973cf9d780b93d2da8eec7e4a9e63bbe',
),
'webmozart/path-util' =>
array (
'pretty_version' => '2.3.0',
'version' => '2.3.0.0',
'aliases' =>
array (
),
'reference' => 'd939f7edc24c9a1bb9c0dee5cb05d8e859490725',
),
),
);
public static function getInstalledPackages()
{
return array_keys(self::$installed['versions']);
}
public static function isInstalled($packageName)
{
return isset(self::$installed['versions'][$packageName]);
}
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints($constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
public static function getVersionRanges($packageName)
{
if (!isset(self::$installed['versions'][$packageName])) {
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
$ranges = array();
if (isset(self::$installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = self::$installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', self::$installed['versions'][$packageName])) {
$ranges = array_merge($ranges, self::$installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', self::$installed['versions'][$packageName])) {
$ranges = array_merge($ranges, self::$installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', self::$installed['versions'][$packageName])) {
$ranges = array_merge($ranges, self::$installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
public static function getVersion($packageName)
{
if (!isset(self::$installed['versions'][$packageName])) {
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
if (!isset(self::$installed['versions'][$packageName]['version'])) {
return null;
}
return self::$installed['versions'][$packageName]['version'];
}
public static function getPrettyVersion($packageName)
{
if (!isset(self::$installed['versions'][$packageName])) {
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
if (!isset(self::$installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return self::$installed['versions'][$packageName]['pretty_version'];
}
public static function getReference($packageName)
{
if (!isset(self::$installed['versions'][$packageName])) {
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
if (!isset(self::$installed['versions'][$packageName]['reference'])) {
return null;
}
return self::$installed['versions'][$packageName]['reference'];
}
public static function getRootPackage()
{
return self::$installed['root'];
}
public static function getRawData()
{
return self::$installed;
}
public static function reload($data)
{
self::$installed = $data;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname(dirname($vendorDir));
return array(
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php',
'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php',
'0d59ee240a4cd96ddbb4ff164fccea4d' => $vendorDir . '/symfony/polyfill-php73/bootstrap.php',
'e8aa6e4b5a1db2f56ae794f1505391a8' => $vendorDir . '/amphp/amp/lib/functions.php',
'76cd0796156622033397994f25b0d8fc' => $vendorDir . '/amphp/amp/lib/Internal/functions.php',
'dc51568953534d6c54b08731e61104e2' => $vendorDir . '/vimeo/psalm/src/functions.php',
'8e4171839e12546525126d38dac3dafa' => $vendorDir . '/vimeo/psalm/src/spl_object_id.php',
'25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php',
'023d27dca8066ef29e6739335ea73bad' => $vendorDir . '/symfony/polyfill-php70/bootstrap.php',
'6cd5651c4fef5ed6b63e8d8b8ffbf3cc' => $vendorDir . '/amphp/byte-stream/lib/functions.php',
);

View File

@ -6,4 +6,6 @@ $vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname(dirname($vendorDir));
return array(
'LSS' => array($vendorDir . '/openlss/lib-array2xml'),
'JsonMapper' => array($vendorDir . '/netresearch/jsonmapper/src'),
);

View File

@ -6,8 +6,47 @@ $vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname(dirname($vendorDir));
return array(
'phpDocumentor\\Reflection\\' => array($vendorDir . '/phpdocumentor/reflection-common/src', $vendorDir . '/phpdocumentor/type-resolver/src', $vendorDir . '/phpdocumentor/reflection-docblock/src'),
'XdgBaseDir\\' => array($vendorDir . '/dnoegel/php-xdg-base-dir/src'),
'Webmozart\\PathUtil\\' => array($vendorDir . '/webmozart/path-util/src'),
'Webmozart\\Glob\\' => array($vendorDir . '/webmozart/glob/src'),
'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'),
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Polyfill\\Php73\\' => array($vendorDir . '/symfony/polyfill-php73'),
'Symfony\\Polyfill\\Php72\\' => array($vendorDir . '/symfony/polyfill-php72'),
'Symfony\\Polyfill\\Php70\\' => array($vendorDir . '/symfony/polyfill-php70'),
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'),
'Symfony\\Polyfill\\Intl\\Grapheme\\' => array($vendorDir . '/symfony/polyfill-intl-grapheme'),
'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
'Symfony\\Contracts\\Service\\' => array($vendorDir . '/symfony/service-contracts'),
'Symfony\\Contracts\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher-contracts'),
'Symfony\\Component\\String\\' => array($vendorDir . '/symfony/string'),
'Symfony\\Component\\Stopwatch\\' => array($vendorDir . '/symfony/stopwatch'),
'Symfony\\Component\\Process\\' => array($vendorDir . '/symfony/process'),
'Symfony\\Component\\OptionsResolver\\' => array($vendorDir . '/symfony/options-resolver'),
'Symfony\\Component\\Finder\\' => array($vendorDir . '/symfony/finder'),
'Symfony\\Component\\Filesystem\\' => array($vendorDir . '/symfony/filesystem'),
'Symfony\\Component\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher'),
'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'),
'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
'Psr\\EventDispatcher\\' => array($vendorDir . '/psr/event-dispatcher/src'),
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
'Psalm\\' => array($vendorDir . '/vimeo/psalm/src/Psalm'),
'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'),
'PhpCsFixer\\' => array($vendorDir . '/friendsofphp/php-cs-fixer/src'),
'PackageVersions\\' => array($vendorDir . '/composer/package-versions-deprecated/src/PackageVersions'),
'OC\\Core\\' => array($baseDir . '/core'),
'OC\\' => array($baseDir . '/lib/private'),
'OCP\\' => array($baseDir . '/lib/public'),
'Nextcloud\\CodingStandard\\' => array($vendorDir . '/nextcloud/coding-standard/src'),
'LanguageServerProtocol\\' => array($vendorDir . '/felixfbecker/language-server-protocol/src'),
'Doctrine\\Common\\Lexer\\' => array($vendorDir . '/doctrine/lexer/lib/Doctrine/Common/Lexer'),
'Doctrine\\Common\\Annotations\\' => array($vendorDir . '/doctrine/annotations/lib/Doctrine/Common/Annotations'),
'Composer\\XdebugHandler\\' => array($vendorDir . '/composer/xdebug-handler/src'),
'Composer\\Semver\\' => array($vendorDir . '/composer/semver/src'),
'Amp\\ByteStream\\' => array($vendorDir . '/amphp/byte-stream/lib'),
'Amp\\' => array($vendorDir . '/amphp/amp/lib'),
'AdvancedJsonRpc\\' => array($vendorDir . '/felixfbecker/advanced-json-rpc/lib'),
'' => array($baseDir . '/lib/private/legacy'),
);

View File

@ -22,13 +22,15 @@ class ComposerAutoloaderInit53792487c5a8370acc0b06b1a864ff4c
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit53792487c5a8370acc0b06b1a864ff4c', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit53792487c5a8370acc0b06b1a864ff4c', 'loadClassLoader'));
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
require_once __DIR__ . '/autoload_static.php';
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c::getInitializer($loader));
} else {
@ -50,6 +52,24 @@ class ComposerAutoloaderInit53792487c5a8370acc0b06b1a864ff4c
$loader->register(true);
if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire53792487c5a8370acc0b06b1a864ff4c($fileIdentifier, $file);
}
return $loader;
}
}
function composerRequire53792487c5a8370acc0b06b1a864ff4c($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
require $file;
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,482 @@
<?php return array (
'root' =>
array (
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'aliases' =>
array (
),
'reference' => '11fca45e4c9ed5bc53436b6232a656a51f4984fa',
'name' => '__root__',
),
'versions' =>
array (
'__root__' =>
array (
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'aliases' =>
array (
),
'reference' => '11fca45e4c9ed5bc53436b6232a656a51f4984fa',
),
'amphp/amp' =>
array (
'pretty_version' => 'v2.5.0',
'version' => '2.5.0.0',
'aliases' =>
array (
),
'reference' => 'f220a51458bf4dd0dedebb171ac3457813c72bbc',
),
'amphp/byte-stream' =>
array (
'pretty_version' => 'v1.8.0',
'version' => '1.8.0.0',
'aliases' =>
array (
),
'reference' => 'f0c20cf598a958ba2aa8c6e5a71c697d652c7088',
),
'composer/package-versions-deprecated' =>
array (
'pretty_version' => '1.11.99',
'version' => '1.11.99.0',
'aliases' =>
array (
),
'reference' => 'c8c9aa8a14cc3d3bec86d0a8c3fa52ea79936855',
),
'composer/semver' =>
array (
'pretty_version' => '1.7.1',
'version' => '1.7.1.0',
'aliases' =>
array (
),
'reference' => '38276325bd896f90dfcfe30029aa5db40df387a7',
),
'composer/xdebug-handler' =>
array (
'pretty_version' => '1.4.3',
'version' => '1.4.3.0',
'aliases' =>
array (
),
'reference' => 'ebd27a9866ae8254e873866f795491f02418c5a5',
),
'dnoegel/php-xdg-base-dir' =>
array (
'pretty_version' => 'v0.1.1',
'version' => '0.1.1.0',
'aliases' =>
array (
),
'reference' => '8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd',
),
'doctrine/annotations' =>
array (
'pretty_version' => '1.10.3',
'version' => '1.10.3.0',
'aliases' =>
array (
),
'reference' => '5db60a4969eba0e0c197a19c077780aadbc43c5d',
),
'doctrine/lexer' =>
array (
'pretty_version' => '1.2.1',
'version' => '1.2.1.0',
'aliases' =>
array (
),
'reference' => 'e864bbf5904cb8f5bb334f99209b48018522f042',
),
'felixfbecker/advanced-json-rpc' =>
array (
'pretty_version' => 'v3.1.1',
'version' => '3.1.1.0',
'aliases' =>
array (
),
'reference' => '0ed363f8de17d284d479ec813c9ad3f6834b5c40',
),
'felixfbecker/language-server-protocol' =>
array (
'pretty_version' => 'v1.4.0',
'version' => '1.4.0.0',
'aliases' =>
array (
),
'reference' => '378801f6139bb74ac215d81cca1272af61df9a9f',
),
'friendsofphp/php-cs-fixer' =>
array (
'pretty_version' => 'v2.16.3',
'version' => '2.16.3.0',
'aliases' =>
array (
),
'reference' => '83baf823a33a1cbd5416c8626935cf3f843c10b0',
),
'netresearch/jsonmapper' =>
array (
'pretty_version' => 'v2.1.0',
'version' => '2.1.0.0',
'aliases' =>
array (
),
'reference' => 'e0f1e33a71587aca81be5cffbb9746510e1fe04e',
),
'nextcloud/coding-standard' =>
array (
'pretty_version' => 'v0.3.0',
'version' => '0.3.0.0',
'aliases' =>
array (
),
'reference' => '4f5cd012760f8293e19e602651a0ecaa265e4db9',
),
'nikic/php-parser' =>
array (
'pretty_version' => 'v4.10.2',
'version' => '4.10.2.0',
'aliases' =>
array (
),
'reference' => '658f1be311a230e0907f5dfe0213742aff0596de',
),
'ocramius/package-versions' =>
array (
'replaced' =>
array (
0 => '1.11.99',
),
),
'openlss/lib-array2xml' =>
array (
'pretty_version' => '1.0.0',
'version' => '1.0.0.0',
'aliases' =>
array (
),
'reference' => 'a91f18a8dfc69ffabe5f9b068bc39bb202c81d90',
),
'paragonie/random_compat' =>
array (
'pretty_version' => 'v9.99.99',
'version' => '9.99.99.0',
'aliases' =>
array (
),
'reference' => '84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95',
),
'php-cs-fixer/diff' =>
array (
'pretty_version' => 'v1.3.0',
'version' => '1.3.0.0',
'aliases' =>
array (
),
'reference' => '78bb099e9c16361126c86ce82ec4405ebab8e756',
),
'phpdocumentor/reflection-common' =>
array (
'pretty_version' => '2.2.0',
'version' => '2.2.0.0',
'aliases' =>
array (
),
'reference' => '1d01c49d4ed62f25aa84a747ad35d5a16924662b',
),
'phpdocumentor/reflection-docblock' =>
array (
'pretty_version' => '5.2.2',
'version' => '5.2.2.0',
'aliases' =>
array (
),
'reference' => '069a785b2141f5bcf49f3e353548dc1cce6df556',
),
'phpdocumentor/type-resolver' =>
array (
'pretty_version' => '1.4.0',
'version' => '1.4.0.0',
'aliases' =>
array (
),
'reference' => '6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0',
),
'psalm/psalm' =>
array (
'provided' =>
array (
0 => '4.0.1',
),
),
'psr/container' =>
array (
'pretty_version' => '1.0.0',
'version' => '1.0.0.0',
'aliases' =>
array (
),
'reference' => 'b7ce3b176482dbbc1245ebf52b181af44c2cf55f',
),
'psr/event-dispatcher' =>
array (
'pretty_version' => '1.0.0',
'version' => '1.0.0.0',
'aliases' =>
array (
),
'reference' => 'dbefd12671e8a14ec7f180cab83036ed26714bb0',
),
'psr/event-dispatcher-implementation' =>
array (
'provided' =>
array (
0 => '1.0',
),
),
'psr/log' =>
array (
'pretty_version' => '1.1.3',
'version' => '1.1.3.0',
'aliases' =>
array (
),
'reference' => '0f73288fd15629204f9d42b7055f72dacbe811fc',
),
'psr/log-implementation' =>
array (
'provided' =>
array (
0 => '1.0',
),
),
'sebastian/diff' =>
array (
'pretty_version' => '4.0.3',
'version' => '4.0.3.0',
'aliases' =>
array (
),
'reference' => 'ffc949a1a2aae270ea064453d7535b82e4c32092',
),
'symfony/console' =>
array (
'pretty_version' => 'v5.1.7',
'version' => '5.1.7.0',
'aliases' =>
array (
),
'reference' => 'ae789a8a2ad189ce7e8216942cdb9b77319f5eb8',
),
'symfony/deprecation-contracts' =>
array (
'pretty_version' => 'v2.1.2',
'version' => '2.1.2.0',
'aliases' =>
array (
),
'reference' => 'dd99cb3a0aff6cadd2a8d7d7ed72c2161e218337',
),
'symfony/event-dispatcher' =>
array (
'pretty_version' => 'v5.1.2',
'version' => '5.1.2.0',
'aliases' =>
array (
),
'reference' => 'cc0d059e2e997e79ca34125a52f3e33de4424ac7',
),
'symfony/event-dispatcher-contracts' =>
array (
'pretty_version' => 'v2.1.2',
'version' => '2.1.2.0',
'aliases' =>
array (
),
'reference' => '405952c4e90941a17e52ef7489a2bd94870bb290',
),
'symfony/event-dispatcher-implementation' =>
array (
'provided' =>
array (
0 => '2.0',
),
),
'symfony/filesystem' =>
array (
'pretty_version' => 'v5.1.2',
'version' => '5.1.2.0',
'aliases' =>
array (
),
'reference' => '6e4320f06d5f2cce0d96530162491f4465179157',
),
'symfony/finder' =>
array (
'pretty_version' => 'v5.1.2',
'version' => '5.1.2.0',
'aliases' =>
array (
),
'reference' => '4298870062bfc667cb78d2b379be4bf5dec5f187',
),
'symfony/options-resolver' =>
array (
'pretty_version' => 'v5.1.2',
'version' => '5.1.2.0',
'aliases' =>
array (
),
'reference' => '663f5dd5e14057d1954fe721f9709d35837f2447',
),
'symfony/polyfill-ctype' =>
array (
'pretty_version' => 'v1.18.1',
'version' => '1.18.1.0',
'aliases' =>
array (
),
'reference' => '1c302646f6efc070cd46856e600e5e0684d6b454',
),
'symfony/polyfill-intl-grapheme' =>
array (
'pretty_version' => 'v1.18.1',
'version' => '1.18.1.0',
'aliases' =>
array (
),
'reference' => 'b740103edbdcc39602239ee8860f0f45a8eb9aa5',
),
'symfony/polyfill-intl-normalizer' =>
array (
'pretty_version' => 'v1.18.1',
'version' => '1.18.1.0',
'aliases' =>
array (
),
'reference' => '37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e',
),
'symfony/polyfill-mbstring' =>
array (
'pretty_version' => 'v1.18.1',
'version' => '1.18.1.0',
'aliases' =>
array (
),
'reference' => 'a6977d63bf9a0ad4c65cd352709e230876f9904a',
),
'symfony/polyfill-php70' =>
array (
'pretty_version' => 'v1.17.1',
'version' => '1.17.1.0',
'aliases' =>
array (
),
'reference' => '471b096aede7025bace8eb356b9ac801aaba7e2d',
),
'symfony/polyfill-php72' =>
array (
'pretty_version' => 'v1.17.0',
'version' => '1.17.0.0',
'aliases' =>
array (
),
'reference' => 'f048e612a3905f34931127360bdd2def19a5e582',
),
'symfony/polyfill-php73' =>
array (
'pretty_version' => 'v1.18.1',
'version' => '1.18.1.0',
'aliases' =>
array (
),
'reference' => 'fffa1a52a023e782cdcc221d781fe1ec8f87fcca',
),
'symfony/polyfill-php80' =>
array (
'pretty_version' => 'v1.18.1',
'version' => '1.18.1.0',
'aliases' =>
array (
),
'reference' => 'd87d5766cbf48d72388a9f6b85f280c8ad51f981',
),
'symfony/process' =>
array (
'pretty_version' => 'v5.1.2',
'version' => '5.1.2.0',
'aliases' =>
array (
),
'reference' => '7f6378c1fa2147eeb1b4c385856ce9de0d46ebd1',
),
'symfony/service-contracts' =>
array (
'pretty_version' => 'v2.2.0',
'version' => '2.2.0.0',
'aliases' =>
array (
),
'reference' => 'd15da7ba4957ffb8f1747218be9e1a121fd298a1',
),
'symfony/stopwatch' =>
array (
'pretty_version' => 'v5.1.2',
'version' => '5.1.2.0',
'aliases' =>
array (
),
'reference' => '0f7c58cf81dbb5dd67d423a89d577524a2ec0323',
),
'symfony/string' =>
array (
'pretty_version' => 'v5.1.7',
'version' => '5.1.7.0',
'aliases' =>
array (
),
'reference' => '4a9afe9d07bac506f75bcee8ed3ce76da5a9343e',
),
'vimeo/psalm' =>
array (
'pretty_version' => '4.0.1',
'version' => '4.0.1.0',
'aliases' =>
array (
),
'reference' => 'b1e2e30026936ef8d5bf6a354d1c3959b6231f44',
),
'webmozart/assert' =>
array (
'pretty_version' => '1.9.1',
'version' => '1.9.1.0',
'aliases' =>
array (
),
'reference' => 'bafc69caeb4d49c39fd0779086c03a3738cbb389',
),
'webmozart/glob' =>
array (
'pretty_version' => '4.1.0',
'version' => '4.1.0.0',
'aliases' =>
array (
),
'reference' => '3cbf63d4973cf9d780b93d2da8eec7e4a9e63bbe',
),
'webmozart/path-util' =>
array (
'pretty_version' => '2.3.0',
'version' => '2.3.0.0',
'aliases' =>
array (
),
'reference' => 'd939f7edc24c9a1bb9c0dee5cb05d8e859490725',
),
),
);

View File

@ -0,0 +1,120 @@
# CHANGELOG
## 1.1.3 - 2017-09-06
This release fixes a bug that caused PackageVersions to prevent
the `composer remove` and `composer update` commands to fail when
this package is removed.
In addition to that, mutation testing has been added to the suite,
ensuring that the package is accurately and extensively tested.
Total issues resolved: **3**
- [40: Mutation testing, PHP 7.1 testing](https://github.com/Ocramius/PackageVersions/pull/40) thanks to @Ocramius
- [41: Removing this package on install results in file access error](https://github.com/Ocramius/PackageVersions/issues/41) thanks to @Xerkus
- [46: #41 Avoid issues when the package is scheduled for removal](https://github.com/Ocramius/PackageVersions/pull/46) thanks to @Jean85
## 1.1.2 - 2016-12-30
This release fixes a bug that caused PackageVersions to be enabled
even when it was part of a globally installed package.
Total issues resolved: **3**
- [35: remove all temp directories](https://github.com/Ocramius/PackageVersions/pull/35)
- [38: Interferes with other projects when installed globally](https://github.com/Ocramius/PackageVersions/issues/38)
- [39: Ignore the global plugin when updating local projects](https://github.com/Ocramius/PackageVersions/pull/39)
## 1.1.1 - 2016-07-25
This release removes the [`"files"`](https://getcomposer.org/doc/04-schema.md#files) directive from
[`composer.json`](https://github.com/Ocramius/PackageVersions/commit/86f2636f7c5e7b56fa035fa3826d5fcf80b6dc72),
as it is no longer needed for `composer install --classmap-authoritative`.
Also, that directive was causing issues with HHVM installations, since
PackageVersions is not compatible with it.
Total issues resolved: **1**
- [34: Fatal error during travis build after update to 1.1.0](https://github.com/Ocramius/PackageVersions/issues/34)
## 1.1.0 - 2016-07-22
This release introduces support for running `composer install --classmap-authoritative`
and `composer install --no-scripts`. Please note that performance
while using these modes may be degraded, but the package will
still work.
Additionally, the package was tuned to prevent the plugin from
running twice at installation.
Total issues resolved: **10**
- [18: Fails when using composer install --no-scripts](https://github.com/Ocramius/PackageVersions/issues/18)
- [20: CS (spacing)](https://github.com/Ocramius/PackageVersions/pull/20)
- [22: Document the way the require-dev section is treated](https://github.com/Ocramius/PackageVersions/issues/22)
- [23: Underline that composer.lock is used as source of information](https://github.com/Ocramius/PackageVersions/pull/23)
- [27: Fix incompatibility with --classmap-authoritative](https://github.com/Ocramius/PackageVersions/pull/27)
- [29: mention optimize-autoloader composer.json config option in README](https://github.com/Ocramius/PackageVersions/pull/29)
- [30: The version class is generated twice during composer update](https://github.com/Ocramius/PackageVersions/issues/30)
- [31: Remove double registration of the event listeners](https://github.com/Ocramius/PackageVersions/pull/31)
- [32: Update the usage of mock APIs to use the new API](https://github.com/Ocramius/PackageVersions/pull/32)
- [33: Fix for #18 - support running with --no-scripts flag](https://github.com/Ocramius/PackageVersions/pull/33)
## 1.0.4 - 2016-04-23
This release includes a fix/workaround for composer/composer#5237,
which causes `ocramius/package-versions` to sometimes generate a
`Versions` class with malformed name (something like
`Versions_composer_tmp0`) when running `composer require <package-name>`.
Total issues resolved: **2**
- [16: Workaround for composer/composer#5237 - class parsing](https://github.com/Ocramius/PackageVersions/pull/16)
- [17: Weird Class name being generated](https://github.com/Ocramius/PackageVersions/issues/17)
## 1.0.3 - 2016-02-26
This release fixes an issue related to concurrent autoloader
re-generation caused by multiple composer plugins being installed.
The issue was solved by removing autoloader re-generation from this
package, but it may still affect other packages.
It is now recommended that you run `composer dump-autoload --optimize`
after installation when using this particular package.
Please note that `composer (install|update) -o` is not sufficient
to avoid autoload overhead when using this particular package.
Total issues resolved: **1**
- [15: Remove autoload re-dump optimization](https://github.com/Ocramius/PackageVersions/pull/15)
## 1.0.2 - 2016-02-24
This release fixes issues related to installing the component without
any dev dependencies or with packages that don't have a source or dist
reference, which is usual with packages defined directly in the
`composer.json`.
Total issues resolved: **3**
- [11: fix composer install --no-dev PHP7](https://github.com/Ocramius/PackageVersions/pull/11)
- [12: Packages don't always have a source/reference](https://github.com/Ocramius/PackageVersions/issues/12)
- [13: Fix #12 - support dist and missing package version references](https://github.com/Ocramius/PackageVersions/pull/13)
## 1.0.1 - 2016-02-01
This release fixes an issue related with composer updates to
already installed versions.
Using `composer require` within a package that already used
`ocramius/package-versions` caused the installation to be unable
to write the `PackageVersions\Versions` class to a file.
Total issues resolved: **6**
- [2: remove unused use statement](https://github.com/Ocramius/PackageVersions/pull/2)
- [3: Remove useless files from dist package](https://github.com/Ocramius/PackageVersions/pull/3)
- [5: failed to open stream: phar error: write operations disabled by the php.ini setting phar.readonly](https://github.com/Ocramius/PackageVersions/issues/5)
- [6: Fix/#5 use composer vendor dir](https://github.com/Ocramius/PackageVersions/pull/6)
- [7: Hotfix - #5 generate package versions also when in phar context](https://github.com/Ocramius/PackageVersions/pull/7)
- [8: Versions class should be ignored by VCS, as it is an install-time artifact](https://github.com/Ocramius/PackageVersions/pull/8)

View File

@ -0,0 +1,39 @@
---
title: Contributing
---
# Contributing
* Coding standard for the project is [PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)
* The project will follow strict [object calisthenics](http://www.slideshare.net/guilhermeblanco/object-calisthenics-applied-to-php)
* Any contribution must provide tests for additional introduced conditions
* Any un-confirmed issue needs a failing test case before being accepted
* Pull requests must be sent from a new hotfix/feature branch, not from `master`.
## Installation
To install the project and run the tests, you need to clone it first:
```sh
$ git clone git://github.com/Ocramius/PackageVersions.git
```
You will then need to run a composer installation:
```sh
$ cd PackageVersions
$ curl -s https://getcomposer.org/installer | php
$ php composer.phar update
```
## Testing
The PHPUnit version to be used is the one installed as a dev- dependency via composer:
```sh
$ ./vendor/bin/phpunit
```
Accepted coverage for new contributions is 80%. Any contribution not satisfying this requirement
won't be merged.

View File

@ -0,0 +1,19 @@
Copyright (c) 2016 Marco Pivetta
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

@ -0,0 +1,5 @@
# Package Versions
**`composer/package-versions-deprecated` is a fully-compatible fork of [`ocramius/package-versions`](https://github.com/Ocramius/PackageVersions)** which provides compatibility with Composer 1 and 2 on PHP 7+. It replaces ocramius/package-versions so if you have a dependency requiring it and you want to use Composer v2 but can not upgrade to PHP 7.4 just yet, you can require this package instead.
If you have a direct dependency on ocramius/package-versions, we recommend instead that once you migrated to Composer 2 you also migrate to use the `Composer\Versions` class which offers the functionality present here out of the box.

View File

@ -0,0 +1,5 @@
## Security contact information
To report a security vulnerability, please use the
[Tidelift security contact](https://tidelift.com/security).
Tidelift will coordinate the fix and disclosure.

View File

@ -0,0 +1,48 @@
{
"name": "composer/package-versions-deprecated",
"description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)",
"type": "composer-plugin",
"license": "MIT",
"authors": [
{
"name": "Marco Pivetta",
"email": "ocramius@gmail.com"
},
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be"
}
],
"require": {
"php": "^7 || ^8",
"composer-plugin-api": "^1.1.0 || ^2.0"
},
"replace": {
"ocramius/package-versions": "1.11.99"
},
"require-dev": {
"phpunit/phpunit": "^6.5 || ^7",
"composer/composer": "^1.9.3 || ^2.0@dev",
"ext-zip": "^1.13"
},
"autoload": {
"psr-4": {
"PackageVersions\\": "src/PackageVersions"
}
},
"autoload-dev": {
"psr-4": {
"PackageVersionsTest\\": "test/PackageVersionsTest"
}
},
"extra": {
"class": "PackageVersions\\Installer",
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"scripts": {
"post-update-cmd": "PackageVersions\\Installer::dumpVersionsClass",
"post-install-cmd": "PackageVersions\\Installer::dumpVersionsClass"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
<?xml version="1.0"?>
<ruleset>
<arg name="basepath" value="."/>
<arg name="extensions" value="php"/>
<arg name="parallel" value="80"/>
<arg name="cache" value=".phpcs-cache"/>
<arg name="colors"/>
<arg value="nps"/>
<file>src</file>
<file>test</file>
<rule ref="Doctrine">
<exclude-pattern>src/PackageVersions/Versions.php</exclude-pattern>
</rule>
<rule ref="Generic.Strings.UnnecessaryStringConcat.Found">
<exclude-pattern>src/PackageVersions/Installer.php</exclude-pattern>
</rule>
</ruleset>

View File

@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
namespace PackageVersions;
use Generator;
use OutOfBoundsException;
use UnexpectedValueException;
use function array_key_exists;
use function array_merge;
use function basename;
use function file_exists;
use function file_get_contents;
use function getcwd;
use function iterator_to_array;
use function json_decode;
use function json_encode;
use function sprintf;
/**
* @internal
*
* This is a fallback for {@see \PackageVersions\Versions::getVersion()}
* Do not use this class directly: it is intended to be only used when
* {@see \PackageVersions\Versions} fails to be generated, which typically
* happens when running composer with `--no-scripts` flag)
*/
final class FallbackVersions
{
const ROOT_PACKAGE_NAME = 'unknown/root-package@UNKNOWN';
private function __construct()
{
}
/**
* @throws OutOfBoundsException If a version cannot be located.
* @throws UnexpectedValueException If the composer.lock file could not be located.
*/
public static function getVersion(string $packageName): string
{
$versions = iterator_to_array(self::getVersions(self::getPackageData()));
if (! array_key_exists($packageName, $versions)) {
throw new OutOfBoundsException(
'Required package "' . $packageName . '" is not installed: check your ./vendor/composer/installed.json and/or ./composer.lock files'
);
}
return $versions[$packageName];
}
/**
* @return mixed[]
*
* @throws UnexpectedValueException
*/
private static function getPackageData(): array
{
$checkedPaths = [
// The top-level project's ./vendor/composer/installed.json
getcwd() . '/vendor/composer/installed.json',
__DIR__ . '/../../../../composer/installed.json',
// The top-level project's ./composer.lock
getcwd() . '/composer.lock',
__DIR__ . '/../../../../../composer.lock',
// This package's composer.lock
__DIR__ . '/../../composer.lock',
];
$packageData = [];
foreach ($checkedPaths as $path) {
if (! file_exists($path)) {
continue;
}
$data = json_decode(file_get_contents($path), true);
switch (basename($path)) {
case 'installed.json':
// composer 2.x installed.json format
if (isset($data['packages'])) {
$packageData[] = $data['packages'];
} else {
// composer 1.x installed.json format
$packageData[] = $data;
}
break;
case 'composer.lock':
$packageData[] = $data['packages'] + ($data['packages-dev'] ?? []);
break;
default:
// intentionally left blank
}
}
if ($packageData !== []) {
return array_merge(...$packageData);
}
throw new UnexpectedValueException(sprintf(
'PackageVersions could not locate the `vendor/composer/installed.json` or your `composer.lock` '
. 'location. This is assumed to be in %s. If you customized your composer vendor directory and ran composer '
. 'installation with --no-scripts, or if you deployed without the required composer files, PackageVersions '
. 'can\'t detect installed versions.',
json_encode($checkedPaths)
));
}
/**
* @param mixed[] $packageData
*
* @return Generator&string[]
*
* @psalm-return Generator<string, string>
*/
private static function getVersions(array $packageData): Generator
{
foreach ($packageData as $package) {
yield $package['name'] => $package['version'] . '@' . (
$package['source']['reference'] ?? $package['dist']['reference'] ?? ''
);
}
yield self::ROOT_PACKAGE_NAME => self::ROOT_PACKAGE_NAME;
}
}

View File

@ -0,0 +1,265 @@
<?php
declare(strict_types=1);
namespace PackageVersions;
use Composer\Composer;
use Composer\Config;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\IO\IOInterface;
use Composer\Package\AliasPackage;
use Composer\Package\Locker;
use Composer\Package\PackageInterface;
use Composer\Package\RootPackageInterface;
use Composer\Plugin\PluginInterface;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;
use Generator;
use RuntimeException;
use function array_key_exists;
use function array_merge;
use function chmod;
use function dirname;
use function file_exists;
use function file_put_contents;
use function is_writable;
use function iterator_to_array;
use function rename;
use function sprintf;
use function uniqid;
use function var_export;
final class Installer implements PluginInterface, EventSubscriberInterface
{
private static $generatedClassTemplate = <<<'PHP'
<?php
declare(strict_types=1);
namespace PackageVersions;
use Composer\InstalledVersions;
use OutOfBoundsException;
class_exists(InstalledVersions::class);
/**
* This class is generated by composer/package-versions-deprecated, specifically by
* @see \PackageVersions\Installer
*
* This file is overwritten at every run of `composer install` or `composer update`.
*
* @deprecated in favor of the Composer\InstalledVersions class provided by Composer 2. Require composer-runtime-api:^2 to ensure it is present.
*/
%s
{
/**
* @deprecated please use {@see self::rootPackageName()} instead.
* This constant will be removed in version 2.0.0.
*/
const ROOT_PACKAGE_NAME = '%s';
/**
* Array of all available composer packages.
* Dont read this array from your calling code, but use the \PackageVersions\Versions::getVersion() method instead.
*
* @var array<string, string>
* @internal
*/
const VERSIONS = %s;
private function __construct()
{
}
/**
* @psalm-pure
*
* @psalm-suppress ImpureMethodCall we know that {@see InstalledVersions} interaction does not
* cause any side effects here.
*/
public static function rootPackageName() : string
{
if (!class_exists(InstalledVersions::class, false) || !InstalledVersions::getRawData()) {
return self::ROOT_PACKAGE_NAME;
}
return InstalledVersions::getRootPackage()['name'];
}
/**
* @throws OutOfBoundsException If a version cannot be located.
*
* @psalm-param key-of<self::VERSIONS> $packageName
* @psalm-pure
*
* @psalm-suppress ImpureMethodCall we know that {@see InstalledVersions} interaction does not
* cause any side effects here.
*/
public static function getVersion(string $packageName): string
{
if (class_exists(InstalledVersions::class, false) && InstalledVersions::getRawData()) {
return InstalledVersions::getPrettyVersion($packageName)
. '@' . InstalledVersions::getReference($packageName);
}
if (isset(self::VERSIONS[$packageName])) {
return self::VERSIONS[$packageName];
}
throw new OutOfBoundsException(
'Required package "' . $packageName . '" is not installed: check your ./vendor/composer/installed.json and/or ./composer.lock files'
);
}
}
PHP;
public function activate(Composer $composer, IOInterface $io)
{
// Nothing to do here, as all features are provided through event listeners
}
public function deactivate(Composer $composer, IOInterface $io)
{
// Nothing to do here, as all features are provided through event listeners
}
public function uninstall(Composer $composer, IOInterface $io)
{
// Nothing to do here, as all features are provided through event listeners
}
/**
* {@inheritDoc}
*/
public static function getSubscribedEvents(): array
{
return [ScriptEvents::POST_AUTOLOAD_DUMP => 'dumpVersionsClass'];
}
/**
* @throws RuntimeException
*/
public static function dumpVersionsClass(Event $composerEvent)
{
$composer = $composerEvent->getComposer();
$rootPackage = $composer->getPackage();
$versions = iterator_to_array(self::getVersions($composer->getLocker(), $rootPackage));
if (! array_key_exists('composer/package-versions-deprecated', $versions)) {
//plugin must be globally installed - we only want to generate versions for projects which specifically
//require composer/package-versions-deprecated
return;
}
$versionClass = self::generateVersionsClass($rootPackage->getName(), $versions);
self::writeVersionClassToFile($versionClass, $composer, $composerEvent->getIO());
}
/**
* @param string[] $versions
*/
private static function generateVersionsClass(string $rootPackageName, array $versions): string
{
return sprintf(
self::$generatedClassTemplate,
'fin' . 'al ' . 'cla' . 'ss ' . 'Versions', // note: workaround for regex-based code parsers :-(
$rootPackageName,
var_export($versions, true)
);
}
/**
* @throws RuntimeException
*/
private static function writeVersionClassToFile(string $versionClassSource, Composer $composer, IOInterface $io)
{
$installPath = self::locateRootPackageInstallPath($composer->getConfig(), $composer->getPackage())
. '/src/PackageVersions/Versions.php';
$installDir = dirname($installPath);
if (! file_exists($installDir)) {
$io->write('<info>composer/package-versions-deprecated:</info> Package not found (probably scheduled for removal); generation of version class skipped.');
return;
}
if (! is_writable($installDir)) {
$io->write(
sprintf(
'<info>composer/package-versions-deprecated:</info> %s is not writable; generation of version class skipped.',
$installDir
)
);
return;
}
$io->write('<info>composer/package-versions-deprecated:</info> Generating version class...');
$installPathTmp = $installPath . '_' . uniqid('tmp', true);
file_put_contents($installPathTmp, $versionClassSource);
chmod($installPathTmp, 0664);
rename($installPathTmp, $installPath);
$io->write('<info>composer/package-versions-deprecated:</info> ...done generating version class');
}
/**
* @throws RuntimeException
*/
private static function locateRootPackageInstallPath(
Config $composerConfig,
RootPackageInterface $rootPackage
): string {
if (self::getRootPackageAlias($rootPackage)->getName() === 'composer/package-versions-deprecated') {
return dirname($composerConfig->get('vendor-dir'));
}
return $composerConfig->get('vendor-dir') . '/composer/package-versions-deprecated';
}
private static function getRootPackageAlias(RootPackageInterface $rootPackage): PackageInterface
{
$package = $rootPackage;
while ($package instanceof AliasPackage) {
$package = $package->getAliasOf();
}
return $package;
}
/**
* @return Generator&string[]
*
* @psalm-return Generator<string, string>
*/
private static function getVersions(Locker $locker, RootPackageInterface $rootPackage): Generator
{
$lockData = $locker->getLockData();
$lockData['packages-dev'] = $lockData['packages-dev'] ?? [];
foreach (array_merge($lockData['packages'], $lockData['packages-dev']) as $package) {
yield $package['name'] => $package['version'] . '@' . (
$package['source']['reference'] ?? $package['dist']['reference'] ?? ''
);
}
foreach ($rootPackage->getReplaces() as $replace) {
$version = $replace->getPrettyConstraint();
if ($version === 'self.version') {
$version = $rootPackage->getPrettyVersion();
}
yield $replace->getTarget() => $version . '@' . $rootPackage->getSourceReference();
}
yield $rootPackage->getName() => $rootPackage->getPrettyVersion() . '@' . $rootPackage->getSourceReference();
}
}

View File

@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace PackageVersions;
use Composer\InstalledVersions;
use OutOfBoundsException;
class_exists(InstalledVersions::class);
/**
* This class is generated by composer/package-versions-deprecated, specifically by
* @see \PackageVersions\Installer
*
* This file is overwritten at every run of `composer install` or `composer update`.
*
* @deprecated in favor of the Composer\InstalledVersions class provided by Composer 2. Require composer-runtime-api:^2 to ensure it is present.
*/
final class Versions
{
/**
* @deprecated please use {@see self::rootPackageName()} instead.
* This constant will be removed in version 2.0.0.
*/
const ROOT_PACKAGE_NAME = '__root__';
/**
* Array of all available composer packages.
* Dont read this array from your calling code, but use the \PackageVersions\Versions::getVersion() method instead.
*
* @var array<string, string>
* @internal
*/
const VERSIONS = array (
'amphp/amp' => 'v2.5.0@f220a51458bf4dd0dedebb171ac3457813c72bbc',
'amphp/byte-stream' => 'v1.8.0@f0c20cf598a958ba2aa8c6e5a71c697d652c7088',
'composer/package-versions-deprecated' => '1.11.99@c8c9aa8a14cc3d3bec86d0a8c3fa52ea79936855',
'composer/semver' => '1.7.1@38276325bd896f90dfcfe30029aa5db40df387a7',
'composer/xdebug-handler' => '1.4.3@ebd27a9866ae8254e873866f795491f02418c5a5',
'dnoegel/php-xdg-base-dir' => 'v0.1.1@8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd',
'doctrine/annotations' => '1.10.3@5db60a4969eba0e0c197a19c077780aadbc43c5d',
'doctrine/lexer' => '1.2.1@e864bbf5904cb8f5bb334f99209b48018522f042',
'felixfbecker/advanced-json-rpc' => 'v3.1.1@0ed363f8de17d284d479ec813c9ad3f6834b5c40',
'felixfbecker/language-server-protocol' => 'v1.4.0@378801f6139bb74ac215d81cca1272af61df9a9f',
'friendsofphp/php-cs-fixer' => 'v2.16.3@83baf823a33a1cbd5416c8626935cf3f843c10b0',
'netresearch/jsonmapper' => 'v2.1.0@e0f1e33a71587aca81be5cffbb9746510e1fe04e',
'nextcloud/coding-standard' => 'v0.3.0@4f5cd012760f8293e19e602651a0ecaa265e4db9',
'nikic/php-parser' => 'v4.10.2@658f1be311a230e0907f5dfe0213742aff0596de',
'openlss/lib-array2xml' => '1.0.0@a91f18a8dfc69ffabe5f9b068bc39bb202c81d90',
'paragonie/random_compat' => 'v9.99.99@84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95',
'php-cs-fixer/diff' => 'v1.3.0@78bb099e9c16361126c86ce82ec4405ebab8e756',
'phpdocumentor/reflection-common' => '2.2.0@1d01c49d4ed62f25aa84a747ad35d5a16924662b',
'phpdocumentor/reflection-docblock' => '5.2.2@069a785b2141f5bcf49f3e353548dc1cce6df556',
'phpdocumentor/type-resolver' => '1.4.0@6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0',
'psr/container' => '1.0.0@b7ce3b176482dbbc1245ebf52b181af44c2cf55f',
'psr/event-dispatcher' => '1.0.0@dbefd12671e8a14ec7f180cab83036ed26714bb0',
'psr/log' => '1.1.3@0f73288fd15629204f9d42b7055f72dacbe811fc',
'sebastian/diff' => '4.0.3@ffc949a1a2aae270ea064453d7535b82e4c32092',
'symfony/console' => 'v5.1.7@ae789a8a2ad189ce7e8216942cdb9b77319f5eb8',
'symfony/deprecation-contracts' => 'v2.1.2@dd99cb3a0aff6cadd2a8d7d7ed72c2161e218337',
'symfony/event-dispatcher' => 'v5.1.2@cc0d059e2e997e79ca34125a52f3e33de4424ac7',
'symfony/event-dispatcher-contracts' => 'v2.1.2@405952c4e90941a17e52ef7489a2bd94870bb290',
'symfony/filesystem' => 'v5.1.2@6e4320f06d5f2cce0d96530162491f4465179157',
'symfony/finder' => 'v5.1.2@4298870062bfc667cb78d2b379be4bf5dec5f187',
'symfony/options-resolver' => 'v5.1.2@663f5dd5e14057d1954fe721f9709d35837f2447',
'symfony/polyfill-ctype' => 'v1.18.1@1c302646f6efc070cd46856e600e5e0684d6b454',
'symfony/polyfill-intl-grapheme' => 'v1.18.1@b740103edbdcc39602239ee8860f0f45a8eb9aa5',
'symfony/polyfill-intl-normalizer' => 'v1.18.1@37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e',
'symfony/polyfill-mbstring' => 'v1.18.1@a6977d63bf9a0ad4c65cd352709e230876f9904a',
'symfony/polyfill-php70' => 'v1.17.1@471b096aede7025bace8eb356b9ac801aaba7e2d',
'symfony/polyfill-php72' => 'v1.17.0@f048e612a3905f34931127360bdd2def19a5e582',
'symfony/polyfill-php73' => 'v1.18.1@fffa1a52a023e782cdcc221d781fe1ec8f87fcca',
'symfony/polyfill-php80' => 'v1.18.1@d87d5766cbf48d72388a9f6b85f280c8ad51f981',
'symfony/process' => 'v5.1.2@7f6378c1fa2147eeb1b4c385856ce9de0d46ebd1',
'symfony/service-contracts' => 'v2.2.0@d15da7ba4957ffb8f1747218be9e1a121fd298a1',
'symfony/stopwatch' => 'v5.1.2@0f7c58cf81dbb5dd67d423a89d577524a2ec0323',
'symfony/string' => 'v5.1.7@4a9afe9d07bac506f75bcee8ed3ce76da5a9343e',
'vimeo/psalm' => '4.0.1@b1e2e30026936ef8d5bf6a354d1c3959b6231f44',
'webmozart/assert' => '1.9.1@bafc69caeb4d49c39fd0779086c03a3738cbb389',
'webmozart/glob' => '4.1.0@3cbf63d4973cf9d780b93d2da8eec7e4a9e63bbe',
'webmozart/path-util' => '2.3.0@d939f7edc24c9a1bb9c0dee5cb05d8e859490725',
'__root__' => 'dev-master@11fca45e4c9ed5bc53436b6232a656a51f4984fa',
);
private function __construct()
{
}
/**
* @psalm-pure
*
* @psalm-suppress ImpureMethodCall we know that {@see InstalledVersions} interaction does not
* cause any side effects here.
*/
public static function rootPackageName() : string
{
if (!class_exists(InstalledVersions::class, false) || !InstalledVersions::getRawData()) {
return self::ROOT_PACKAGE_NAME;
}
return InstalledVersions::getRootPackage()['name'];
}
/**
* @throws OutOfBoundsException If a version cannot be located.
*
* @psalm-param key-of<self::VERSIONS> $packageName
* @psalm-pure
*
* @psalm-suppress ImpureMethodCall we know that {@see InstalledVersions} interaction does not
* cause any side effects here.
*/
public static function getVersion(string $packageName): string
{
if (class_exists(InstalledVersions::class, false) && InstalledVersions::getRawData()) {
return InstalledVersions::getPrettyVersion($packageName)
. '@' . InstalledVersions::getReference($packageName);
}
if (isset(self::VERSIONS[$packageName])) {
return self::VERSIONS[$packageName];
}
throw new OutOfBoundsException(
'Required package "' . $packageName . '" is not installed: check your ./vendor/composer/installed.json and/or ./composer.lock files'
);
}
}

View File

@ -0,0 +1,31 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 70300)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.3.0". You are running ' . PHP_VERSION . '.';
}
$missingExtensions = array();
extension_loaded('dom') || $missingExtensions[] = 'dom';
extension_loaded('filter') || $missingExtensions[] = 'filter';
extension_loaded('json') || $missingExtensions[] = 'json';
extension_loaded('libxml') || $missingExtensions[] = 'libxml';
extension_loaded('mbstring') || $missingExtensions[] = 'mbstring';
extension_loaded('pcre') || $missingExtensions[] = 'pcre';
extension_loaded('pdo') || $missingExtensions[] = 'pdo';
extension_loaded('reflection') || $missingExtensions[] = 'reflection';
extension_loaded('simplexml') || $missingExtensions[] = 'simplexml';
extension_loaded('spl') || $missingExtensions[] = 'spl';
extension_loaded('tokenizer') || $missingExtensions[] = 'tokenizer';
if ($missingExtensions) {
$issues[] = 'Your Composer dependencies require the following PHP extensions to be installed: ' . implode(', ', $missingExtensions);
}
if ($issues) {
echo 'Composer detected issues in your platform:' . "\n\n" . implode("\n", $issues);
exit(104);
}

View File

@ -0,0 +1,102 @@
# Change Log
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
### [1.7.1] 2020-09-27
* Fixed: accidental validation of broken constraints combining ^/~ and wildcards, and -dev suffix allowing weird cases
* Fixed: normalization of beta0 and such which was dropping the 0
### [1.7.0] 2020-09-09
* Added: support for `x || @dev`, not very useful but seen in the wild and failed to validate with 1.5.2/1.6.0
* Added: support for `foobar-dev` being equal to `dev-foobar`, dev-foobar is the official way to write it but we need to support the other for BC and convenience
### [1.6.0] 2020-09-08
* Added: support for constraints like `^2.x-dev` and `~2.x-dev`, not very useful but seen in the wild and failed to validate with 1.5.2
* Fixed: invalid aliases will no longer throw, unless explicitly validated by Composer in the root package
### [1.5.2] 2020-09-08
* Fixed: handling of some invalid -dev versions which were seen as valid
* Fixed: some doctypes
### [1.5.1] 2020-01-13
* Fixed: Parsing of aliased version was not validating the alias to be a valid version
### [1.5.0] 2019-03-19
* Added: some support for date versions (e.g. 201903) in `~` operator
* Fixed: support for stabilities in `~` operator was inconsistent
### [1.4.2] 2016-08-30
* Fixed: collapsing of complex constraints lead to buggy constraints
### [1.4.1] 2016-06-02
* Changed: branch-like requirements no longer strip build metadata - [composer/semver#38](https://github.com/composer/semver/pull/38).
### [1.4.0] 2016-03-30
* Added: getters on MultiConstraint - [composer/semver#35](https://github.com/composer/semver/pull/35).
### [1.3.0] 2016-02-25
* Fixed: stability parsing - [composer/composer#1234](https://github.com/composer/composer/issues/4889).
* Changed: collapse contiguous constraints when possible.
### [1.2.0] 2015-11-10
* Changed: allow multiple numerical identifiers in 'pre-release' version part.
* Changed: add more 'v' prefix support.
### [1.1.0] 2015-11-03
* Changed: dropped redundant `test` namespace.
* Changed: minor adjustment in datetime parsing normalization.
* Changed: `ConstraintInterface` relaxed, setPrettyString is not required anymore.
* Changed: `AbstractConstraint` marked deprecated, will be removed in 2.0.
* Changed: `Constraint` is now extensible.
### [1.0.0] 2015-09-21
* Break: `VersionConstraint` renamed to `Constraint`.
* Break: `SpecificConstraint` renamed to `AbstractConstraint`.
* Break: `LinkConstraintInterface` renamed to `ConstraintInterface`.
* Break: `VersionParser::parseNameVersionPairs` was removed.
* Changed: `VersionParser::parseConstraints` allows (but ignores) build metadata now.
* Changed: `VersionParser::parseConstraints` allows (but ignores) prefixing numeric versions with a 'v' now.
* Changed: Fixed namespace(s) of test files.
* Changed: `Comparator::compare` no longer throws `InvalidArgumentException`.
* Changed: `Constraint` now throws `InvalidArgumentException`.
### [0.1.0] 2015-07-23
* Added: `Composer\Semver\Comparator`, various methods to compare versions.
* Added: various documents such as README.md, LICENSE, etc.
* Added: configuration files for Git, Travis, php-cs-fixer, phpunit.
* Break: the following namespaces were renamed:
- Namespace: `Composer\Package\Version` -> `Composer\Semver`
- Namespace: `Composer\Package\LinkConstraint` -> `Composer\Semver\Constraint`
- Namespace: `Composer\Test\Package\Version` -> `Composer\Test\Semver`
- Namespace: `Composer\Test\Package\LinkConstraint` -> `Composer\Test\Semver\Constraint`
* Changed: code style using php-cs-fixer.
[1.7.1]: https://github.com/composer/semver/compare/1.7.0...1.7.1
[1.7.0]: https://github.com/composer/semver/compare/1.6.0...1.7.0
[1.6.0]: https://github.com/composer/semver/compare/1.5.2...1.6.0
[1.5.2]: https://github.com/composer/semver/compare/1.5.1...1.5.2
[1.5.1]: https://github.com/composer/semver/compare/1.5.0...1.5.1
[1.5.0]: https://github.com/composer/semver/compare/1.4.2...1.5.0
[1.4.2]: https://github.com/composer/semver/compare/1.4.1...1.4.2
[1.4.1]: https://github.com/composer/semver/compare/1.4.0...1.4.1
[1.4.0]: https://github.com/composer/semver/compare/1.3.0...1.4.0
[1.3.0]: https://github.com/composer/semver/compare/1.2.0...1.3.0
[1.2.0]: https://github.com/composer/semver/compare/1.1.0...1.2.0
[1.1.0]: https://github.com/composer/semver/compare/1.0.0...1.1.0
[1.0.0]: https://github.com/composer/semver/compare/0.1.0...1.0.0
[0.1.0]: https://github.com/composer/semver/compare/5e0b9a4da...0.1.0

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