This commit is contained in:
cutemeli
2025-12-22 10:35:30 +00:00
parent 0bfc6c8425
commit 5ce7ca2c5d
38927 changed files with 0 additions and 4594700 deletions

View File

@@ -1 +0,0 @@
Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. (https://getlaminas.org/)

View File

@@ -1,26 +0,0 @@
Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
- Neither the name of Laminas Foundation nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,48 +0,0 @@
# laminas-diactoros
[![Build Status](https://github.com/laminas/laminas-diactoros/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/laminas/laminas-diactoros/actions/workflows/continuous-integration.yml)
[![type-coverage](https://shepherd.dev/github/laminas/laminas-diactoros/coverage.svg)](https://shepherd.dev/github/laminas/laminas-diactoros)
[![Psalm level](https://shepherd.dev/github/laminas/laminas-diactoros/level.svg)](https://shepherd.dev/github/laminas/laminas-diactoros)
> ## 🇷🇺 Русским гражданам
>
> Мы, участники Laminas, родились и живем в разных странах. У многих из нас есть друзья, родственники и коллеги как в России, так и в Украине. Некоторые из нас родились в России. Некоторые из нас живут в России. У некоторых бабушки и дедушки сражались с фашистами во Второй мировой войне. Здесь никто не поддерживает фашизм.
>
> У одного из нас есть украинская родственница, которая спаслась из дома вместе с сыном. Поезд задержался из-за бомбежки на дороге впереди. У нас есть друзья, которые прячутся в бомбоубежищах. Мы с тревогой ждем весточки от них после воздушных налетов, которые беспорядочно наносят удары и попадают по больницам, школам, детским садам и домам. Мы не берем это из каких-либо СМИ. Мы наблюдаем это напрямую.
>
> Вы доверяете нам достаточно, чтоб использовать наши программы, и мы просим вас довериться нам вновь. Мы нуждаемся в помощи. Выходите и протестуйте против этой бесполезной войны. Остановите кровопролитие. Скажите "Нет войне!"
>
> ## 🇺🇸 To Citizens of Russia
>
> We at Laminas come from all over the world. Many of us have friends, family and colleagues in both Russia and Ukraine. Some of us were born in Russia. Some of us currently live in Russia. Some have grandparents who fought Nazis in World War II. Nobody here supports fascism.
>
> One team member has a Ukrainian relative who fled her home with her son. The train was delayed due to bombing on the road ahead. We have friends who are hiding in bomb shelters. We anxiously follow up on them after the air raids, which indiscriminately fire at hospitals, schools, kindergartens and houses. We're not taking this from any media. These are our actual experiences.
>
> You trust us enough to use our software. We ask that you trust us to say the truth on this. We need your help. Go out and protest this unnecessary war. Stop the bloodshed. Say "stop the war!"
> Diactoros (pronunciation: `/dɪʌktɒrɒs/`): an epithet for Hermes, meaning literally, "the messenger."
This package supercedes and replaces [phly/http](https://github.com/phly/http).
`laminas-diactoros` is a PHP package containing implementations of the
[PSR-7 HTTP message interfaces](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md)
and [PSR-17 HTTP message factory interfaces](https://www.php-fig.org/psr/psr-17).
- File issues at https://github.com/laminas/laminas-diactoros/issues
- Issue patches to https://github.com/laminas/laminas-diactoros/pulls
## Documentation
Documentation is available at:
- https://docs.laminas.dev/laminas-diactoros/
Source files for documentation are [in the docs/ tree](docs/).
-----
## Contributing and Support
- If you need support with the project, read [the support documentation](https://github.com/laminas/.github/blob/main/SUPPORT.md).
- If you wish to contribute to the project, read the [contributing guidelines](https://github.com/laminas/.github/blob/main/CONTRIBUTING.md) as well as the [Code of Conduct](https://github.com/laminas/.github/blob/main/CODE_OF_CONDUCT.md).
- For reporting security issues, please review our [security policy](https://github.com/laminas/.github/blob/main/SECURITY.md).

View File

@@ -1,90 +0,0 @@
{
"name": "laminas/laminas-diactoros",
"description": "PSR HTTP Message implementations",
"license": "BSD-3-Clause",
"keywords": [
"laminas",
"http",
"psr",
"psr-7",
"psr-17"
],
"homepage": "https://laminas.dev",
"support": {
"docs": "https://docs.laminas.dev/laminas-diactoros/",
"issues": "https://github.com/laminas/laminas-diactoros/issues",
"source": "https://github.com/laminas/laminas-diactoros",
"rss": "https://github.com/laminas/laminas-diactoros/releases.atom",
"chat": "https://laminas.dev/chat",
"forum": "https://discourse.laminas.dev"
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
},
"sort-packages": true,
"platform": {
"php": "8.2.99"
}
},
"extra": {
"laminas": {
"config-provider": "Laminas\\Diactoros\\ConfigProvider",
"module": "Laminas\\Diactoros"
}
},
"require": {
"php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"psr/http-factory": "^1.1",
"psr/http-message": "^1.1 || ^2.0"
},
"require-dev": {
"ext-curl": "*",
"ext-dom": "*",
"ext-gd": "*",
"ext-libxml": "*",
"http-interop/http-factory-tests": "^2.2.0",
"laminas/laminas-coding-standard": "~3.1.0",
"php-http/psr7-integration-tests": "^1.4.0",
"phpunit/phpunit": "^10.5.36",
"psalm/plugin-phpunit": "^0.19.5",
"vimeo/psalm": "^6.13"
},
"provide": {
"psr/http-factory-implementation": "^1.0",
"psr/http-message-implementation": "^1.1 || ^2.0"
},
"autoload": {
"files": [
"src/functions/create_uploaded_file.php",
"src/functions/marshal_headers_from_sapi.php",
"src/functions/marshal_method_from_sapi.php",
"src/functions/marshal_protocol_version_from_sapi.php",
"src/functions/normalize_server.php",
"src/functions/normalize_uploaded_files.php",
"src/functions/parse_cookie_header.php"
],
"psr-4": {
"Laminas\\Diactoros\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"LaminasTest\\Diactoros\\": "test/"
}
},
"conflict": {
"amphp/amp": "<2.6.4"
},
"scripts": {
"check": [
"@cs-check",
"@test"
],
"cs-check": "phpcs",
"cs-fix": "phpcbf",
"test": "phpunit --colors=always",
"test-coverage": "phpunit --colors=always --coverage-clover clover.xml",
"static-analysis": "psalm --shepherd --stats"
}
}

View File

@@ -1,151 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Psr\Http\Message\StreamInterface;
use function array_pop;
use function assert;
use function implode;
use function is_string;
use function preg_match;
use function sprintf;
use function str_replace;
use function trim;
use function ucwords;
/**
* Provides base functionality for request and response de/serialization
* strategies, including functionality for retrieving a line at a time from
* the message, splitting headers from the body, and serializing headers.
*/
abstract class AbstractSerializer
{
public const CR = "\r";
public const EOL = "\r\n";
public const LF = "\n";
/**
* Retrieve a single line from the stream.
*
* Retrieves a line from the stream; a line is defined as a sequence of
* characters ending in a CRLF sequence.
*
* @throws Exception\DeserializationException If the sequence contains a CR
* or LF in isolation, or ends in a CR.
*/
protected static function getLine(StreamInterface $stream): string
{
$line = '';
$crFound = false;
while (! $stream->eof()) {
$char = $stream->read(1);
if ($crFound && $char === self::LF) {
$crFound = false;
break;
}
// CR NOT followed by LF
if ($crFound && $char !== self::LF) {
throw Exception\DeserializationException::forUnexpectedCarriageReturn();
}
// LF in isolation
if (! $crFound && $char === self::LF) {
throw Exception\DeserializationException::forUnexpectedLineFeed();
}
// CR found; do not append
if ($char === self::CR) {
$crFound = true;
continue;
}
// Any other character: append
$line .= $char;
}
// CR found at end of stream
if ($crFound) {
throw Exception\DeserializationException::forUnexpectedEndOfHeaders();
}
return $line;
}
/**
* Split the stream into headers and body content.
*
* Returns an array containing two elements
*
* - The first is an array of headers
* - The second is a StreamInterface containing the body content
*
* @throws Exception\DeserializationException For invalid headers.
*/
protected static function splitStream(StreamInterface $stream): array
{
$headers = [];
$currentHeader = false;
while ($line = self::getLine($stream)) {
if (preg_match(';^(?P<name>[!#$%&\'*+.^_`\|~0-9a-zA-Z-]+):(?P<value>.*)$;', $line, $matches)) {
$currentHeader = $matches['name'];
if (! isset($headers[$currentHeader])) {
$headers[$currentHeader] = [];
}
$headers[$currentHeader][] = trim($matches['value'], "\t ");
continue;
}
if ($currentHeader === false) {
throw Exception\DeserializationException::forInvalidHeader();
}
if (! preg_match('#^[ \t]#', $line)) {
throw Exception\DeserializationException::forInvalidHeaderContinuation();
}
// Append continuation to last header value found
$value = array_pop($headers[$currentHeader]);
assert(is_string($value));
$headers[$currentHeader][] = $value . ' ' . trim($line, "\t ");
}
// use RelativeStream to avoid copying initial stream into memory
return [$headers, new RelativeStream($stream, $stream->tell())];
}
/**
* Serialize headers to string values.
*
* @psalm-param array<non-empty-string, string[]> $headers
*/
protected static function serializeHeaders(array $headers): string
{
$lines = [];
foreach ($headers as $header => $values) {
$normalized = self::filterHeader($header);
foreach ($values as $value) {
$lines[] = sprintf('%s: %s', $normalized, $value);
}
}
return implode("\r\n", $lines);
}
/**
* Filter a header name to wordcase
*
* @param string $header
*/
protected static function filterHeader($header): string
{
$filtered = str_replace('-', ' ', $header);
$filtered = ucwords($filtered);
return str_replace(' ', '-', $filtered);
}
}

View File

@@ -1,192 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Override;
use Psr\Http\Message\StreamInterface;
use Stringable;
use function array_key_exists;
use const SEEK_SET;
/**
* Implementation of PSR HTTP streams
*/
class CallbackStream implements StreamInterface, Stringable
{
/** @var callable|null */
protected $callback;
/**
* @throws Exception\InvalidArgumentException
*/
public function __construct(callable $callback)
{
$this->attach($callback);
}
/**
* {@inheritdoc}
*/
#[Override]
public function __toString(): string
{
return $this->getContents();
}
/**
* {@inheritdoc}
*/
#[Override]
public function close(): void
{
$this->callback = null;
}
/**
* {@inheritdoc}
*
* @return null|callable
*/
#[Override]
public function detach(): ?callable
{
$callback = $this->callback;
$this->callback = null;
return $callback;
}
/**
* Attach a new callback to the instance.
*/
public function attach(callable $callback): void
{
$this->callback = $callback;
}
/**
* {@inheritdoc}
*/
#[Override]
public function getSize(): ?int
{
return null;
}
/**
* {@inheritdoc}
*/
#[Override]
public function tell(): int
{
throw Exception\UntellableStreamException::forCallbackStream();
}
/**
* {@inheritdoc}
*/
#[Override]
public function eof(): bool
{
return $this->callback === null;
}
/**
* {@inheritdoc}
*/
#[Override]
public function isSeekable(): bool
{
return false;
}
/**
* {@inheritdoc}
*/
#[Override]
public function seek(int $offset, int $whence = SEEK_SET): void
{
throw Exception\UnseekableStreamException::forCallbackStream();
}
/**
* {@inheritdoc}
*/
#[Override]
public function rewind(): void
{
throw Exception\UnrewindableStreamException::forCallbackStream();
}
/**
* {@inheritdoc}
*/
#[Override]
public function isWritable(): bool
{
return false;
}
/**
* {@inheritdoc}
*/
#[Override]
public function write(string $string): int
{
throw Exception\UnwritableStreamException::forCallbackStream();
}
/**
* {@inheritdoc}
*/
#[Override]
public function isReadable(): bool
{
return false;
}
/**
* {@inheritdoc}
*/
#[Override]
public function read(int $length): string
{
throw Exception\UnreadableStreamException::forCallbackStream();
}
/**
* {@inheritdoc}
*/
#[Override]
public function getContents(): string
{
$callback = $this->detach();
return $callback !== null ? (string) $callback() : '';
}
/**
* {@inheritdoc}
*/
#[Override]
public function getMetadata(?string $key = null)
{
$metadata = [
'eof' => $this->eof(),
'stream_type' => 'callback',
'seekable' => false,
];
if (null === $key) {
return $metadata;
}
if (! array_key_exists($key, $metadata)) {
return null;
}
return $metadata[$key];
}
}

View File

@@ -1,61 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UploadedFileFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;
class ConfigProvider
{
public const CONFIG_KEY = 'laminas-diactoros';
public const X_FORWARDED = 'x-forwarded-request-filter';
public const X_FORWARDED_TRUSTED_PROXIES = 'trusted-proxies';
public const X_FORWARDED_TRUSTED_HEADERS = 'trusted-headers';
/**
* Retrieve configuration for laminas-diactoros.
*/
public function __invoke(): array
{
return [
'dependencies' => $this->getDependencies(),
self::CONFIG_KEY => $this->getComponentConfig(),
];
}
/**
* Returns the container dependencies.
* Maps factory interfaces to factories.
*/
public function getDependencies(): array
{
// @codingStandardsIgnoreStart
return [
'invokables' => [
RequestFactoryInterface::class => RequestFactory::class,
ResponseFactoryInterface::class => ResponseFactory::class,
StreamFactoryInterface::class => StreamFactory::class,
ServerRequestFactoryInterface::class => ServerRequestFactory::class,
UploadedFileFactoryInterface::class => UploadedFileFactory::class,
UriFactoryInterface::class => UriFactory::class
],
];
// @codingStandardsIgnoreEnd
}
public function getComponentConfig(): array
{
return [
self::X_FORWARDED => [
self::X_FORWARDED_TRUSTED_PROXIES => '',
self::X_FORWARDED_TRUSTED_HEADERS => [],
],
];
}
}

View File

@@ -1,46 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use Throwable;
use UnexpectedValueException;
class DeserializationException extends UnexpectedValueException implements ExceptionInterface
{
public static function forInvalidHeader(): self
{
throw new self('Invalid header detected');
}
public static function forInvalidHeaderContinuation(): self
{
throw new self('Invalid header continuation');
}
public static function forRequestFromArray(Throwable $previous): self
{
return new self('Cannot deserialize request', (int) $previous->getCode(), $previous);
}
public static function forResponseFromArray(Throwable $previous): self
{
return new self('Cannot deserialize response', (int) $previous->getCode(), $previous);
}
public static function forUnexpectedCarriageReturn(): self
{
throw new self('Unexpected carriage return detected');
}
public static function forUnexpectedEndOfHeaders(): self
{
throw new self('Unexpected end of headers');
}
public static function forUnexpectedLineFeed(): self
{
throw new self('Unexpected line feed detected');
}
}

View File

@@ -1,14 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use Throwable;
/**
* Marker interface for package-specific exceptions.
*/
interface ExceptionInterface extends Throwable
{
}

View File

@@ -1,9 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@@ -1,27 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use Laminas\Diactoros\ServerRequestFilter\FilterUsingXForwardedHeaders;
use function get_debug_type;
use function is_string;
use function sprintf;
class InvalidForwardedHeaderNameException extends RuntimeException implements ExceptionInterface
{
public static function forHeader(mixed $name): self
{
if (! is_string($name)) {
$name = sprintf('(value of type %s)', get_debug_type($name));
}
return new self(sprintf(
'Invalid X-Forwarded-* header name "%s" provided to %s',
$name,
FilterUsingXForwardedHeaders::class
));
}
}

View File

@@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use function get_debug_type;
use function sprintf;
class InvalidProxyAddressException extends RuntimeException implements ExceptionInterface
{
public static function forInvalidProxyArgument(mixed $proxy): self
{
$type = get_debug_type($proxy);
return new self(sprintf(
'Invalid proxy of type "%s" provided;'
. ' must be a valid IPv4 or IPv6 address, optionally with a subnet mask provided'
. ' or an array of such values',
$type,
));
}
public static function forAddress(string $address): self
{
return new self(sprintf(
'Invalid proxy address "%s" provided;'
. ' must be a valid IPv4 or IPv6 address, optionally with a subnet mask provided',
$address,
));
}
}

View File

@@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use RuntimeException;
use Throwable;
class InvalidStreamPointerPositionException extends RuntimeException implements ExceptionInterface
{
/** {@inheritDoc} */
public function __construct(
string $message = 'Invalid pointer position',
int $code = 0,
?Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}

View File

@@ -1,9 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use UnexpectedValueException;
class SerializationException extends UnexpectedValueException implements ExceptionInterface
{
public static function forInvalidRequestLine(): self
{
return new self('Invalid request line detected');
}
public static function forInvalidStatusLine(): self
{
return new self('No status line detected');
}
}

View File

@@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use RuntimeException;
class UnreadableStreamException extends RuntimeException implements ExceptionInterface
{
public static function dueToConfiguration(): self
{
return new self('Stream is not readable');
}
public static function dueToMissingResource(): self
{
return new self('No resource available; cannot read');
}
public static function dueToPhpError(): self
{
return new self('Error reading stream');
}
public static function forCallbackStream(): self
{
return new self('Callback streams cannot read');
}
}

View File

@@ -1,17 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use UnexpectedValueException;
use function sprintf;
class UnrecognizedProtocolVersionException extends UnexpectedValueException implements ExceptionInterface
{
public static function forVersion(string $version): self
{
return new self(sprintf('Unrecognized protocol version (%s)', $version));
}
}

View File

@@ -1,15 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use RuntimeException;
class UnrewindableStreamException extends RuntimeException implements ExceptionInterface
{
public static function forCallbackStream(): self
{
return new self('Callback streams cannot rewind position');
}
}

View File

@@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use RuntimeException;
class UnseekableStreamException extends RuntimeException implements ExceptionInterface
{
public static function dueToConfiguration(): self
{
return new self('Stream is not seekable');
}
public static function dueToMissingResource(): self
{
return new self('No resource available; cannot seek position');
}
public static function dueToPhpError(): self
{
return new self('Error seeking within stream');
}
public static function forCallbackStream(): self
{
return new self('Callback streams cannot seek position');
}
}

View File

@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use RuntimeException;
class UntellableStreamException extends RuntimeException implements ExceptionInterface
{
public static function dueToMissingResource(): self
{
return new self('No resource available; cannot tell position');
}
public static function dueToPhpError(): self
{
return new self('Error occurred during tell operation');
}
public static function forCallbackStream(): self
{
return new self('Callback streams cannot tell position');
}
}

View File

@@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use RuntimeException;
class UnwritableStreamException extends RuntimeException implements ExceptionInterface
{
public static function dueToConfiguration(): self
{
return new self('Stream is not writable');
}
public static function dueToMissingResource(): self
{
return new self('No resource available; cannot write');
}
public static function dueToPhpError(): self
{
return new self('Error writing to stream');
}
public static function forCallbackStream(): self
{
return new self('Callback streams cannot write');
}
}

View File

@@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use RuntimeException;
use Throwable;
class UploadedFileAlreadyMovedException extends RuntimeException implements ExceptionInterface
{
/** {@inheritDoc} */
public function __construct(
string $message = 'Cannot retrieve stream after it has already moved',
int $code = 0,
?Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
}
}

View File

@@ -1,38 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Exception;
use RuntimeException;
use function sprintf;
class UploadedFileErrorException extends RuntimeException implements ExceptionInterface
{
public static function forUnmovableFile(): self
{
return new self('Error occurred while moving uploaded file');
}
public static function dueToStreamUploadError(string $error): self
{
return new self(sprintf(
'Cannot retrieve stream due to upload error: %s',
$error
));
}
public static function dueToUnwritablePath(): self
{
return new self('Unable to write to designated path');
}
public static function dueToUnwritableTarget(string $targetDirectory): self
{
return new self(sprintf(
'The target directory `%s` does not exist or is not writable',
$targetDirectory
));
}
}

View File

@@ -1,163 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use function get_debug_type;
use function in_array;
use function is_numeric;
use function is_string;
use function ord;
use function preg_match;
use function sprintf;
use function strlen;
/**
* Provide security tools around HTTP headers to prevent common injection vectors.
*/
final class HeaderSecurity
{
/**
* Private constructor; non-instantiable.
*
* @codeCoverageIgnore
*/
private function __construct()
{
}
/**
* Filter a header value
*
* Ensures CRLF header injection vectors are filtered.
*
* Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
* tabs are allowed in values; header continuations MUST consist of
* a single CRLF sequence followed by a space or horizontal tab.
*
* This method filters any values not allowed from the string, and is
* lossy.
*
* @see http://en.wikipedia.org/wiki/HTTP_response_splitting
*/
public static function filter(string $value): string
{
$length = strlen($value);
$string = '';
for ($i = 0; $i < $length; $i += 1) {
$ascii = ord($value[$i]);
// Detect continuation sequences
if ($ascii === 13) {
$lf = ord($value[$i + 1]);
$ws = ord($value[$i + 2]);
if ($lf === 10 && in_array($ws, [9, 32], true)) {
$string .= $value[$i] . $value[$i + 1];
$i += 1;
}
continue;
}
// Non-visible, non-whitespace characters
// 9 === horizontal tab
// 32-126, 128-254 === visible
// 127 === DEL
// 255 === null byte
if (
($ascii < 32 && $ascii !== 9)
|| $ascii === 127
|| $ascii > 254
) {
continue;
}
$string .= $value[$i];
}
return $string;
}
/**
* Validate a header value.
*
* Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
* tabs are allowed in values; header continuations MUST consist of
* a single CRLF sequence followed by a space or horizontal tab.
*
* @see http://en.wikipedia.org/wiki/HTTP_response_splitting
*
* @param string|int|float $value
*/
public static function isValid($value): bool
{
$value = (string) $value;
// Look for:
// \n not preceded by \r, OR
// \r not followed by \n, OR
// \r\n not followed by space or horizontal tab; these are all CRLF attacks
if (preg_match("#(?:(?:(?<!\r)\n)|(?:\r(?!\n))|(?:\r\n(?![ \t])))#", $value)) {
return false;
}
// Non-visible, non-whitespace characters
// 9 === horizontal tab
// 10 === line feed
// 13 === carriage return
// 32-126, 128-254 === visible
// 127 === DEL (disallowed)
// 255 === null byte (disallowed)
if (preg_match('/[^\x09\x0a\x0d\x20-\x7E\x80-\xFE]/', $value)) {
return false;
}
return true;
}
/**
* Assert a header value is valid.
*
* @param mixed $value Value to be tested. This method asserts it is a string or number.
* @throws Exception\InvalidArgumentException For invalid values.
*/
public static function assertValid(mixed $value): void
{
if (! is_string($value) && ! is_numeric($value)) {
throw new Exception\InvalidArgumentException(sprintf(
'Invalid header value type; must be a string or numeric; received %s',
get_debug_type($value)
));
}
if (! self::isValid($value)) {
throw new Exception\InvalidArgumentException(sprintf(
'"%s" is not valid header value',
$value
));
}
}
/**
* Assert whether or not a header name is valid.
*
* @see http://tools.ietf.org/html/rfc7230#section-3.2
*
* @throws Exception\InvalidArgumentException
*/
public static function assertValidName(mixed $name): void
{
if (! is_string($name)) {
throw new Exception\InvalidArgumentException(sprintf(
'Invalid header name type; expected string; received %s',
get_debug_type($name)
));
}
if (! preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $name)) {
throw new Exception\InvalidArgumentException(sprintf(
'"%s" is not valid header name',
$name
));
}
}
}

View File

@@ -1,407 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\StreamInterface;
use function array_map;
use function array_merge;
use function array_values;
use function implode;
use function is_array;
use function is_resource;
use function is_string;
use function preg_match;
use function sprintf;
use function str_replace;
use function strtolower;
use function trim;
/**
* Trait implementing the various methods defined in MessageInterface.
*
* @see https://github.com/php-fig/http-message/tree/master/src/MessageInterface.php
*/
trait MessageTrait
{
/**
* List of all registered headers, as key => array of values.
*
* @var array
* @psalm-var array<non-empty-string, list<string>>
*/
protected $headers = [];
/**
* Map of normalized header name to original name used to register header.
*
* @var array
* @psalm-var array<non-empty-string, non-empty-string>
*/
protected $headerNames = [];
/** @var string */
private $protocol = '1.1';
/** @var StreamInterface */
private $stream;
/**
* Retrieves the HTTP protocol version as a string.
*
* The string MUST contain only the HTTP version number (e.g., "1.1", "1.0").
*
* @return string HTTP protocol version.
*/
public function getProtocolVersion(): string
{
return $this->protocol;
}
/**
* Return an instance with the specified HTTP protocol version.
*
* The version string MUST contain only the HTTP version number (e.g.,
* "1.1", "1.0").
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new protocol version.
*
* @param string $version HTTP protocol version
* @return static
*/
public function withProtocolVersion(string $version): MessageInterface
{
$this->validateProtocolVersion($version);
$new = clone $this;
$new->protocol = $version;
return $new;
}
/**
* Retrieves all message headers.
*
* The keys represent the header name as it will be sent over the wire, and
* each value is an array of strings associated with the header.
*
* // Represent the headers as a string
* foreach ($message->getHeaders() as $name => $values) {
* echo $name . ": " . implode(", ", $values);
* }
*
* // Emit headers iteratively:
* foreach ($message->getHeaders() as $name => $values) {
* foreach ($values as $value) {
* header(sprintf('%s: %s', $name, $value), false);
* }
* }
*
* @return array Returns an associative array of the message's headers. Each
* key MUST be a header name, and each value MUST be an array of strings.
* @psalm-return array<non-empty-string, list<string>>
*/
public function getHeaders(): array
{
return $this->headers;
}
/**
* Checks if a header exists by the given case-insensitive name.
*
* @param string $name Case-insensitive header name.
* @return bool Returns true if any header names match the given header
* name using a case-insensitive string comparison. Returns false if
* no matching header name is found in the message.
*/
public function hasHeader(string $name): bool
{
return isset($this->headerNames[strtolower($name)]);
}
/**
* Retrieves a message header value by the given case-insensitive name.
*
* This method returns an array of all the header values of the given
* case-insensitive header name.
*
* If the header does not appear in the message, this method MUST return an
* empty array.
*
* @param string $name Case-insensitive header field name.
* @return string[] An array of string values as provided for the given
* header. If the header does not appear in the message, this method MUST
* return an empty array.
*/
public function getHeader(string $name): array
{
if (! $this->hasHeader($name)) {
return [];
}
/** @psalm-suppress PossiblyInvalidArrayOffset */
$name = $this->headerNames[strtolower($name)];
return $this->headers[$name];
}
/**
* Retrieves a comma-separated string of the values for a single header.
*
* This method returns all of the header values of the given
* case-insensitive header name as a string concatenated together using
* a comma.
*
* NOTE: Not all header values may be appropriately represented using
* comma concatenation. For such headers, use getHeader() instead
* and supply your own delimiter when concatenating.
*
* If the header does not appear in the message, this method MUST return
* an empty string.
*
* @param string $name Case-insensitive header field name.
* @return string A string of values as provided for the given header
* concatenated together using a comma. If the header does not appear in
* the message, this method MUST return an empty string.
*/
public function getHeaderLine(string $name): string
{
$value = $this->getHeader($name);
if (empty($value)) {
return '';
}
return implode(',', $value);
}
/**
* Return an instance with the provided header, replacing any existing
* values of any headers with the same case-insensitive name.
*
* While header names are case-insensitive, the casing of the header will
* be preserved by this function, and returned from getHeaders().
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new and/or updated header and value.
*
* @param string $name Case-insensitive header field name.
* @param string|string[] $value Header value(s).
* @return static
* @throws Exception\InvalidArgumentException For invalid header names or values.
*/
public function withHeader(string $name, $value): MessageInterface
{
$this->assertHeader($name);
$normalized = strtolower($name);
$new = clone $this;
if ($new->hasHeader($name)) {
unset($new->headers[$new->headerNames[$normalized]]);
}
$value = $this->filterHeaderValue($value);
$new->headerNames[$normalized] = $name;
$new->headers[$name] = $value;
return $new;
}
/**
* Return an instance with the specified header appended with the
* given value.
*
* Existing values for the specified header will be maintained. The new
* value(s) will be appended to the existing list. If the header did not
* exist previously, it will be added.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new header and/or value.
*
* @param string $name Case-insensitive header field name to add.
* @param string|string[] $value Header value(s).
* @return static
* @throws Exception\InvalidArgumentException For invalid header names or values.
*/
public function withAddedHeader(string $name, $value): MessageInterface
{
$this->assertHeader($name);
if (! $this->hasHeader($name)) {
return $this->withHeader($name, $value);
}
$header = $this->headerNames[strtolower($name)];
$new = clone $this;
$value = $this->filterHeaderValue($value);
$new->headers[$header] = array_merge($this->headers[$header], $value);
return $new;
}
/**
* Return an instance without the specified header.
*
* Header resolution MUST be done without case-sensitivity.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that removes
* the named header.
*
* @param string $name Case-insensitive header field name to remove.
* @return static
*/
public function withoutHeader(string $name): MessageInterface
{
if ($name === '' || ! $this->hasHeader($name)) {
return clone $this;
}
$normalized = strtolower($name);
$original = $this->headerNames[$normalized];
$new = clone $this;
unset($new->headers[$original], $new->headerNames[$normalized]);
return $new;
}
/**
* Gets the body of the message.
*
* @return StreamInterface Returns the body as a stream.
*/
public function getBody(): StreamInterface
{
return $this->stream;
}
/**
* Return an instance with the specified message body.
*
* The body MUST be a StreamInterface object.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return a new instance that has the
* new body stream.
*
* @param StreamInterface $body Body.
* @return static
* @throws Exception\InvalidArgumentException When the body is not valid.
*/
public function withBody(StreamInterface $body): MessageInterface
{
$new = clone $this;
$new->stream = $body;
return $new;
}
/** @param StreamInterface|string|resource $stream */
private function getStream($stream, string $modeIfNotInstance): StreamInterface
{
if ($stream instanceof StreamInterface) {
return $stream;
}
/** @psalm-suppress DocblockTypeContradiction */
if (! is_string($stream) && ! is_resource($stream)) {
throw new Exception\InvalidArgumentException(
'Stream must be a string stream resource identifier, '
. 'an actual stream resource, '
. 'or a Psr\Http\Message\StreamInterface implementation'
);
}
return new Stream($stream, $modeIfNotInstance);
}
/**
* Filter a set of headers to ensure they are in the correct internal format.
*
* Used by message constructors to allow setting all initial headers at once.
*
* @param array<non-empty-string, string|string[]> $originalHeaders Headers to filter.
*/
private function setHeaders(array $originalHeaders): void
{
$headerNames = $headers = [];
foreach ($originalHeaders as $header => $value) {
$value = $this->filterHeaderValue($value);
$this->assertHeader($header);
$headerNames[strtolower($header)] = $header;
$headers[$header] = $value;
}
$this->headerNames = $headerNames;
$this->headers = $headers;
}
/**
* Validate the HTTP protocol version
*
* @throws Exception\InvalidArgumentException On invalid HTTP protocol version.
*/
private function validateProtocolVersion(string $version): void
{
if (empty($version)) {
throw new Exception\InvalidArgumentException(
'HTTP protocol version can not be empty'
);
}
// HTTP/1 uses a "<major>.<minor>" numbering scheme to indicate
// versions of the protocol, while HTTP/2 does not.
if (! preg_match('#^(1\.[01]|2(\.0)?)$#', $version)) {
throw new Exception\InvalidArgumentException(sprintf(
'Unsupported HTTP protocol version "%s" provided',
$version
));
}
}
/** @return list<string> */
private function filterHeaderValue(mixed $values): array
{
if (! is_array($values)) {
$values = [$values];
}
if ([] === $values) {
throw new Exception\InvalidArgumentException(
'Invalid header value: must be a string or array of strings; '
. 'cannot be an empty array'
);
}
return array_map(static function ($value): string {
HeaderSecurity::assertValid($value);
$value = (string) $value;
// Normalize line folding to a single space (RFC 7230#3.2.4).
$value = str_replace(["\r\n\t", "\r\n "], ' ', $value);
// Remove optional whitespace (OWS, RFC 7230#3.2.3) around the header value.
return trim($value, "\t ");
}, array_values($values));
}
/**
* Ensure header name and values are valid.
*
* @psalm-assert non-empty-string $name
* @throws Exception\InvalidArgumentException
*/
private function assertHeader(mixed $name): void
{
HeaderSecurity::assertValidName($name);
}
}

View File

@@ -1,15 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
class Module
{
public function getConfig(): array
{
return [
'service_manager' => (new ConfigProvider())->getDependencies(),
];
}
}

View File

@@ -1,182 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Override;
use Psr\Http\Message\StreamInterface;
use Stringable;
use const SEEK_SET;
/**
* Wrapper for default Stream class, representing subpart (starting from given offset) of initial stream.
* It can be used to avoid copying full stream, conserving memory.
*
* @see AbstractSerializer::splitStream()
*/
final class RelativeStream implements StreamInterface, Stringable
{
private readonly int $offset;
public function __construct(private readonly StreamInterface $decoratedStream, ?int $offset)
{
$this->offset = (int) $offset;
}
/**
* {@inheritdoc}
*/
#[Override]
public function __toString(): string
{
if ($this->isSeekable()) {
$this->seek(0);
}
return $this->getContents();
}
/**
* {@inheritdoc}
*/
#[Override]
public function close(): void
{
$this->decoratedStream->close();
}
/**
* {@inheritdoc}
*/
#[Override]
public function detach()
{
return $this->decoratedStream->detach();
}
/**
* {@inheritdoc}
*/
#[Override]
public function getSize(): ?int
{
$size = $this->decoratedStream->getSize();
if ($size === null) {
return null;
}
return $size - $this->offset;
}
/**
* {@inheritdoc}
*/
#[Override]
public function tell(): int
{
return $this->decoratedStream->tell() - $this->offset;
}
/**
* {@inheritdoc}
*/
#[Override]
public function eof(): bool
{
return $this->decoratedStream->eof();
}
/**
* {@inheritdoc}
*/
#[Override]
public function isSeekable(): bool
{
return $this->decoratedStream->isSeekable();
}
/**
* {@inheritdoc}
*/
#[Override]
public function seek(int $offset, int $whence = SEEK_SET): void
{
if ($whence === SEEK_SET) {
$this->decoratedStream->seek($offset + $this->offset, $whence);
return;
}
$this->decoratedStream->seek($offset, $whence);
}
/**
* {@inheritdoc}
*/
#[Override]
public function rewind(): void
{
$this->seek(0);
}
/**
* {@inheritdoc}
*/
#[Override]
public function isWritable(): bool
{
return $this->decoratedStream->isWritable();
}
/**
* {@inheritdoc}
*/
#[Override]
public function write(string $string): int
{
if ($this->tell() < 0) {
throw new Exception\InvalidStreamPointerPositionException();
}
return $this->decoratedStream->write($string);
}
/**
* {@inheritdoc}
*/
#[Override]
public function isReadable(): bool
{
return $this->decoratedStream->isReadable();
}
/**
* {@inheritdoc}
*/
#[Override]
public function read(int $length): string
{
if ($this->tell() < 0) {
throw new Exception\InvalidStreamPointerPositionException();
}
return $this->decoratedStream->read($length);
}
/**
* {@inheritdoc}
*/
#[Override]
public function getContents(): string
{
if ($this->tell() < 0) {
throw new Exception\InvalidStreamPointerPositionException();
}
return $this->decoratedStream->getContents();
}
/**
* {@inheritdoc}
*/
#[Override]
public function getMetadata(?string $key = null)
{
return $this->decoratedStream->getMetadata($key);
}
}

View File

@@ -1,75 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Override;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
use function strtolower;
/**
* HTTP Request encapsulation
*
* Requests are considered immutable; all methods that might change state are
* implemented such that they retain the internal state of the current
* message and return a new instance that contains the changed state.
*/
class Request implements RequestInterface
{
use RequestTrait;
/**
* @param null|string|UriInterface $uri URI for the request, if any.
* @param null|string $method HTTP method for the request, if any.
* @param string|resource|StreamInterface $body Message body, if any.
* @param array<non-empty-string, string|string[]> $headers Headers for the message, if any.
* @throws Exception\InvalidArgumentException For any invalid value.
*/
public function __construct($uri = null, ?string $method = null, $body = 'php://temp', array $headers = [])
{
$this->initialize($uri, $method, $body, $headers);
}
/**
* {@inheritdoc}
*/
#[Override]
public function getHeaders(): array
{
$headers = $this->headers;
if (
! $this->hasHeader('host')
&& $this->uri->getHost()
) {
$headers['Host'] = [$this->getHostFromUri()];
}
return $headers;
}
/**
* {@inheritdoc}
*/
#[Override]
public function getHeader(string $name): array
{
if (empty($name) || ! $this->hasHeader($name)) {
if (
strtolower($name) === 'host'
&& $this->uri->getHost()
) {
return [$this->getHostFromUri()];
}
return [];
}
$header = $this->headerNames[strtolower($name)];
return $this->headers[$header];
}
}

View File

@@ -1,86 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Request;
use Laminas\Diactoros\Exception;
use Laminas\Diactoros\Request;
use Laminas\Diactoros\Stream;
use Psr\Http\Message\RequestInterface;
use Throwable;
use function sprintf;
/**
* Serialize or deserialize request messages to/from arrays.
*
* This class provides functionality for serializing a RequestInterface instance
* to an array, as well as the reverse operation of creating a Request instance
* from an array representing a message.
*/
final class ArraySerializer
{
/**
* Serialize a request message to an array.
*
* @return array{
* method: string,
* request_target: string,
* uri: string,
* protocol_version: string,
* headers: array<array<string>>,
* body: string
* }
*/
public static function toArray(RequestInterface $request): array
{
return [
'method' => $request->getMethod(),
'request_target' => $request->getRequestTarget(),
'uri' => (string) $request->getUri(),
'protocol_version' => $request->getProtocolVersion(),
'headers' => $request->getHeaders(),
'body' => (string) $request->getBody(),
];
}
/**
* Deserialize a request array to a request instance.
*
* @throws Exception\DeserializationException When the response cannot be deserialized.
*/
public static function fromArray(array $serializedRequest): Request
{
try {
$uri = self::getValueFromKey($serializedRequest, 'uri');
$method = self::getValueFromKey($serializedRequest, 'method');
$body = new Stream('php://memory', 'wb+');
$body->write(self::getValueFromKey($serializedRequest, 'body'));
$headers = self::getValueFromKey($serializedRequest, 'headers');
$requestTarget = self::getValueFromKey($serializedRequest, 'request_target');
$protocolVersion = self::getValueFromKey($serializedRequest, 'protocol_version');
return (new Request($uri, $method, $body, $headers))
->withRequestTarget($requestTarget)
->withProtocolVersion($protocolVersion);
} catch (Throwable $exception) {
throw Exception\DeserializationException::forRequestFromArray($exception);
}
}
/**
* @return mixed
* @throws Exception\DeserializationException
*/
private static function getValueFromKey(array $data, string $key, ?string $message = null)
{
if (isset($data[$key])) {
return $data[$key];
}
if ($message === null) {
$message = sprintf('Missing "%s" key in serialized request', $key);
}
throw new Exception\DeserializationException($message);
}
}

View File

@@ -1,137 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Request;
use Laminas\Diactoros\AbstractSerializer;
use Laminas\Diactoros\Exception;
use Laminas\Diactoros\Request;
use Laminas\Diactoros\Stream;
use Laminas\Diactoros\Uri;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use function preg_match;
use function sprintf;
/**
* Serialize (cast to string) or deserialize (cast string to Request) messages.
*
* This class provides functionality for serializing a RequestInterface instance
* to a string, as well as the reverse operation of creating a Request instance
* from a string/stream representing a message.
*/
final class Serializer extends AbstractSerializer
{
/**
* Deserialize a request string to a request instance.
*
* Internally, casts the message to a stream and invokes fromStream().
*
* @throws Exception\SerializationException When errors occur parsing the message.
*/
public static function fromString(string $message): Request
{
$stream = new Stream('php://temp', 'wb+');
$stream->write($message);
return self::fromStream($stream);
}
/**
* Deserialize a request stream to a request instance.
*
* @throws Exception\InvalidArgumentException If the message stream is not readable or seekable.
* @throws Exception\SerializationException If an invalid request line is detected.
*/
public static function fromStream(StreamInterface $stream): Request
{
if (! $stream->isReadable() || ! $stream->isSeekable()) {
throw new Exception\InvalidArgumentException('Message stream must be both readable and seekable');
}
$stream->rewind();
[$method, $requestTarget, $version] = self::getRequestLine($stream);
$uri = self::createUriFromRequestTarget($requestTarget);
[$headers, $body] = self::splitStream($stream);
return (new Request($uri, $method, $body, $headers))
->withProtocolVersion($version)
->withRequestTarget($requestTarget);
}
/**
* Serialize a request message to a string.
*/
public static function toString(RequestInterface $request): string
{
$httpMethod = $request->getMethod();
$headers = self::serializeHeaders($request->getHeaders());
$body = (string) $request->getBody();
$format = '%s %s HTTP/%s%s%s';
if (! empty($headers)) {
$headers = "\r\n" . $headers;
}
if (! empty($body)) {
$headers .= "\r\n\r\n";
}
return sprintf(
$format,
$httpMethod,
$request->getRequestTarget(),
$request->getProtocolVersion(),
$headers,
$body
);
}
/**
* Retrieve the components of the request line.
*
* Retrieves the first line of the stream and parses it, raising an
* exception if it does not follow specifications; if valid, returns a list
* with the method, target, and version, in that order.
*
* @throws Exception\SerializationException
*/
private static function getRequestLine(StreamInterface $stream): array
{
$requestLine = self::getLine($stream);
if (
! preg_match(
'#^(?P<method>[!\#$%&\'*+.^_`|~a-zA-Z0-9-]+) (?P<target>[^\s]+) HTTP/(?P<version>[1-9]\d*\.\d+)$#',
$requestLine,
$matches
)
) {
throw Exception\SerializationException::forInvalidRequestLine();
}
return [$matches['method'], $matches['target'], $matches['version']];
}
/**
* Create and return a Uri instance based on the provided request target.
*
* If the request target is of authority or asterisk form, an empty Uri
* instance is returned; otherwise, the value is used to create and return
* a new Uri instance.
*/
private static function createUriFromRequestTarget(string $requestTarget): Uri
{
if (preg_match('#^https?://#', $requestTarget)) {
return new Uri($requestTarget);
}
if (preg_match('#^(\*|[^/])#', $requestTarget)) {
return new Uri();
}
return new Uri($requestTarget);
}
}

View File

@@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Override;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
class RequestFactory implements RequestFactoryInterface
{
/**
* {@inheritDoc}
*/
#[Override]
public function createRequest(string $method, $uri): RequestInterface
{
return new Request($uri, $method);
}
}

View File

@@ -1,300 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
use function array_keys;
use function is_string;
use function preg_match;
use function sprintf;
use function strtolower;
/**
* Trait with common request behaviors.
*
* Server and client-side requests differ slightly in how the Host header is
* handled; on client-side, it should be calculated on-the-fly from the
* composed URI (if present), while on server-side, it will be calculated from
* the environment. As such, this trait exists to provide the common code
* between both client-side and server-side requests, and each can then
* use the headers functionality required by their implementations.
*/
trait RequestTrait
{
use MessageTrait;
/** @var string */
private $method = 'GET';
/**
* The request-target, if it has been provided or calculated.
*
* @var null|string
*/
private $requestTarget;
/** @var UriInterface */
private $uri;
/**
* Initialize request state.
*
* Used by constructors.
*
* @param null|string|UriInterface $uri URI for the request, if any.
* @param null|string $method HTTP method for the request, if any.
* @param string|resource|StreamInterface $body Message body, if any.
* @param array<non-empty-string, string|string[]> $headers Headers for the message, if any.
* @throws Exception\InvalidArgumentException For any invalid value.
*/
private function initialize(
$uri = null,
?string $method = null,
$body = 'php://memory',
array $headers = []
): void {
if ($method !== null) {
$this->setMethod($method);
}
$this->uri = $this->createUri($uri);
$this->stream = $this->getStream($body, 'wb+');
$this->setHeaders($headers);
// per PSR-7: attempt to set the Host header from a provided URI if no
// Host header is provided
if (! $this->hasHeader('Host') && $this->uri->getHost()) {
$this->headerNames['host'] = 'Host';
$this->headers['Host'] = [$this->getHostFromUri()];
}
}
/**
* Create and return a URI instance.
*
* If `$uri` is a already a `UriInterface` instance, returns it.
*
* If `$uri` is a string, passes it to the `Uri` constructor to return an
* instance.
*
* If `$uri is null, creates and returns an empty `Uri` instance.
*
* Otherwise, it raises an exception.
*
* @throws Exception\InvalidArgumentException
*/
private function createUri(null|string|UriInterface $uri): UriInterface
{
if ($uri instanceof UriInterface) {
return $uri;
}
if (is_string($uri)) {
return new Uri($uri);
}
return new Uri();
}
/**
* Retrieves the message's request target.
*
* Retrieves the message's request-target either as it will appear (for
* clients), as it appeared at request (for servers), or as it was
* specified for the instance (see withRequestTarget()).
*
* In most cases, this will be the origin-form of the composed URI,
* unless a value was provided to the concrete implementation (see
* withRequestTarget() below).
*
* If no URI is available, and no request-target has been specifically
* provided, this method MUST return the string "/".
*/
public function getRequestTarget(): string
{
if (null !== $this->requestTarget) {
return $this->requestTarget;
}
$target = $this->uri->getPath();
if ($this->uri->getQuery()) {
$target .= '?' . $this->uri->getQuery();
}
if (empty($target)) {
$target = '/';
}
return $target;
}
/**
* Create a new instance with a specific request-target.
*
* If the request needs a non-origin-form request-target — e.g., for
* specifying an absolute-form, authority-form, or asterisk-form —
* this method may be used to create an instance with the specified
* request-target, verbatim.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return a new instance that has the
* changed request target.
*
* @link http://tools.ietf.org/html/rfc7230#section-2.7 (for the various
* request-target forms allowed in request messages)
*
* @throws Exception\InvalidArgumentException If the request target is invalid.
* @return static
*/
public function withRequestTarget(string $requestTarget): RequestInterface
{
if (preg_match('#\s#', $requestTarget)) {
throw new Exception\InvalidArgumentException(
'Invalid request target provided; cannot contain whitespace'
);
}
$new = clone $this;
$new->requestTarget = $requestTarget;
return $new;
}
/**
* Retrieves the HTTP method of the request.
*
* @return string Returns the request method.
*/
public function getMethod(): string
{
return $this->method;
}
/**
* Return an instance with the provided HTTP method.
*
* While HTTP method names are typically all uppercase characters, HTTP
* method names are case-sensitive and thus implementations SHOULD NOT
* modify the given string.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* changed request method.
*
* @param string $method Case-insensitive method.
* @throws Exception\InvalidArgumentException For invalid HTTP methods.
* @return static
*/
public function withMethod(string $method): RequestInterface
{
$new = clone $this;
$new->setMethod($method);
return $new;
}
/**
* Retrieves the URI instance.
*
* This method MUST return a UriInterface instance.
*
* @link http://tools.ietf.org/html/rfc3986#section-4.3
*
* @return UriInterface Returns a UriInterface instance
* representing the URI of the request, if any.
*/
public function getUri(): UriInterface
{
return $this->uri;
}
/**
* Returns an instance with the provided URI.
*
* This method will update the Host header of the returned request by
* default if the URI contains a host component. If the URI does not
* contain a host component, any pre-existing Host header will be carried
* over to the returned request.
*
* You can opt-in to preserving the original state of the Host header by
* setting `$preserveHost` to `true`. When `$preserveHost` is set to
* `true`, the returned request will not update the Host header of the
* returned message -- even if the message contains no Host header. This
* means that a call to `getHeader('Host')` on the original request MUST
* equal the return value of a call to `getHeader('Host')` on the returned
* request.
*
* This method MUST be implemented in such a way as to retain the
* immutability of the message, and MUST return an instance that has the
* new UriInterface instance.
*
* @link http://tools.ietf.org/html/rfc3986#section-4.3
*
* @param UriInterface $uri New request URI to use.
* @param bool $preserveHost Preserve the original state of the Host header.
* @return static
*/
public function withUri(UriInterface $uri, bool $preserveHost = false): RequestInterface
{
$new = clone $this;
$new->uri = $uri;
if ($preserveHost && $this->hasHeader('Host')) {
return $new;
}
if (! $uri->getHost()) {
return $new;
}
$host = $uri->getHost();
if ($uri->getPort() !== null) {
$host .= ':' . $uri->getPort();
}
$new->headerNames['host'] = 'Host';
// Remove an existing host header if present, regardless of current
// de-normalization of the header name.
// @see https://github.com/zendframework/zend-diactoros/issues/91
foreach (array_keys($new->headers) as $header) {
if (strtolower($header) === 'host') {
unset($new->headers[$header]);
}
}
$new->headers['Host'] = [$host];
return $new;
}
/**
* Set and validate the HTTP method
*
* @throws Exception\InvalidArgumentException On invalid HTTP method.
*/
private function setMethod(string $method): void
{
if (! preg_match('/^[!#$%&\'*+.^_`\|~0-9a-z-]+$/i', $method)) {
throw new Exception\InvalidArgumentException(sprintf(
'Unsupported HTTP method "%s" provided',
$method
));
}
$this->method = $method;
}
/**
* Retrieve the host from the URI instance
*/
private function getHostFromUri(): string
{
$host = $this->uri->getHost();
$host .= $this->uri->getPort() !== null ? ':' . $this->uri->getPort() : '';
return $host;
}
}

View File

@@ -1,180 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Override;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use function sprintf;
/**
* HTTP response encapsulation.
*
* Responses are considered immutable; all methods that might change state are
* implemented such that they retain the internal state of the current
* message and return a new instance that contains the changed state.
*/
class Response implements ResponseInterface
{
use MessageTrait;
public const MIN_STATUS_CODE_VALUE = 100;
public const MAX_STATUS_CODE_VALUE = 599;
/**
* Map of standard HTTP status code/reason phrases
*
* @psalm-var array<positive-int, non-empty-string>
*/
private array $phrases = [
// INFORMATIONAL CODES
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing',
103 => 'Early Hints',
// phpcs:ignore Generic.Files.LineLength.TooLong
104 => 'Upload Resumption Supported (TEMPORARY - registered 2024-11-13, extension registered 2025-09-15, expires 2026-11-13)',
// SUCCESS CODES
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
207 => 'Multi-Status',
208 => 'Already Reported',
226 => 'IM Used',
// REDIRECTION CODES
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
306 => 'Switch Proxy', // Deprecated to 306 => '(Unused)'
307 => 'Temporary Redirect',
308 => 'Permanent Redirect',
// CLIENT ERROR
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Content Too Large',
414 => 'URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Range Not Satisfiable',
417 => 'Expectation Failed',
418 => 'I\'m a teapot',
421 => 'Misdirected Request',
422 => 'Unprocessable Content',
423 => 'Locked',
424 => 'Failed Dependency',
425 => 'Too Early',
426 => 'Upgrade Required',
428 => 'Precondition Required',
429 => 'Too Many Requests',
431 => 'Request Header Fields Too Large',
444 => 'Connection Closed Without Response',
451 => 'Unavailable For Legal Reasons',
// SERVER ERROR
499 => 'Client Closed Request',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
506 => 'Variant Also Negotiates',
507 => 'Insufficient Storage',
508 => 'Loop Detected',
510 => 'Not Extended (OBSOLETED)',
511 => 'Network Authentication Required',
599 => 'Network Connect Timeout Error',
];
private string $reasonPhrase;
private int $statusCode;
/**
* @param string|resource|StreamInterface $body Stream identifier and/or actual stream resource
* @param int $status Status code for the response, if any.
* @param array<non-empty-string, string|string[]> $headers Headers for the response, if any.
* @throws Exception\InvalidArgumentException On any invalid element.
*/
public function __construct($body = 'php://memory', int $status = 200, array $headers = [])
{
$this->setStatusCode($status);
$this->stream = $this->getStream($body, 'wb+');
$this->setHeaders($headers);
}
/**
* {@inheritdoc}
*/
#[Override]
public function getStatusCode(): int
{
return $this->statusCode;
}
/**
* {@inheritdoc}
*/
#[Override]
public function getReasonPhrase(): string
{
return $this->reasonPhrase;
}
/**
* {@inheritdoc}
*/
#[Override]
public function withStatus(int $code, string $reasonPhrase = ''): Response
{
$new = clone $this;
$new->setStatusCode($code, $reasonPhrase);
return $new;
}
/**
* Set a valid status code.
*
* @throws Exception\InvalidArgumentException On an invalid status code.
*/
private function setStatusCode(int $code, string $reasonPhrase = ''): void
{
if (
$code < static::MIN_STATUS_CODE_VALUE
|| $code > static::MAX_STATUS_CODE_VALUE
) {
throw new Exception\InvalidArgumentException(sprintf(
'Invalid status code "%s"; must be an integer between %d and %d, inclusive',
$code,
self::MIN_STATUS_CODE_VALUE,
self::MAX_STATUS_CODE_VALUE
));
}
if ($reasonPhrase === '' && isset($this->phrases[$code])) {
$reasonPhrase = $this->phrases[$code];
}
$this->reasonPhrase = $reasonPhrase;
$this->statusCode = $code;
}
}

View File

@@ -1,84 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Response;
use Laminas\Diactoros\Exception;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\Stream;
use Psr\Http\Message\ResponseInterface;
use Throwable;
use function sprintf;
/**
* Serialize or deserialize response messages to/from arrays.
*
* This class provides functionality for serializing a ResponseInterface instance
* to an array, as well as the reverse operation of creating a Response instance
* from an array representing a message.
*/
final class ArraySerializer
{
/**
* Serialize a response message to an array.
*
* @return array{
* status_code: int,
* reason_phrase: string,
* protocol_version: string,
* headers: array<array<string>>,
* body: string
* }
*/
public static function toArray(ResponseInterface $response): array
{
return [
'status_code' => $response->getStatusCode(),
'reason_phrase' => $response->getReasonPhrase(),
'protocol_version' => $response->getProtocolVersion(),
'headers' => $response->getHeaders(),
'body' => (string) $response->getBody(),
];
}
/**
* Deserialize a response array to a response instance.
*
* @throws Exception\DeserializationException When cannot deserialize response.
*/
public static function fromArray(array $serializedResponse): Response
{
try {
$body = new Stream('php://memory', 'wb+');
$body->write(self::getValueFromKey($serializedResponse, 'body'));
$statusCode = self::getValueFromKey($serializedResponse, 'status_code');
$headers = self::getValueFromKey($serializedResponse, 'headers');
$protocolVersion = self::getValueFromKey($serializedResponse, 'protocol_version');
$reasonPhrase = self::getValueFromKey($serializedResponse, 'reason_phrase');
return (new Response($body, $statusCode, $headers))
->withProtocolVersion($protocolVersion)
->withStatus($statusCode, $reasonPhrase);
} catch (Throwable $exception) {
throw Exception\DeserializationException::forResponseFromArray($exception);
}
}
/**
* @return mixed
* @throws Exception\DeserializationException
*/
private static function getValueFromKey(array $data, string $key, ?string $message = null)
{
if (isset($data[$key])) {
return $data[$key];
}
if ($message === null) {
$message = sprintf('Missing "%s" key in serialized response', $key);
}
throw new Exception\DeserializationException($message);
}
}

View File

@@ -1,36 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Response;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\Stream;
/**
* A class representing empty HTTP responses.
*/
class EmptyResponse extends Response
{
/**
* Create an empty response with the given status code.
*
* @param int $status Status code for the response, if any.
* @param array<non-empty-string, string|string[]> $headers Headers for the response, if any.
*/
public function __construct(int $status = 204, array $headers = [])
{
$body = new Stream('php://temp', 'r');
parent::__construct($body, $status, $headers);
}
/**
* Create an empty response with the given headers.
*
* @param array<non-empty-string, string[]> $headers Headers for the response.
*/
public static function withHeaders(array $headers): EmptyResponse
{
return new static(204, $headers);
}
}

View File

@@ -1,73 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Response;
use Laminas\Diactoros\Exception;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\Stream;
use Psr\Http\Message\StreamInterface;
use function get_debug_type;
use function is_string;
use function sprintf;
/**
* HTML response.
*
* Allows creating a response by passing an HTML string to the constructor;
* by default, sets a status code of 200 and sets the Content-Type header to
* text/html.
*/
class HtmlResponse extends Response
{
use InjectContentTypeTrait;
/**
* Create an HTML response.
*
* Produces an HTML response with a Content-Type of text/html and a default
* status of 200.
*
* @param string|StreamInterface $html HTML or stream for the message body.
* @param int $status Integer status code for the response; 200 by default.
* @param array<non-empty-string, string|string[]> $headers Array of headers to use at initialization.
* @throws Exception\InvalidArgumentException If $html is neither a string or stream.
*/
public function __construct($html, int $status = 200, array $headers = [])
{
parent::__construct(
$this->createBody($html),
$status,
$this->injectContentType('text/html; charset=utf-8', $headers)
);
}
/**
* Create the message body.
*
* @param string|StreamInterface $html
* @throws Exception\InvalidArgumentException If $html is neither a string or stream.
*/
private function createBody($html): StreamInterface
{
if ($html instanceof StreamInterface) {
return $html;
}
/** @psalm-suppress DocblockTypeContradiction */
if (! is_string($html)) {
throw new Exception\InvalidArgumentException(sprintf(
'Invalid content (%s) provided to %s',
get_debug_type($html),
self::class
));
}
$body = new Stream('php://temp', 'wb+');
$body->write($html);
$body->rewind();
return $body;
}
}

View File

@@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Response;
use function array_keys;
use function array_reduce;
use function strtolower;
trait InjectContentTypeTrait
{
/**
* Inject the provided Content-Type, if none is already present.
*
* @param array<non-empty-string, string|string[]> $headers
* @return array<non-empty-string, string|string[]> Headers with injected Content-Type
*/
private function injectContentType(string $contentType, array $headers): array
{
$hasContentType = array_reduce(
array_keys($headers),
static fn(bool $carry, string $item): bool => $carry ?: strtolower($item) === 'content-type',
false
);
if (! $hasContentType) {
$headers['content-type'] = [$contentType];
}
return $headers;
}
}

View File

@@ -1,162 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Response;
use JsonException;
use Laminas\Diactoros\Exception;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\Stream;
use function is_object;
use function is_resource;
use function json_encode;
use function sprintf;
use const JSON_HEX_AMP;
use const JSON_HEX_APOS;
use const JSON_HEX_QUOT;
use const JSON_HEX_TAG;
use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_SLASHES;
/**
* JSON response.
*
* Allows creating a response by passing data to the constructor; by default,
* serializes the data to JSON, sets a status code of 200 and sets the
* Content-Type header to application/json.
*/
class JsonResponse extends Response
{
use InjectContentTypeTrait;
/**
* Default flags for json_encode
*
* @const int
*/
public const DEFAULT_JSON_FLAGS = JSON_HEX_TAG
| JSON_HEX_APOS
| JSON_HEX_AMP
| JSON_HEX_QUOT
| JSON_UNESCAPED_SLASHES;
/** @var mixed */
private $payload;
/**
* Create a JSON response with the given data.
*
* Default JSON encoding is performed with the following options, which
* produces RFC4627-compliant JSON, capable of embedding into HTML.
*
* - JSON_HEX_TAG
* - JSON_HEX_APOS
* - JSON_HEX_AMP
* - JSON_HEX_QUOT
* - JSON_UNESCAPED_SLASHES
*
* @param mixed $data Data to convert to JSON.
* @param int $status Integer status code for the response; 200 by default.
* @param array<non-empty-string, string|string[]> $headers Array of headers to use at initialization.
* @param int $encodingOptions JSON encoding options to use.
* @throws Exception\InvalidArgumentException If unable to encode the $data to JSON.
*/
public function __construct(
$data,
int $status = 200,
array $headers = [],
private int $encodingOptions = self::DEFAULT_JSON_FLAGS
) {
$this->setPayload($data);
$json = $this->jsonEncode($data, $this->encodingOptions);
$body = $this->createBodyFromJson($json);
$headers = $this->injectContentType('application/json', $headers);
parent::__construct($body, $status, $headers);
}
/**
* @return mixed
*/
public function getPayload()
{
return $this->payload;
}
public function withPayload(mixed $data): JsonResponse
{
$new = clone $this;
$new->setPayload($data);
return $this->updateBodyFor($new);
}
public function getEncodingOptions(): int
{
return $this->encodingOptions;
}
public function withEncodingOptions(int $encodingOptions): JsonResponse
{
$new = clone $this;
$new->encodingOptions = $encodingOptions;
return $this->updateBodyFor($new);
}
private function createBodyFromJson(string $json): Stream
{
$body = new Stream('php://temp', 'wb+');
$body->write($json);
$body->rewind();
return $body;
}
/**
* Encode the provided data to JSON.
*
* @throws Exception\InvalidArgumentException If unable to encode the $data to JSON.
*/
private function jsonEncode(mixed $data, int $encodingOptions): string
{
if (is_resource($data)) {
throw new Exception\InvalidArgumentException('Cannot JSON encode resources');
}
try {
return json_encode($data, $encodingOptions | JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
throw new Exception\InvalidArgumentException(sprintf(
'Unable to encode data to JSON in %s: %s',
self::class,
$e->getMessage()
), 0, $e);
}
}
private function setPayload(mixed $data): void
{
if (is_object($data)) {
$data = clone $data;
}
$this->payload = $data;
}
/**
* Update the response body for the given instance.
*
* @param self $toUpdate Instance to update.
* @return JsonResponse Returns a new instance with an updated body.
*/
private function updateBodyFor(JsonResponse $toUpdate): JsonResponse
{
$json = $this->jsonEncode($toUpdate->payload, $toUpdate->encodingOptions);
$body = $this->createBodyFromJson($json);
return $toUpdate->withBody($body);
}
}

View File

@@ -1,45 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Response;
use Laminas\Diactoros\Exception;
use Laminas\Diactoros\Response;
use Psr\Http\Message\UriInterface;
use function get_debug_type;
use function is_string;
use function sprintf;
/**
* Produce a redirect response.
*/
class RedirectResponse extends Response
{
/**
* Create a redirect response.
*
* Produces a redirect response with a Location header and the given status
* (302 by default).
*
* Note: this method overwrites the `location` $headers value.
*
* @param string|UriInterface $uri URI for the Location header.
* @param int $status Integer status code for the redirect; 302 by default.
* @param array<non-empty-string, string|string[]> $headers Array of headers to use at initialization.
*/
public function __construct($uri, int $status = 302, array $headers = [])
{
if (! is_string($uri) && ! $uri instanceof UriInterface) {
throw new Exception\InvalidArgumentException(sprintf(
'Uri provided to %s MUST be a string or Psr\Http\Message\UriInterface instance; received "%s"',
self::class,
get_debug_type($uri)
));
}
$headers['location'] = [(string) $uri];
parent::__construct('php://temp', $status, $headers);
}
}

View File

@@ -1,101 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Response;
use Laminas\Diactoros\AbstractSerializer;
use Laminas\Diactoros\Exception;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\Stream;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use function preg_match;
use function sprintf;
final class Serializer extends AbstractSerializer
{
/**
* Deserialize a response string to a response instance.
*
* @throws Exception\SerializationException When errors occur parsing the message.
*/
public static function fromString(string $message): Response
{
$stream = new Stream('php://temp', 'wb+');
$stream->write($message);
return static::fromStream($stream);
}
/**
* Parse a response from a stream.
*
* @throws Exception\InvalidArgumentException When the stream is not readable.
* @throws Exception\SerializationException When errors occur parsing the message.
*/
public static function fromStream(StreamInterface $stream): Response
{
if (! $stream->isReadable() || ! $stream->isSeekable()) {
throw new Exception\InvalidArgumentException('Message stream must be both readable and seekable');
}
$stream->rewind();
[$version, $status, $reasonPhrase] = self::getStatusLine($stream);
[$headers, $body] = self::splitStream($stream);
return (new Response($body, $status, $headers))
->withProtocolVersion($version)
->withStatus((int) $status, $reasonPhrase);
}
/**
* Create a string representation of a response.
*/
public static function toString(ResponseInterface $response): string
{
$reasonPhrase = $response->getReasonPhrase();
$headers = self::serializeHeaders($response->getHeaders());
$body = (string) $response->getBody();
$format = 'HTTP/%s %d%s%s%s';
if (! empty($headers)) {
$headers = "\r\n" . $headers;
}
$headers .= "\r\n\r\n";
return sprintf(
$format,
$response->getProtocolVersion(),
$response->getStatusCode(),
$reasonPhrase ? ' ' . $reasonPhrase : '',
$headers,
$body
);
}
/**
* Retrieve the status line for the message.
*
* @return array Array with three elements: 0 => version, 1 => status, 2 => reason
* @throws Exception\SerializationException If line is malformed.
*/
private static function getStatusLine(StreamInterface $stream): array
{
$line = self::getLine($stream);
if (
! preg_match(
'#^HTTP/(?P<version>[1-9]\d*\.\d) (?P<status>[1-5]\d{2})(\s+(?P<reason>.+))?$#',
$line,
$matches
)
) {
throw Exception\SerializationException::forInvalidStatusLine();
}
return [$matches['version'], (int) $matches['status'], $matches['reason'] ?? ''];
}
}

View File

@@ -1,73 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Response;
use Laminas\Diactoros\Exception;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\Stream;
use Psr\Http\Message\StreamInterface;
use function get_debug_type;
use function is_string;
use function sprintf;
/**
* Plain text response.
*
* Allows creating a response by passing a string to the constructor;
* by default, sets a status code of 200 and sets the Content-Type header to
* text/plain.
*/
class TextResponse extends Response
{
use InjectContentTypeTrait;
/**
* Create a plain text response.
*
* Produces a text response with a Content-Type of text/plain and a default
* status of 200.
*
* @param string|StreamInterface $text String or stream for the message body.
* @param int $status Integer status code for the response; 200 by default.
* @param array<non-empty-string, string|string[]> $headers Array of headers to use at initialization.
* @throws Exception\InvalidArgumentException If $text is neither a string or stream.
*/
public function __construct($text, int $status = 200, array $headers = [])
{
parent::__construct(
$this->createBody($text),
$status,
$this->injectContentType('text/plain; charset=utf-8', $headers)
);
}
/**
* Create the message body.
*
* @param string|StreamInterface $text
* @throws Exception\InvalidArgumentException If $text is neither a string or stream.
*/
private function createBody($text): StreamInterface
{
if ($text instanceof StreamInterface) {
return $text;
}
/** @psalm-suppress DocblockTypeContradiction */
if (! is_string($text)) {
throw new Exception\InvalidArgumentException(sprintf(
'Invalid content (%s) provided to %s',
get_debug_type($text),
self::class
));
}
$body = new Stream('php://temp', 'wb+');
$body->write($text);
$body->rewind();
return $body;
}
}

View File

@@ -1,75 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\Response;
use Laminas\Diactoros\Exception;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\Stream;
use Psr\Http\Message\StreamInterface;
use function get_debug_type;
use function is_string;
use function sprintf;
/**
* XML response.
*
* Allows creating a response by passing an XML string to the constructor; by default,
* sets a status code of 200 and sets the Content-Type header to application/xml.
*/
class XmlResponse extends Response
{
use InjectContentTypeTrait;
/**
* Create an XML response.
*
* Produces an XML response with a Content-Type of application/xml and a default
* status of 200.
*
* @param string|StreamInterface $xml String or stream for the message body.
* @param int $status Integer status code for the response; 200 by default.
* @param array<non-empty-string, string|string[]> $headers Array of headers to use at initialization.
* @throws Exception\InvalidArgumentException If $text is neither a string or stream.
*/
public function __construct(
$xml,
int $status = 200,
array $headers = []
) {
parent::__construct(
$this->createBody($xml),
$status,
$this->injectContentType('application/xml; charset=utf-8', $headers)
);
}
/**
* Create the message body.
*
* @param string|StreamInterface $xml
* @throws Exception\InvalidArgumentException If $xml is neither a string or stream.
*/
private function createBody($xml): StreamInterface
{
if ($xml instanceof StreamInterface) {
return $xml;
}
/** @psalm-suppress DocblockTypeContradiction */
if (! is_string($xml)) {
throw new Exception\InvalidArgumentException(sprintf(
'Invalid content (%s) provided to %s',
get_debug_type($xml),
self::class
));
}
$body = new Stream('php://temp', 'wb+');
$body->write($xml);
$body->rewind();
return $body;
}
}

View File

@@ -1,22 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Override;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
class ResponseFactory implements ResponseFactoryInterface
{
/**
* {@inheritDoc}
*/
#[Override]
public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface
{
return (new Response())
->withStatus($code, $reasonPhrase);
}
}

View File

@@ -1,238 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Override;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UploadedFileInterface;
use Psr\Http\Message\UriInterface;
use function array_key_exists;
use function gettype;
use function is_array;
use function is_object;
use function sprintf;
/**
* Server-side HTTP request
*
* Extends the Request definition to add methods for accessing incoming data,
* specifically server parameters, cookies, matched path parameters, query
* string arguments, body parameters, and upload file information.
*
* "Attributes" are discovered via decomposing the request (and usually
* specifically the URI path), and typically will be injected by the application.
*
* Requests are considered immutable; all methods that might change state are
* implemented such that they retain the internal state of the current
* message and return a new instance that contains the changed state.
*/
class ServerRequest implements ServerRequestInterface
{
use RequestTrait;
private array $attributes = [];
private array $uploadedFiles;
/**
* @param array $serverParams Server parameters, typically from $_SERVER
* @param array $uploadedFiles Upload file information, a tree of UploadedFiles
* @param null|string|UriInterface $uri URI for the request, if any.
* @param null|string $method HTTP method for the request, if any.
* @param string|resource|StreamInterface $body Message body, if any.
* @param array<non-empty-string, string|string[]> $headers Headers for the message, if any.
* @param array $cookieParams Cookies for the message, if any.
* @param array $queryParams Query params for the message, if any.
* @param null|array|object $parsedBody The deserialized body parameters, if any.
* @param string $protocol HTTP protocol version.
* @throws Exception\InvalidArgumentException For any invalid value.
*/
public function __construct(
private array $serverParams = [],
array $uploadedFiles = [],
null|string|UriInterface $uri = null,
?string $method = null,
$body = 'php://input',
array $headers = [],
private array $cookieParams = [],
private array $queryParams = [],
private $parsedBody = null,
string $protocol = '1.1'
) {
$this->validateUploadedFiles($uploadedFiles);
if ($body === 'php://input') {
$body = new Stream($body, 'r');
}
$this->initialize($uri, $method, $body, $headers);
$this->uploadedFiles = $uploadedFiles;
$this->protocol = $protocol;
}
/**
* {@inheritdoc}
*/
#[Override]
public function getServerParams(): array
{
return $this->serverParams;
}
/**
* {@inheritdoc}
*/
#[Override]
public function getUploadedFiles(): array
{
return $this->uploadedFiles;
}
/**
* {@inheritdoc}
*/
#[Override]
public function withUploadedFiles(array $uploadedFiles): ServerRequest
{
$this->validateUploadedFiles($uploadedFiles);
$new = clone $this;
$new->uploadedFiles = $uploadedFiles;
return $new;
}
/**
* {@inheritdoc}
*/
#[Override]
public function getCookieParams(): array
{
return $this->cookieParams;
}
/**
* {@inheritdoc}
*/
#[Override]
public function withCookieParams(array $cookies): ServerRequest
{
$new = clone $this;
$new->cookieParams = $cookies;
return $new;
}
/**
* {@inheritdoc}
*/
#[Override]
public function getQueryParams(): array
{
return $this->queryParams;
}
/**
* {@inheritdoc}
*/
#[Override]
public function withQueryParams(array $query): ServerRequest
{
$new = clone $this;
$new->queryParams = $query;
return $new;
}
/**
* {@inheritdoc}
*/
#[Override]
public function getParsedBody()
{
return $this->parsedBody;
}
/**
* {@inheritdoc}
*/
#[Override]
public function withParsedBody($data): ServerRequest
{
/** @psalm-suppress DocblockTypeContradiction */
if (! is_array($data) && ! is_object($data) && null !== $data) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects a null, array, or object argument; received %s',
__METHOD__,
gettype($data)
));
}
$new = clone $this;
$new->parsedBody = $data;
return $new;
}
/**
* {@inheritdoc}
*/
#[Override]
public function getAttributes(): array
{
return $this->attributes;
}
/**
* {@inheritdoc}
*/
#[Override]
public function getAttribute(string $name, $default = null)
{
if (! array_key_exists($name, $this->attributes)) {
return $default;
}
return $this->attributes[$name];
}
/**
* {@inheritdoc}
*/
#[Override]
public function withAttribute(string $name, $value): ServerRequest
{
$new = clone $this;
$new->attributes[$name] = $value;
return $new;
}
/**
* {@inheritdoc}
*/
#[Override]
public function withoutAttribute(string $name): ServerRequest
{
$new = clone $this;
unset($new->attributes[$name]);
return $new;
}
/**
* Recursively validate the structure in an uploaded files array.
*
* @throws Exception\InvalidArgumentException If any leaf is not an UploadedFileInterface instance.
*/
private function validateUploadedFiles(array $uploadedFiles): void
{
foreach ($uploadedFiles as $file) {
if (is_array($file)) {
$this->validateUploadedFiles($file);
continue;
}
if (! $file instanceof UploadedFileInterface) {
throw new Exception\InvalidArgumentException('Invalid leaf in uploaded files structure');
}
}
}
}

View File

@@ -1,101 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Laminas\Diactoros\ServerRequestFilter\FilterServerRequestInterface;
use Laminas\Diactoros\ServerRequestFilter\FilterUsingXForwardedHeaders;
use Override;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;
use function array_key_exists;
use function is_callable;
/**
* Class for marshaling a request object from the current PHP environment.
*/
class ServerRequestFactory implements ServerRequestFactoryInterface
{
/**
* Function to use to get apache request headers; present only to simplify mocking.
*
* @var callable|string
*/
private static $apacheRequestHeaders = 'apache_request_headers';
/**
* Create a request from the supplied superglobal values.
*
* If any argument is not supplied, the corresponding superglobal value will
* be used.
*
* The ServerRequest created is then passed to the fromServer() method in
* order to marshal the request URI and headers.
*
* @see fromServer()
*
* @param null|array $server $_SERVER superglobal
* @param null|array $query $_GET superglobal
* @param null|array $body $_POST superglobal
* @param null|array $cookies $_COOKIE superglobal
* @param null|array $files $_FILES superglobal
* @param null|FilterServerRequestInterface $requestFilter If present, the
* generated request will be passed to this instance and the result
* returned by this method. When not present, a default instance of
* FilterUsingXForwardedHeaders is created, using the `trustReservedSubnets()`
* constructor.
*/
public static function fromGlobals(
?array $server = null,
?array $query = null,
?array $body = null,
?array $cookies = null,
?array $files = null,
?FilterServerRequestInterface $requestFilter = null
): ServerRequestInterface {
$requestFilter ??= FilterUsingXForwardedHeaders::trustReservedSubnets();
$server = normalizeServer(
$server ?? $_SERVER,
is_callable(self::$apacheRequestHeaders) ? self::$apacheRequestHeaders : null
);
$files = normalizeUploadedFiles($files ?? $_FILES);
$headers = marshalHeadersFromSapi($server);
if (null === $cookies && array_key_exists('cookie', $headers)) {
$cookies = parseCookieHeader($headers['cookie']);
}
return $requestFilter(new ServerRequest(
$server,
$files,
UriFactory::createFromSapi($server, $headers),
marshalMethodFromSapi($server),
'php://input',
$headers,
$cookies ?? $_COOKIE,
$query ?? $_GET,
$body ?? $_POST,
marshalProtocolVersionFromSapi($server)
));
}
/**
* {@inheritDoc}
*/
#[Override]
public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
{
$uploadedFiles = [];
return new ServerRequest(
$serverParams,
$uploadedFiles,
$uri,
$method,
'php://temp'
);
}
}

View File

@@ -1,17 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\ServerRequestFilter;
use Override;
use Psr\Http\Message\ServerRequestInterface;
final class DoNotFilter implements FilterServerRequestInterface
{
#[Override]
public function __invoke(ServerRequestInterface $request): ServerRequestInterface
{
return $request;
}
}

View File

@@ -1,29 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\ServerRequestFilter;
use Psr\Http\Message\ServerRequestInterface;
/**
* Filter/initialize a server request.
*
* Implementations of this interface will take an incoming request, and
* decide if additional modifications are necessary. As examples:
*
* - Injecting a unique request identifier header.
* - Using the X-Forwarded-* headers to rewrite the URI to reflect the original request.
* - Using the Forwarded header to rewrite the URI to reflect the original request.
*
* This functionality is consumed by the ServerRequestFactory using the request
* instance it generates, just prior to returning a request.
*/
interface FilterServerRequestInterface
{
/**
* Determine if a request needs further modification, and if so, return a
* new instance reflecting those modifications.
*/
public function __invoke(ServerRequestInterface $request): ServerRequestInterface;
}

View File

@@ -1,266 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\ServerRequestFilter;
use Laminas\Diactoros\Exception\InvalidForwardedHeaderNameException;
use Laminas\Diactoros\Exception\InvalidProxyAddressException;
use Laminas\Diactoros\UriFactory;
use Override;
use Psr\Http\Message\ServerRequestInterface;
use function array_values;
use function assert;
use function count;
use function explode;
use function filter_var;
use function in_array;
use function is_string;
use function str_contains;
use function strtolower;
use const FILTER_FLAG_IPV4;
use const FILTER_FLAG_IPV6;
use const FILTER_VALIDATE_IP;
/**
* Modify the URI to reflect the X-Forwarded-* headers.
*
* If the request comes from a trusted proxy, this filter will analyze the
* various X-Forwarded-* headers, if any, and if they are marked as trusted,
* in order to return a new request that composes a URI instance that reflects
* those headers.
*/
final class FilterUsingXForwardedHeaders implements FilterServerRequestInterface
{
public const HEADER_HOST = 'X-FORWARDED-HOST';
public const HEADER_PORT = 'X-FORWARDED-PORT';
public const HEADER_PROTO = 'X-FORWARDED-PROTO';
private const X_FORWARDED_HEADERS = [
self::HEADER_HOST,
self::HEADER_PORT,
self::HEADER_PROTO,
];
/**
* Only allow construction via named constructors
*
* @param list<non-empty-string> $trustedProxies
* @param list<FilterUsingXForwardedHeaders::HEADER_*> $trustedHeaders
*/
private function __construct(
private readonly array $trustedProxies = [],
private readonly array $trustedHeaders = []
) {
}
#[Override]
public function __invoke(ServerRequestInterface $request): ServerRequestInterface
{
$remoteAddress = $request->getServerParams()['REMOTE_ADDR'] ?? '';
if ('' === $remoteAddress || ! is_string($remoteAddress)) {
// Should we trigger a warning here?
return $request;
}
if (! $this->isFromTrustedProxy($remoteAddress)) {
// Do nothing
return $request;
}
// Update the URI based on the trusted headers
$uri = $originalUri = $request->getUri();
foreach ($this->trustedHeaders as $headerName) {
$header = $request->getHeaderLine($headerName);
if ('' === $header || str_contains($header, ',')) {
// Reject empty headers and/or headers with multiple values
continue;
}
switch ($headerName) {
case self::HEADER_HOST:
[$host, $port] = UriFactory::marshalHostAndPortFromHeader($header);
$uri = $uri
->withHost($host);
if ($port !== null) {
$uri = $uri->withPort($port);
}
break;
case self::HEADER_PORT:
$uri = $uri->withPort((int) $header);
break;
case self::HEADER_PROTO:
$scheme = strtolower($header) === 'https' ? 'https' : 'http';
$uri = $uri->withScheme($scheme);
break;
}
}
if ($uri !== $originalUri) {
return $request->withUri($uri);
}
return $request;
}
/**
* Indicate which proxies and which X-Forwarded headers to trust.
*
* @param list<non-empty-string> $proxyCIDRList Each element may
* be an IP address or a subnet specified using CIDR notation; both IPv4
* and IPv6 are supported. The special string "*" will be translated to
* two entries, "0.0.0.0/0" and "::/0". An empty list indicates no
* proxies are trusted.
* @param list<FilterUsingXForwardedHeaders::HEADER_*> $trustedHeaders If
* the list is empty, all X-Forwarded headers are trusted.
* @throws InvalidProxyAddressException
* @throws InvalidForwardedHeaderNameException
*/
public static function trustProxies(
array $proxyCIDRList,
array $trustedHeaders = self::X_FORWARDED_HEADERS
): self {
$proxyCIDRList = self::normalizeProxiesList($proxyCIDRList);
self::validateTrustedHeaders($trustedHeaders);
return new self($proxyCIDRList, $trustedHeaders);
}
/**
* Trust any X-FORWARDED-* headers from any address.
*
* This is functionally equivalent to calling `trustProxies(['*'])`.
*
* WARNING: Only do this if you know for certain that your application
* sits behind a trusted proxy that cannot be spoofed. This should only
* be the case if your server is not publicly addressable, and all requests
* are routed via a reverse proxy (e.g., a load balancer, a server such as
* Caddy, when using Traefik, etc.).
*/
public static function trustAny(): self
{
return self::trustProxies(['*']);
}
/**
* Trust X-Forwarded headers from reserved subnetworks.
*
* This is functionally equivalent to calling `trustProxies()` where the
* `$proxcyCIDRList` argument is a list with the following:
*
* - 10.0.0.0/8
* - 127.0.0.0/8
* - 172.16.0.0/12
* - 192.168.0.0/16
* - ::1/128 (IPv6 localhost)
* - fc00::/7 (IPv6 private networks)
* - fe80::/10 (IPv6 local-link addresses)
*
* @param list<FilterUsingXForwardedHeaders::HEADER_*> $trustedHeaders If
* the list is empty, all X-Forwarded headers are trusted.
* @throws InvalidForwardedHeaderNameException
*/
public static function trustReservedSubnets(array $trustedHeaders = self::X_FORWARDED_HEADERS): self
{
return self::trustProxies([
'10.0.0.0/8',
'127.0.0.0/8',
'172.16.0.0/12',
'192.168.0.0/16',
'::1/128', // ipv6 localhost
'fc00::/7', // ipv6 private networks
'fe80::/10', // ipv6 local-link addresses
], $trustedHeaders);
}
private function isFromTrustedProxy(string $remoteAddress): bool
{
foreach ($this->trustedProxies as $proxy) {
if (IPRange::matches($remoteAddress, $proxy)) {
return true;
}
}
return false;
}
/** @throws InvalidForwardedHeaderNameException */
private static function validateTrustedHeaders(array $headers): void
{
foreach ($headers as $header) {
if (! in_array($header, self::X_FORWARDED_HEADERS, true)) {
throw InvalidForwardedHeaderNameException::forHeader($header);
}
}
}
/**
* @param list<non-empty-string> $proxyCIDRList
* @return list<non-empty-string>
* @throws InvalidProxyAddressException
*/
private static function normalizeProxiesList(array $proxyCIDRList): array
{
$foundWildcard = false;
foreach ($proxyCIDRList as $index => $cidr) {
if ($cidr === '*') {
unset($proxyCIDRList[$index]);
$foundWildcard = true;
continue;
}
if (! self::validateProxyCIDR($cidr)) {
throw InvalidProxyAddressException::forAddress($cidr);
}
}
if ($foundWildcard) {
$proxyCIDRList[] = '0.0.0.0/0';
$proxyCIDRList[] = '::/0';
}
return array_values($proxyCIDRList);
}
private static function validateProxyCIDR(mixed $cidr): bool
{
if (! is_string($cidr) || '' === $cidr) {
return false;
}
$address = $cidr;
$mask = null;
if (str_contains($cidr, '/')) {
$parts = explode('/', $cidr, 2);
assert(count($parts) >= 2);
[$address, $mask] = $parts;
$mask = (int) $mask;
}
if (str_contains($address, ':')) {
// is IPV6
return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
&& (
$mask === null
|| (
$mask <= 128
&& $mask >= 0
)
);
}
// is IPV4
return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)
&& (
$mask === null
|| (
$mask <= 32
&& $mask >= 0
)
);
}
}

View File

@@ -1,124 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros\ServerRequestFilter;
use function assert;
use function count;
use function explode;
use function inet_pton;
use function intval;
use function ip2long;
use function pack;
use function sprintf;
use function str_contains;
use function str_pad;
use function str_repeat;
use function substr_compare;
use function unpack;
/** @internal */
final class IPRange
{
/**
* Disable instantiation
*/
private function __construct()
{
}
/** @psalm-pure */
public static function matches(string $ip, string $cidr): bool
{
if (str_contains($ip, ':')) {
return self::matchesIPv6($ip, $cidr);
}
return self::matchesIPv4($ip, $cidr);
}
/** @psalm-pure */
public static function matchesIPv4(string $ip, string $cidr): bool
{
$mask = 32;
$subnet = $cidr;
if (str_contains($cidr, '/')) {
$parts = explode('/', $cidr, 2);
assert(count($parts) >= 2);
[$subnet, $mask] = $parts;
$mask = (int) $mask;
}
if ($mask < 0 || $mask > 32) {
return false;
}
$ip = ip2long($ip);
$subnet = ip2long($subnet);
if (false === $ip || false === $subnet) {
// Invalid data
return false;
}
return 0 === substr_compare(
sprintf("%032b", $ip),
sprintf("%032b", $subnet),
0,
$mask
);
}
/** @psalm-pure */
public static function matchesIPv6(string $ip, string $cidr): bool
{
$mask = 128;
$subnet = $cidr;
if (str_contains($cidr, '/')) {
$parts = explode('/', $cidr, 2);
assert(count($parts) >= 2);
[$subnet, $mask] = $parts;
$mask = (int) $mask;
}
if ($mask < 0 || $mask > 128) {
return false;
}
$ip = inet_pton($ip);
$subnet = inet_pton($subnet);
if (false === $ip || false === $subnet) {
// Invalid data
return false;
}
// mask 0: if it's a valid IP, it's valid
if ($mask === 0) {
return (bool) unpack('n*', $ip);
}
// @see http://stackoverflow.com/questions/7951061/matching-ipv6-address-to-a-cidr-subnet, MW answer
$binMask = str_repeat("f", intval($mask / 4));
switch ($mask % 4) {
case 0:
break;
case 1:
$binMask .= "8";
break;
case 2:
$binMask .= "c";
break;
case 3:
$binMask .= "e";
break;
}
$binMask = str_pad($binMask, 32, '0');
$binMask = pack("H*", $binMask);
return ($ip & $binMask) === $subnet;
}
}

View File

@@ -1,392 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Override;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
use Stringable;
use Throwable;
use function array_key_exists;
use function assert;
use function fclose;
use function feof;
use function fopen;
use function fread;
use function fseek;
use function fstat;
use function ftell;
use function fwrite;
use function get_resource_type;
use function in_array;
use function is_int;
use function is_resource;
use function is_string;
use function sprintf;
use function str_contains;
use function stream_get_contents;
use function stream_get_meta_data;
use const SEEK_SET;
/**
* Implementation of PSR HTTP streams
*/
class Stream implements StreamInterface, Stringable
{
/**
* A list of allowed stream resource types that are allowed to instantiate a Stream
*/
private const ALLOWED_STREAM_RESOURCE_TYPES = ['stream'];
/** @var resource|null */
protected $resource;
/** @var string|object|resource|null */
protected $stream;
/**
* @param string|object|resource $stream
* @param string $mode Mode with which to open stream
* @throws Exception\InvalidArgumentException
*/
public function __construct($stream, string $mode = 'r')
{
$this->setStream($stream, $mode);
}
/**
* {@inheritdoc}
*/
#[Override]
public function __toString(): string
{
if (! $this->isReadable()) {
return '';
}
try {
if ($this->isSeekable()) {
$this->rewind();
}
return $this->getContents();
} catch (RuntimeException) {
return '';
}
}
/**
* {@inheritdoc}
*/
#[Override]
public function close(): void
{
if (! $this->resource) {
return;
}
$resource = $this->detach();
assert(is_resource($resource), 'Always true condition for psalm type safety');
fclose($resource);
}
/**
* {@inheritdoc}
*/
#[Override]
public function detach()
{
$resource = $this->resource;
$this->resource = null;
return $resource;
}
/**
* Attach a new stream/resource to the instance.
*
* @param string|object|resource $resource
* @throws Exception\InvalidArgumentException For stream identifier that cannot be cast to a resource.
* @throws Exception\InvalidArgumentException For non-resource stream.
*/
public function attach($resource, string $mode = 'r'): void
{
$this->setStream($resource, $mode);
}
/**
* {@inheritdoc}
*/
#[Override]
public function getSize(): ?int
{
if (null === $this->resource) {
return null;
}
$stats = fstat($this->resource);
if ($stats !== false) {
return $stats['size'];
}
return null;
}
/**
* {@inheritdoc}
*/
#[Override]
public function tell(): int
{
if (! $this->resource) {
throw Exception\UntellableStreamException::dueToMissingResource();
}
$result = ftell($this->resource);
if (! is_int($result)) {
throw Exception\UntellableStreamException::dueToPhpError();
}
return $result;
}
/**
* {@inheritdoc}
*/
#[Override]
public function eof(): bool
{
if (! $this->resource) {
return true;
}
return feof($this->resource);
}
/**
* {@inheritdoc}
*/
#[Override]
public function isSeekable(): bool
{
if (! $this->resource) {
return false;
}
$meta = stream_get_meta_data($this->resource);
return $meta['seekable'];
}
/**
* {@inheritdoc}
*/
#[Override]
public function seek(int $offset, int $whence = SEEK_SET): void
{
if (! $this->resource) {
throw Exception\UnseekableStreamException::dueToMissingResource();
}
if (! $this->isSeekable()) {
throw Exception\UnseekableStreamException::dueToConfiguration();
}
$result = fseek($this->resource, $offset, $whence);
if (0 !== $result) {
throw Exception\UnseekableStreamException::dueToPhpError();
}
}
/**
* {@inheritdoc}
*/
#[Override]
public function rewind(): void
{
$this->seek(0);
}
/**
* {@inheritdoc}
*/
#[Override]
public function isWritable(): bool
{
if (! $this->resource) {
return false;
}
$meta = stream_get_meta_data($this->resource);
$mode = $meta['mode'];
return str_contains($mode, 'x')
|| str_contains($mode, 'w')
|| str_contains($mode, 'c')
|| str_contains($mode, 'a')
|| str_contains($mode, '+');
}
/**
* {@inheritdoc}
*/
#[Override]
public function write($string): int
{
if (! $this->resource) {
throw Exception\UnwritableStreamException::dueToMissingResource();
}
if (! $this->isWritable()) {
throw Exception\UnwritableStreamException::dueToConfiguration();
}
$result = fwrite($this->resource, $string);
if (false === $result) {
throw Exception\UnwritableStreamException::dueToPhpError();
}
return $result;
}
/**
* {@inheritdoc}
*/
#[Override]
public function isReadable(): bool
{
if (! $this->resource) {
return false;
}
$meta = stream_get_meta_data($this->resource);
$mode = $meta['mode'];
return str_contains($mode, 'r') || str_contains($mode, '+');
}
/**
* {@inheritdoc}
*/
#[Override]
public function read(int $length): string
{
if (! $this->resource) {
throw Exception\UnreadableStreamException::dueToMissingResource();
}
if (! $this->isReadable()) {
throw Exception\UnreadableStreamException::dueToConfiguration();
}
$result = fread($this->resource, $length);
if (false === $result) {
throw Exception\UnreadableStreamException::dueToPhpError();
}
return $result;
}
/**
* {@inheritdoc}
*/
#[Override]
public function getContents(): string
{
if (! $this->isReadable()) {
throw Exception\UnreadableStreamException::dueToConfiguration();
}
assert($this->resource !== null, 'Always true condition for psalm type safety');
$result = stream_get_contents($this->resource);
if (false === $result) {
throw Exception\UnreadableStreamException::dueToPhpError();
}
return $result;
}
/**
* {@inheritdoc}
*/
#[Override]
public function getMetadata(?string $key = null)
{
$metadata = [];
if (null !== $this->resource) {
$metadata = stream_get_meta_data($this->resource);
}
if (null === $key) {
return $metadata;
}
if (! array_key_exists($key, $metadata)) {
return null;
}
return $metadata[$key];
}
/**
* Set the internal stream resource.
*
* @param string|object|resource $stream String stream target or stream resource.
* @param string $mode Resource mode for stream target.
* @throws Exception\InvalidArgumentException For invalid streams or resources.
*/
private function setStream($stream, string $mode = 'r'): void
{
$error = null;
$resource = $stream;
if (is_string($stream)) {
try {
$resource = fopen($stream, $mode);
} catch (Throwable $error) {
}
if (! is_resource($resource)) {
throw new Exception\RuntimeException(
sprintf(
'Empty or non-existent stream identifier or file path provided: "%s"',
$stream,
),
0,
$error
);
}
}
if (! $this->isValidStreamResourceType($resource)) {
throw new Exception\InvalidArgumentException(
'Invalid stream provided; must be a string stream identifier or stream resource'
);
}
if ($stream !== $resource) {
$this->stream = $stream;
}
$this->resource = $resource;
}
/**
* Determine if a resource is one of the resource types allowed to instantiate a Stream
*
* @param mixed $resource Stream resource.
* @psalm-assert-if-true resource $resource
*/
private function isValidStreamResourceType(mixed $resource): bool
{
if (is_resource($resource)) {
return in_array(get_resource_type($resource), self::ALLOWED_STREAM_RESOURCE_TYPES, true);
}
return false;
}
}

View File

@@ -1,50 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Override;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\StreamInterface;
use function assert;
use function fopen;
use function fwrite;
use function is_resource;
use function rewind;
class StreamFactory implements StreamFactoryInterface
{
/**
* {@inheritDoc}
*/
#[Override]
public function createStream(string $content = ''): StreamInterface
{
$resource = fopen('php://temp', 'r+');
assert(is_resource($resource), 'Something is really wrong if PHP failed to open stream in memory');
fwrite($resource, $content);
rewind($resource);
return $this->createStreamFromResource($resource);
}
/**
* {@inheritDoc}
*/
#[Override]
public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface
{
return new Stream($filename, $mode);
}
/**
* {@inheritDoc}
*/
#[Override]
public function createStreamFromResource($resource): StreamInterface
{
return new Stream($resource);
}
}

View File

@@ -1,245 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Override;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UploadedFileInterface;
use function assert;
use function dirname;
use function fclose;
use function file_exists;
use function fopen;
use function fwrite;
use function is_dir;
use function is_resource;
use function is_string;
use function is_writable;
use function move_uploaded_file;
use function str_starts_with;
use function unlink;
use const PHP_SAPI;
use const UPLOAD_ERR_CANT_WRITE;
use const UPLOAD_ERR_EXTENSION;
use const UPLOAD_ERR_FORM_SIZE;
use const UPLOAD_ERR_INI_SIZE;
use const UPLOAD_ERR_NO_FILE;
use const UPLOAD_ERR_NO_TMP_DIR;
use const UPLOAD_ERR_OK;
use const UPLOAD_ERR_PARTIAL;
class UploadedFile implements UploadedFileInterface
{
public const ERROR_MESSAGES = [
UPLOAD_ERR_OK => 'There is no error, the file uploaded with success',
UPLOAD_ERR_INI_SIZE => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
UPLOAD_ERR_FORM_SIZE => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was '
. 'specified in the HTML form',
UPLOAD_ERR_PARTIAL => 'The uploaded file was only partially uploaded',
UPLOAD_ERR_NO_FILE => 'No file was uploaded',
UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder',
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the file upload.',
];
private readonly int $error;
private ?string $file = null;
private bool $moved = false;
private ?StreamInterface $stream = null;
/**
* @param string|resource|StreamInterface $streamOrFile
* @throws Exception\InvalidArgumentException
*/
public function __construct(
$streamOrFile,
private readonly ?int $size,
int $errorStatus,
private readonly ?string $clientFilename = null,
private readonly ?string $clientMediaType = null
) {
if ($errorStatus === UPLOAD_ERR_OK) {
if (is_string($streamOrFile)) {
$this->file = $streamOrFile;
}
if (is_resource($streamOrFile)) {
$this->stream = new Stream($streamOrFile);
}
if ($this->file === null && $this->stream === null) {
if (! $streamOrFile instanceof StreamInterface) {
throw new Exception\InvalidArgumentException('Invalid stream or file provided for UploadedFile');
}
$this->stream = $streamOrFile;
}
}
if (0 > $errorStatus || 8 < $errorStatus) {
throw new Exception\InvalidArgumentException(
'Invalid error status for UploadedFile; must be an UPLOAD_ERR_* constant'
);
}
$this->error = $errorStatus;
}
/**
* {@inheritdoc}
*
* @throws Exception\UploadedFileAlreadyMovedException If the upload was not successful.
*/
#[Override]
public function getStream(): StreamInterface
{
if ($this->error !== UPLOAD_ERR_OK) {
throw Exception\UploadedFileErrorException::dueToStreamUploadError(
self::ERROR_MESSAGES[$this->error]
);
}
if ($this->moved) {
throw new Exception\UploadedFileAlreadyMovedException();
}
if ($this->stream instanceof StreamInterface) {
return $this->stream;
}
assert($this->file !== null, 'Always true condition for psalm type safety');
$this->stream = new Stream($this->file);
return $this->stream;
}
/**
* {@inheritdoc}
*
* @see http://php.net/is_uploaded_file
* @see http://php.net/move_uploaded_file
*
* @param string $targetPath Path to which to move the uploaded file.
* @throws Exception\UploadedFileErrorException If the upload was not successful.
* @throws Exception\InvalidArgumentException If the $path specified is invalid.
* @throws Exception\UploadedFileErrorException On any error during the
* move operation, or on the second or subsequent call to the method.
*/
#[Override]
public function moveTo(string $targetPath): void
{
if ($this->moved) {
throw new Exception\UploadedFileAlreadyMovedException('Cannot move file; already moved!');
}
if ($this->error !== UPLOAD_ERR_OK) {
throw Exception\UploadedFileErrorException::dueToStreamUploadError(
self::ERROR_MESSAGES[$this->error]
);
}
if (empty($targetPath)) {
throw new Exception\InvalidArgumentException(
'Invalid path provided for move operation; must be a non-empty string'
);
}
$targetDirectory = dirname($targetPath);
if (! is_dir($targetDirectory) || ! is_writable($targetDirectory)) {
throw Exception\UploadedFileErrorException::dueToUnwritableTarget($targetDirectory);
}
$sapi = PHP_SAPI;
switch (true) {
case empty($sapi)
|| str_starts_with($sapi, 'cli')
|| str_starts_with($sapi, 'phpdbg')
|| $this->file === null:
// Non-SAPI environment, or no filename present
$this->writeFile($targetPath);
if ($this->stream instanceof StreamInterface) {
$this->stream->close();
}
if (is_string($this->file) && file_exists($this->file)) {
unlink($this->file);
}
break;
default:
// SAPI environment, with file present
if (false === move_uploaded_file($this->file, $targetPath)) {
throw Exception\UploadedFileErrorException::forUnmovableFile();
}
break;
}
$this->moved = true;
}
/**
* {@inheritdoc}
*
* @return int|null The file size in bytes or null if unknown.
*/
#[Override]
public function getSize(): ?int
{
return $this->size;
}
/**
* {@inheritdoc}
*
* @see http://php.net/manual/en/features.file-upload.errors.php
*
* @return int One of PHP's UPLOAD_ERR_XXX constants.
*/
#[Override]
public function getError(): int
{
return $this->error;
}
/**
* {@inheritdoc}
*
* @return string|null The filename sent by the client or null if none
* was provided.
*/
#[Override]
public function getClientFilename(): ?string
{
return $this->clientFilename;
}
/**
* {@inheritdoc}
*/
#[Override]
public function getClientMediaType(): ?string
{
return $this->clientMediaType;
}
/**
* Write internal stream to given path
*/
private function writeFile(string $path): void
{
$handle = fopen($path, 'wb+');
if (false === $handle) {
throw Exception\UploadedFileErrorException::dueToUnwritablePath();
}
$stream = $this->getStream();
$stream->rewind();
while (! $stream->eof()) {
fwrite($handle, $stream->read(4096));
}
fclose($handle);
}
}

View File

@@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Override;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UploadedFileFactoryInterface;
use Psr\Http\Message\UploadedFileInterface;
use const UPLOAD_ERR_OK;
class UploadedFileFactory implements UploadedFileFactoryInterface
{
/**
* {@inheritDoc}
*/
#[Override]
public function createUploadedFile(
StreamInterface $stream,
?int $size = null,
int $error = UPLOAD_ERR_OK,
?string $clientFilename = null,
?string $clientMediaType = null
): UploadedFileInterface {
if ($size === null) {
$size = $stream->getSize();
}
return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType);
}
}

View File

@@ -1,645 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Override;
use Psr\Http\Message\UriInterface;
use SensitiveParameter;
use Stringable;
use function array_keys;
use function assert;
use function explode;
use function implode;
use function is_string;
use function ltrim;
use function parse_url;
use function preg_match;
use function preg_replace;
use function preg_replace_callback;
use function rawurlencode;
use function sprintf;
use function str_contains;
use function str_split;
use function str_starts_with;
use function strtolower;
use function substr;
/**
* Implementation of Psr\Http\UriInterface.
*
* Provides a value object representing a URI for HTTP requests.
*
* Instances of this class are considered immutable; all methods that
* might change state are implemented such that they retain the internal
* state of the current instance and return a new instance that contains the
* changed state.
*
* @psalm-immutable
*/
class Uri implements UriInterface, Stringable
{
/**
* Sub-delimiters used in user info, query strings and fragments.
*
* @const string
*/
public const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
/**
* Unreserved characters used in user info, paths, query strings, and fragments.
*
* @const string
*/
public const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~\pL';
/** @var int[] Array indexed by valid scheme names to their corresponding ports. */
protected $allowedSchemes = [
'http' => 80,
'https' => 443,
];
private string $scheme = '';
private string $userInfo = '';
private string $host = '';
private ?int $port = null;
private string $path = '';
private string $query = '';
private string $fragment = '';
/**
* generated uri string cache
*/
private ?string $uriString = null;
public function __construct(string $uri = '')
{
if ('' === $uri) {
return;
}
/** @psalm-suppress UnusedMethodCall Called method is not mutation free. Psalm has no impure annotation */
$this->parseUri($uri);
}
/**
* Operations to perform on clone.
*
* Since cloning usually is for purposes of mutation, we reset the
* $uriString property so it will be re-calculated.
*/
public function __clone()
{
$this->uriString = null;
}
/**
* {@inheritdoc}
*/
#[Override]
public function __toString(): string
{
if (null !== $this->uriString) {
return $this->uriString;
}
/** @psalm-suppress ImpureMethodCall, InaccessibleProperty */
$this->uriString = static::createUriString(
$this->scheme,
$this->getAuthority(),
$this->path, // Absolute URIs should use a "/" for an empty path
$this->query,
$this->fragment
);
return $this->uriString;
}
/**
* {@inheritdoc}
*/
#[Override]
public function getScheme(): string
{
return $this->scheme;
}
/**
* {@inheritdoc}
*/
#[Override]
public function getAuthority(): string
{
if ('' === $this->host) {
return '';
}
$authority = $this->host;
if ('' !== $this->userInfo) {
$authority = $this->userInfo . '@' . $authority;
}
if ($this->isNonStandardPort($this->scheme, $this->host, $this->port)) {
$authority .= ':' . $this->port;
}
return $authority;
}
/**
* Retrieve the user-info part of the URI.
*
* This value is percent-encoded, per RFC 3986 Section 3.2.1.
*
* {@inheritdoc}
*/
#[Override]
public function getUserInfo(): string
{
return $this->userInfo;
}
/**
* {@inheritdoc}
*/
#[Override]
public function getHost(): string
{
return $this->host;
}
/**
* {@inheritdoc}
*/
#[Override]
public function getPort(): ?int
{
return $this->isNonStandardPort($this->scheme, $this->host, $this->port)
? $this->port
: null;
}
/**
* {@inheritdoc}
*/
#[Override]
public function getPath(): string
{
if ('' === $this->path) {
// No path
return $this->path;
}
if ($this->path[0] !== '/') {
// Relative path
return $this->path;
}
// Ensure only one leading slash, to prevent XSS attempts.
return '/' . ltrim($this->path, '/');
}
/**
* {@inheritdoc}
*/
#[Override]
public function getQuery(): string
{
return $this->query;
}
/**
* {@inheritdoc}
*/
#[Override]
public function getFragment(): string
{
return $this->fragment;
}
/**
* {@inheritdoc}
*/
#[Override]
public function withScheme(string $scheme): UriInterface
{
$scheme = $this->filterScheme($scheme);
if ($scheme === $this->scheme) {
// Do nothing if no change was made.
return $this;
}
$new = clone $this;
$new->scheme = $scheme;
return $new;
}
// The following rule is buggy for parameters attributes
// phpcs:disable SlevomatCodingStandard.TypeHints.ParameterTypeHintSpacing.NoSpaceBetweenTypeHintAndParameter
/**
* Create and return a new instance containing the provided user credentials.
*
* The value will be percent-encoded in the new instance, but with measures
* taken to prevent double-encoding.
*
* {@inheritdoc}
*/
#[Override]
public function withUserInfo(
string $user,
#[SensitiveParameter]
?string $password = null
): UriInterface {
$info = $this->filterUserInfoPart($user);
if (null !== $password) {
$info .= ':' . $this->filterUserInfoPart($password);
}
if ($info === $this->userInfo) {
// Do nothing if no change was made.
return $this;
}
$new = clone $this;
$new->userInfo = $info;
return $new;
}
// phpcs:enable SlevomatCodingStandard.TypeHints.ParameterTypeHintSpacing.NoSpaceBetweenTypeHintAndParameter
/**
* {@inheritdoc}
*/
#[Override]
public function withHost(string $host): UriInterface
{
if ($host === $this->host) {
// Do nothing if no change was made.
return $this;
}
$new = clone $this;
$new->host = strtolower($host);
return $new;
}
/**
* {@inheritdoc}
*/
#[Override]
public function withPort(?int $port): UriInterface
{
if ($port === $this->port) {
// Do nothing if no change was made.
return $this;
}
if ($port !== null && ($port < 1 || $port > 65535)) {
throw new Exception\InvalidArgumentException(sprintf(
'Invalid port "%d" specified; must be a valid TCP/UDP port',
$port
));
}
$new = clone $this;
$new->port = $port;
return $new;
}
/**
* {@inheritdoc}
*/
#[Override]
public function withPath(string $path): UriInterface
{
if (str_contains($path, '?')) {
throw new Exception\InvalidArgumentException(
'Invalid path provided; must not contain a query string'
);
}
if (str_contains($path, '#')) {
throw new Exception\InvalidArgumentException(
'Invalid path provided; must not contain a URI fragment'
);
}
$path = $this->filterPath($path);
if ($path === $this->path) {
// Do nothing if no change was made.
return $this;
}
$new = clone $this;
$new->path = $path;
return $new;
}
/**
* {@inheritdoc}
*/
#[Override]
public function withQuery(string $query): UriInterface
{
if (str_contains($query, '#')) {
throw new Exception\InvalidArgumentException(
'Query string must not include a URI fragment'
);
}
$query = $this->filterQuery($query);
if ($query === $this->query) {
// Do nothing if no change was made.
return $this;
}
$new = clone $this;
$new->query = $query;
return $new;
}
/**
* {@inheritdoc}
*/
#[Override]
public function withFragment(string $fragment): UriInterface
{
$fragment = $this->filterFragment($fragment);
if ($fragment === $this->fragment) {
// Do nothing if no change was made.
return $this;
}
$new = clone $this;
$new->fragment = $fragment;
return $new;
}
/**
* Parse a URI into its parts, and set the properties
*
* @psalm-suppress InaccessibleProperty Method is only called in {@see Uri::__construct} and thus immutability is
* still given.
*/
private function parseUri(string $uri): void
{
$parts = parse_url($uri);
if (false === $parts) {
throw new Exception\InvalidArgumentException(
'The source URI string appears to be malformed'
);
}
$this->scheme = isset($parts['scheme']) ? $this->filterScheme($parts['scheme']) : '';
$this->userInfo = isset($parts['user']) ? $this->filterUserInfoPart($parts['user']) : '';
$this->host = isset($parts['host']) ? strtolower($parts['host']) : '';
$this->port = $parts['port'] ?? null;
$this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : '';
$this->query = isset($parts['query']) ? $this->filterQuery($parts['query']) : '';
$this->fragment = isset($parts['fragment']) ? $this->filterFragment($parts['fragment']) : '';
if (isset($parts['pass'])) {
$this->userInfo .= ':' . $parts['pass'];
}
}
/**
* Create a URI string from its various parts
*/
private static function createUriString(
string $scheme,
string $authority,
string $path,
string $query,
string $fragment
): string {
$uri = '';
if ('' !== $scheme) {
$uri .= sprintf('%s:', $scheme);
}
if ('' !== $authority) {
$uri .= '//' . $authority;
}
if ('' !== $path && ! str_starts_with($path, '/')) {
$path = '/' . $path;
}
$uri .= $path;
if ('' !== $query) {
$uri .= sprintf('?%s', $query);
}
if ('' !== $fragment) {
$uri .= sprintf('#%s', $fragment);
}
return $uri;
}
/**
* Is a given port non-standard for the current scheme?
*
* @psalm-assert-if-true int $port
*/
private function isNonStandardPort(string $scheme, string $host, ?int $port): bool
{
if ('' === $scheme) {
return '' === $host || null !== $port;
}
if ('' === $host || null === $port) {
return false;
}
return ! isset($this->allowedSchemes[$scheme]) || $port !== $this->allowedSchemes[$scheme];
}
/**
* Filters the scheme to ensure it is a valid scheme.
*
* @param string $scheme Scheme name.
* @return string Filtered scheme.
*/
private function filterScheme(string $scheme): string
{
$scheme = strtolower($scheme);
$scheme = preg_replace('#:(//)?$#', '', $scheme);
assert(is_string($scheme));
if ('' === $scheme) {
return '';
}
if (! isset($this->allowedSchemes[$scheme])) {
throw new Exception\InvalidArgumentException(sprintf(
'Unsupported scheme "%s"; must be any empty string or in the set (%s)',
$scheme,
implode(', ', array_keys($this->allowedSchemes))
));
}
return $scheme;
}
/**
* Filters a part of user info in a URI to ensure it is properly encoded.
*/
private function filterUserInfoPart(string $part): string
{
$part = $this->filterInvalidUtf8($part);
/**
* Note the addition of `%` to initial charset; this allows `|` portion
* to match and thus prevent double-encoding.
*/
$result = preg_replace_callback(
'/(?:[^%' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . ']+|%(?![A-Fa-f0-9]{2}))/u',
[$this, 'urlEncodeChar'],
$part
);
assert($result !== null, 'Always true condition for psalm type safety');
return $result;
}
/**
* Filters the path of a URI to ensure it is properly encoded.
*/
private function filterPath(string $path): string
{
$path = $this->filterInvalidUtf8($path);
$result = preg_replace_callback(
'/(?:[^' . self::CHAR_UNRESERVED . ')(:@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/u',
[$this, 'urlEncodeChar'],
$path
);
assert($result !== null, 'Always true condition for psalm type safety');
return $result;
}
/**
* Encode invalid UTF-8 characters in given string. All other characters are unchanged.
*/
private function filterInvalidUtf8(string $string): string
{
// check if given string contains only valid UTF-8 characters
if (preg_match('//u', $string)) {
return $string;
}
$letters = str_split($string);
foreach ($letters as $i => $letter) {
if (! preg_match('//u', $letter)) {
$letters[$i] = $this->urlEncodeChar([$letter]);
}
}
return implode('', $letters);
}
/**
* Filter a query string to ensure it is propertly encoded.
*
* Ensures that the values in the query string are properly urlencoded.
*/
private function filterQuery(string $query): string
{
if ('' !== $query && str_starts_with($query, '?')) {
$query = substr($query, 1);
}
$parts = explode('&', $query);
foreach ($parts as $index => $part) {
[$key, $value] = $this->splitQueryValue($part);
if ($value === null) {
$parts[$index] = $this->filterQueryOrFragment($key);
continue;
}
$parts[$index] = sprintf(
'%s=%s',
$this->filterQueryOrFragment($key),
$this->filterQueryOrFragment($value)
);
}
return implode('&', $parts);
}
/**
* Split a query value into a key/value tuple.
*
* @return array{0:string, 1:string|null} A value with exactly two elements, key and value
*/
private function splitQueryValue(string $value): array
{
$data = explode('=', $value, 2);
if (! isset($data[1])) {
$data[1] = null;
}
return $data;
}
/**
* Filter a fragment value to ensure it is properly encoded.
*/
private function filterFragment(string $fragment): string
{
if ('' !== $fragment && str_starts_with($fragment, '#')) {
$fragment = '%23' . substr($fragment, 1);
}
return $this->filterQueryOrFragment($fragment);
}
/**
* Filter a query string key or value, or a fragment.
*/
private function filterQueryOrFragment(string $value): string
{
$value = $this->filterInvalidUtf8($value);
$result = preg_replace_callback(
'/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/u',
[$this, 'urlEncodeChar'],
$value
);
assert($result !== null, 'Always true condition for psalm type safety');
return $result;
}
/**
* URL encode a character returned by a regex.
*
* @param array<string> $matches
* @psalm-pure
*/
private function urlEncodeChar(array $matches): string
{
return rawurlencode($matches[0]);
}
}

View File

@@ -1,256 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Override;
use Psr\Http\Message\UriFactoryInterface;
use Psr\Http\Message\UriInterface;
use function array_change_key_case;
use function array_key_exists;
use function assert;
use function count;
use function explode;
use function gettype;
use function implode;
use function is_bool;
use function is_scalar;
use function is_string;
use function ltrim;
use function preg_match;
use function preg_replace;
use function sprintf;
use function str_contains;
use function strlen;
use function strrpos;
use function strtolower;
use function substr;
use const CASE_LOWER;
class UriFactory implements UriFactoryInterface
{
/**
* {@inheritDoc}
*/
#[Override]
public function createUri(string $uri = ''): UriInterface
{
return new Uri($uri);
}
/**
* Create a Uri instance based on the headers and $_SERVER data.
*
* @param array<non-empty-string, list<string>|int|float|string> $server SAPI parameters
* @param array<string, string|list<string>> $headers
*/
public static function createFromSapi(array $server, array $headers): Uri
{
$uri = new Uri('');
$isHttps = false;
if (array_key_exists('HTTPS', $server)) {
$isHttps = self::marshalHttpsValue($server['HTTPS']);
} elseif (array_key_exists('https', $server)) {
$isHttps = self::marshalHttpsValue($server['https']);
}
$uri = $uri->withScheme($isHttps ? 'https' : 'http');
[$host, $port] = self::marshalHostAndPort($server, $headers);
if (! empty($host)) {
$uri = $uri->withHost($host);
if ($port !== null) {
$uri = $uri->withPort($port);
}
}
$path = self::marshalRequestPath($server);
// Strip query string
$path = explode('?', $path, 2)[0];
$query = '';
if (isset($server['QUERY_STRING']) && is_scalar($server['QUERY_STRING'])) {
$query = ltrim((string) $server['QUERY_STRING'], '?');
}
$fragment = '';
if (str_contains($path, '#')) {
$parts = explode('#', $path, 2);
assert(count($parts) >= 2);
[$path, $fragment] = $parts;
}
return $uri
->withPath($path)
->withFragment($fragment)
->withQuery($query);
}
/**
* Retrieve a header value from an array of headers using a case-insensitive lookup.
*
* @template T
* @param array<string, string|list<string>> $headers Key/value header pairs
* @param T $default Default value to return if header not found
* @return string|T
*/
private static function getHeaderFromArray(string $name, array $headers, $default = null)
{
$header = strtolower($name);
$headers = array_change_key_case($headers, CASE_LOWER);
if (! array_key_exists($header, $headers)) {
return $default;
}
if (is_string($headers[$header])) {
return $headers[$header];
}
return implode(', ', $headers[$header]);
}
/**
* Marshal the host and port from the PHP environment.
*
* @param array<string, string|list<string>> $headers
* @return array{0:string, 1:int|null} Array of two items, host and port,
* in that order (can be passed to a list() operation).
*/
private static function marshalHostAndPort(array $server, array $headers): array
{
/** @var array{string, null} $defaults */
static $defaults = ['', null];
$host = self::getHeaderFromArray('host', $headers, false);
if ($host !== false) {
// Ignore obviously malformed host headers:
// - Whitespace is invalid within a hostname and break the URI representation within HTTP.
// non-printable characters other than SPACE and TAB are already rejected by HeaderSecurity.
// - A comma indicates that multiple host headers have been sent which is not legal
// and might be used in an attack where a load balancer sees a different host header
// than Diactoros.
if (! preg_match('/[\\t ,]/', $host)) {
return self::marshalHostAndPortFromHeader($host);
}
}
if (! isset($server['SERVER_NAME'])) {
return $defaults;
}
$host = (string) $server['SERVER_NAME'];
$port = isset($server['SERVER_PORT']) ? (int) $server['SERVER_PORT'] : null;
if (
! isset($server['SERVER_ADDR'])
|| ! preg_match('/^\[[0-9a-fA-F\:]+\]$/', $host)
) {
return [$host, $port];
}
// Misinterpreted IPv6-Address
// Reported for Safari on Windows
return self::marshalIpv6HostAndPort($server, $port);
}
/**
* @return array{string, int|null} Array of two items, host and port,
* in that order (can be passed to a list() operation).
*/
private static function marshalIpv6HostAndPort(array $server, ?int $port): array
{
$host = '[' . (string) $server['SERVER_ADDR'] . ']';
$port ??= 80;
$portSeparatorPos = strrpos($host, ':');
if (false === $portSeparatorPos) {
return [$host, $port];
}
if ($port . ']' === substr($host, $portSeparatorPos + 1)) {
// The last digit of the IPv6-Address has been taken as port
// Unset the port so the default port can be used
$port = null;
}
return [$host, $port];
}
/**
* Detect the path for the request
*
* Looks at a variety of criteria in order to attempt to autodetect the base
* request path, including:
*
* - IIS7 UrlRewrite environment
* - REQUEST_URI
* - ORIG_PATH_INFO
*/
private static function marshalRequestPath(array $server): string
{
// IIS7 with URL Rewrite: make sure we get the unencoded url
// (double slash problem).
/** @var string|array<string>|null $iisUrlRewritten */
$iisUrlRewritten = $server['IIS_WasUrlRewritten'] ?? null;
/** @var string|array<string> $unencodedUrl */
$unencodedUrl = $server['UNENCODED_URL'] ?? '';
if ('1' === $iisUrlRewritten && is_string($unencodedUrl) && '' !== $unencodedUrl) {
return $unencodedUrl;
}
/** @var string|array<string>|null $requestUri */
$requestUri = $server['REQUEST_URI'] ?? null;
if (is_string($requestUri)) {
$result = preg_replace('#^[^/:]+://[^/]+#', '', $requestUri);
assert($result !== null, 'Always true condition for psalm type safety');
return $result;
}
$origPathInfo = $server['ORIG_PATH_INFO'] ?? '';
if (! is_string($origPathInfo) || '' === $origPathInfo) {
return '/';
}
return $origPathInfo;
}
private static function marshalHttpsValue(mixed $https): bool
{
if (is_bool($https)) {
return $https;
}
if (! is_string($https)) {
throw new Exception\InvalidArgumentException(sprintf(
'SAPI HTTPS value MUST be a string or boolean; received %s',
gettype($https)
));
}
return 'on' === strtolower($https);
}
/**
* @internal
*
* @return array{string, int|null} Array of two items, host and port, in that order (can be
* passed to a list() operation).
* @psalm-mutation-free
*/
public static function marshalHostAndPortFromHeader(string $host): array
{
$port = null;
// works for regname, IPv4 & IPv6
if (preg_match('|\:(\d+)$|', $host, $matches)) {
$host = substr($host, 0, -1 * (strlen($matches[1]) + 1));
$port = (int) $matches[1];
}
return [$host, $port];
}
}

View File

@@ -1,37 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use function sprintf;
/**
* Create an uploaded file instance from an array of values.
*
* @param array $spec A single $_FILES entry.
* @throws Exception\InvalidArgumentException If one or more of the tmp_name,
* size, or error keys are missing from $spec.
*/
function createUploadedFile(array $spec): UploadedFile
{
if (
! isset($spec['tmp_name'])
|| ! isset($spec['size'])
|| ! isset($spec['error'])
) {
throw new Exception\InvalidArgumentException(sprintf(
'$spec provided to %s MUST contain each of the keys "tmp_name",'
. ' "size", and "error"; one or more were missing',
__FUNCTION__
));
}
return new UploadedFile(
$spec['tmp_name'],
(int) $spec['size'],
$spec['error'],
$spec['name'] ?? null,
$spec['type'] ?? null
);
}

View File

@@ -1,74 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use function array_filter;
use function array_key_exists;
use function is_string;
use function str_starts_with;
use function strtolower;
use function strtr;
use function substr;
use const ARRAY_FILTER_USE_KEY;
/**
* @param array $server Values obtained from the SAPI (generally `$_SERVER`).
* @return array<non-empty-string, mixed> Header/value pairs
*/
function marshalHeadersFromSapi(array $server): array
{
$contentHeaderLookup = isset($server['LAMINAS_DIACTOROS_STRICT_CONTENT_HEADER_LOOKUP'])
? static function (string $key): bool {
static $contentHeaders = [
'CONTENT_TYPE' => true,
'CONTENT_LENGTH' => true,
'CONTENT_MD5' => true,
];
return isset($contentHeaders[$key]);
}
: static fn(string $key): bool => str_starts_with($key, 'CONTENT_');
$headers = [];
foreach ($server as $key => $value) {
if (! is_string($key) || $key === '') {
continue;
}
if ($value === '') {
continue;
}
// Apache prefixes environment variables with REDIRECT_
// if they are added by rewrite rules
if (str_starts_with($key, 'REDIRECT_')) {
$key = substr($key, 9);
// We will not overwrite existing variables with the
// prefixed versions, though
if (array_key_exists($key, $server)) {
continue;
}
}
if (str_starts_with($key, 'HTTP_')) {
$name = strtr(strtolower(substr($key, 5)), '_', '-');
$headers[$name] = $value;
continue;
}
if ($contentHeaderLookup($key)) {
$name = strtr(strtolower($key), '_', '-');
$headers[$name] = $value;
continue;
}
}
// Filter out integer keys.
// These can occur if the translated header name is a string integer.
// PHP will cast those to integers when assigned to an array.
// This filters them out.
return array_filter($headers, fn(string|int $key): bool => is_string($key), ARRAY_FILTER_USE_KEY);
}

View File

@@ -1,13 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
/**
* Retrieve the request method from the SAPI parameters.
*/
function marshalMethodFromSapi(array $server): string
{
return $server['REQUEST_METHOD'] ?? 'GET';
}

View File

@@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use function preg_match;
/**
* Return HTTP protocol version (X.Y) as discovered within a `$_SERVER` array.
*
* @throws Exception\UnrecognizedProtocolVersionException If the
* $server['SERVER_PROTOCOL'] value is malformed.
*/
function marshalProtocolVersionFromSapi(array $server): string
{
if (! isset($server['SERVER_PROTOCOL'])) {
return '1.1';
}
if (! preg_match('#^(HTTP/)?(?P<version>[1-9]\d*(?:\.\d)?)$#', $server['SERVER_PROTOCOL'], $matches)) {
throw Exception\UnrecognizedProtocolVersionException::forVersion(
(string) $server['SERVER_PROTOCOL']
);
}
return $matches['version'];
}

View File

@@ -1,48 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use function is_callable;
/**
* Marshal the $_SERVER array
*
* Pre-processes and returns the $_SERVER superglobal. In particularly, it
* attempts to detect the Authorization header, which is often not aggregated
* correctly under various SAPI/httpd combinations.
*
* @param null|callable $apacheRequestHeaderCallback Callback that can be used to
* retrieve Apache request headers. This defaults to
* `apache_request_headers` under the Apache mod_php.
* @return array Either $server verbatim, or with an added HTTP_AUTHORIZATION header.
*/
function normalizeServer(array $server, ?callable $apacheRequestHeaderCallback = null): array
{
if (null === $apacheRequestHeaderCallback && is_callable('apache_request_headers')) {
$apacheRequestHeaderCallback = 'apache_request_headers';
}
// If the HTTP_AUTHORIZATION value is already set, or the callback is not
// callable, we return verbatim
if (
isset($server['HTTP_AUTHORIZATION'])
|| ! is_callable($apacheRequestHeaderCallback)
) {
return $server;
}
$apacheRequestHeaders = $apacheRequestHeaderCallback();
if (isset($apacheRequestHeaders['Authorization'])) {
$server['HTTP_AUTHORIZATION'] = $apacheRequestHeaders['Authorization'];
return $server;
}
if (isset($apacheRequestHeaders['authorization'])) {
$server['HTTP_AUTHORIZATION'] = $apacheRequestHeaders['authorization'];
return $server;
}
return $server;
}

View File

@@ -1,126 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use Psr\Http\Message\UploadedFileInterface;
use function is_array;
use function sprintf;
/**
* Normalize uploaded files
*
* Transforms each value into an UploadedFile instance, and ensures that nested
* arrays are normalized.
*
* @return UploadedFileInterface[]
* @throws Exception\InvalidArgumentException For unrecognized values.
*/
function normalizeUploadedFiles(array $files): array
{
/**
* Traverse a nested tree of uploaded file specifications.
*
* @param string[]|array[] $tmpNameTree
* @param int[]|array[] $sizeTree
* @param int[]|array[] $errorTree
* @param string[]|array[]|null $nameTree
* @param string[]|array[]|null $typeTree
* @return UploadedFile[]|array[]
*/
$recursiveNormalize = static function (
array $tmpNameTree,
array $sizeTree,
array $errorTree,
?array $nameTree = null,
?array $typeTree = null
) use (&$recursiveNormalize): array {
$normalized = [];
foreach ($tmpNameTree as $key => $value) {
if (is_array($value)) {
// Traverse
$normalized[$key] = $recursiveNormalize(
$tmpNameTree[$key],
$sizeTree[$key],
$errorTree[$key],
$nameTree[$key] ?? null,
$typeTree[$key] ?? null
);
continue;
}
$normalized[$key] = createUploadedFile([
'tmp_name' => $tmpNameTree[$key],
'size' => $sizeTree[$key],
'error' => $errorTree[$key],
'name' => $nameTree[$key] ?? null,
'type' => $typeTree[$key] ?? null,
]);
}
return $normalized;
};
/**
* Normalize an array of file specifications.
*
* Loops through all nested files (as determined by receiving an array to the
* `tmp_name` key of a `$_FILES` specification) and returns a normalized array
* of UploadedFile instances.
*
* This function normalizes a `$_FILES` array representing a nested set of
* uploaded files as produced by the php-fpm SAPI, CGI SAPI, or mod_php
* SAPI.
*
* @param array $files
* @return UploadedFile[]
*/
$normalizeUploadedFileSpecification = static function (array $files = []) use (&$recursiveNormalize): array {
if (
! isset($files['tmp_name']) || ! is_array($files['tmp_name'])
|| ! isset($files['size']) || ! is_array($files['size'])
|| ! isset($files['error']) || ! is_array($files['error'])
) {
throw new Exception\InvalidArgumentException(sprintf(
'$files provided to %s MUST contain each of the keys "tmp_name",'
. ' "size", and "error", with each represented as an array;'
. ' one or more were missing or non-array values',
__FUNCTION__
));
}
return $recursiveNormalize(
$files['tmp_name'],
$files['size'],
$files['error'],
$files['name'] ?? null,
$files['type'] ?? null
);
};
$normalized = [];
foreach ($files as $key => $value) {
if ($value instanceof UploadedFileInterface) {
$normalized[$key] = $value;
continue;
}
if (is_array($value) && isset($value['tmp_name']) && is_array($value['tmp_name'])) {
$normalized[$key] = $normalizeUploadedFileSpecification($value);
continue;
}
if (is_array($value) && isset($value['tmp_name'])) {
$normalized[$key] = createUploadedFile($value);
continue;
}
if (is_array($value)) {
$normalized[$key] = normalizeUploadedFiles($value);
continue;
}
throw new Exception\InvalidArgumentException('Invalid value in files specification');
}
return $normalized;
}

View File

@@ -1,40 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\Diactoros;
use function preg_match_all;
use function rawurldecode;
use const PREG_SET_ORDER;
/**
* Parse a cookie header according to RFC 6265.
*
* PHP will replace special characters in cookie names, which results in other cookies not being available due to
* overwriting. Thus, the server request should take the cookies from the request header instead.
*
* @param string $cookieHeader A string cookie header value.
* @return array<non-empty-string, string> key/value cookie pairs.
*/
function parseCookieHeader($cookieHeader): array
{
preg_match_all('(
(?:^\\n?[ \t]*|;[ ])
(?P<name>[!#$%&\'*+-.0-9A-Z^_`a-z|~]+)
=
(?P<DQUOTE>"?)
(?P<value>[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*)
(?P=DQUOTE)
(?=\\n?[ \t]*$|;[ ])
)x', $cookieHeader, $matches, PREG_SET_ORDER);
$cookies = [];
foreach ($matches as $match) {
$cookies[$match['name']] = rawurldecode($match['value']);
}
return $cookies;
}

View File

@@ -1 +0,0 @@
Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. (https://getlaminas.org/)

View File

@@ -1,26 +0,0 @@
Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
- Neither the name of Laminas Foundation nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,26 +0,0 @@
# laminas-loader
> [!CAUTION]
> This package is **abandoned** and will receive no further development.
>
> See the Technical Steering Committee [meeting minutes](https://github.com/laminas/technical-steering-committee/blob/main/meetings/minutes/2024-11-04-TSC-Minutes.md#archive--abandon-various-legacy-libraries).
> ## 🇷🇺 Русским гражданам
>
> Мы, участники Laminas, родились и живем в разных странах. У многих из нас есть друзья, родственники и коллеги как в России, так и в Украине. Некоторые из нас родились в России. Некоторые из нас живут в России. У некоторых бабушки и дедушки сражались с фашистами во Второй мировой войне. Здесь никто не поддерживает фашизм.
>
> У одного из нас есть украинская родственница, которая спаслась из дома вместе с сыном. Поезд задержался из-за бомбежки на дороге впереди. У нас есть друзья, которые прячутся в бомбоубежищах. Мы с тревогой ждем весточки от них после воздушных налетов, которые беспорядочно наносят удары и попадают по больницам, школам, детским садам и домам. Мы не берем это из каких-либо СМИ. Мы наблюдаем это напрямую.
>
> Вы доверяете нам достаточно, чтоб использовать наши программы, и мы просим вас довериться нам вновь. Мы нуждаемся в помощи. Выходите и протестуйте против этой бесполезной войны. Остановите кровопролитие. Скажите "Нет войне!"
>
> ## 🇺🇸 To Citizens of Russia
>
> We at Laminas come from all over the world. Many of us have friends, family and colleagues in both Russia and Ukraine. Some of us were born in Russia. Some of us currently live in Russia. Some have grandparents who fought Nazis in World War II. Nobody here supports fascism.
>
> One team member has a Ukrainian relative who fled her home with her son. The train was delayed due to bombing on the road ahead. We have friends who are hiding in bomb shelters. We anxiously follow up on them after the air raids, which indiscriminately fire at hospitals, schools, kindergartens and houses. We're not taking this from any media. These are our actual experiences.
>
> You trust us enough to use our software. We ask that you trust us to say the truth on this. We need your help. Go out and protest this unnecessary war. Stop the bloodshed. Say "stop the war!"
laminas-loader provides different strategies for autoloading PHP classes.
- Documentation is at https://docs.laminas.dev/laminas-loader/

View File

@@ -1,55 +0,0 @@
{
"name": "laminas/laminas-loader",
"description": "Autoloading and plugin loading strategies",
"license": "BSD-3-Clause",
"keywords": [
"laminas",
"loader"
],
"homepage": "https://laminas.dev",
"support": {
"docs": "https://docs.laminas.dev/laminas-loader/",
"issues": "https://github.com/laminas/laminas-loader/issues",
"source": "https://github.com/laminas/laminas-loader",
"rss": "https://github.com/laminas/laminas-loader/releases.atom",
"chat": "https://laminas.dev/chat",
"forum": "https://discourse.laminas.dev"
},
"config": {
"sort-packages": true,
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
},
"abandoned": true,
"require": {
"php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
},
"require-dev": {
"laminas/laminas-coding-standard": "~2.4.0",
"phpunit/phpunit": "~9.5.25"
},
"autoload": {
"psr-4": {
"Laminas\\Loader\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"LaminasTest\\Loader\\": "test/"
}
},
"scripts": {
"check": [
"@cs-check",
"@test"
],
"cs-check": "phpcs",
"cs-fix": "phpcbf",
"test": "phpunit --colors=always",
"test-coverage": "phpunit --colors=always --coverage-clover clover.xml"
},
"conflict": {
"zendframework/zend-loader": "*"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,212 +0,0 @@
<?php
namespace Laminas\Loader;
use Laminas\Loader\SplAutoloader;
use Laminas\Loader\StandardAutoloader;
use Traversable;
use function class_exists;
use function is_array;
use function is_subclass_of;
use function spl_autoload_unregister;
use function sprintf;
use function strrchr;
use function substr;
if (class_exists(AutoloaderFactory::class)) {
return;
}
// phpcs:ignore WebimpressCodingStandard.NamingConventions.AbstractClass.Prefix
abstract class AutoloaderFactory
{
public const STANDARD_AUTOLOADER = StandardAutoloader::class;
/** @var array All autoloaders registered using the factory */
protected static $loaders = [];
/**
* @var StandardAutoloader StandardAutoloader instance for resolving
* autoloader classes via the include_path
*/
protected static $standardAutoloader;
/**
* Factory for autoloaders
*
* Options should be an array or Traversable object of the following structure:
* <code>
* array(
* '<autoloader class name>' => $autoloaderOptions,
* )
* </code>
*
* The factory will then loop through and instantiate each autoloader with
* the specified options, and register each with the spl_autoloader.
*
* You may retrieve the concrete autoloader instances later using
* {@link getRegisteredAutoloaders()}.
*
* Note that the class names must be resolvable on the include_path or via
* the Laminas library, using PSR-0 rules (unless the class has already been
* loaded).
*
* @param array|Traversable $options (optional) options to use. Defaults to Laminas\Loader\StandardAutoloader
* @return void
* @throws Exception\InvalidArgumentException For invalid options.
* @throws Exception\InvalidArgumentException For unloadable autoloader classes.
* @throws Exception\DomainException For autoloader classes not implementing SplAutoloader.
*/
public static function factory($options = null)
{
if (null === $options) {
if (! isset(static::$loaders[static::STANDARD_AUTOLOADER])) {
$autoloader = static::getStandardAutoloader();
$autoloader->register();
static::$loaders[static::STANDARD_AUTOLOADER] = $autoloader;
}
// Return so we don't hit the next check's exception (we're done here anyway)
return;
}
if (! is_array($options) && ! $options instanceof Traversable) {
require_once __DIR__ . '/Exception/InvalidArgumentException.php';
throw new Exception\InvalidArgumentException(
'Options provided must be an array or Traversable'
);
}
foreach ($options as $class => $autoloaderOptions) {
if (! isset(static::$loaders[$class])) {
$autoloader = static::getStandardAutoloader();
if (! class_exists($class) && ! $autoloader->autoload($class)) {
require_once 'Exception/InvalidArgumentException.php';
throw new Exception\InvalidArgumentException(
sprintf('Autoloader class "%s" not loaded', $class)
);
}
if (! is_subclass_of($class, SplAutoloader::class)) {
require_once 'Exception/InvalidArgumentException.php';
throw new Exception\InvalidArgumentException(
sprintf('Autoloader class %s must implement Laminas\\Loader\\SplAutoloader', $class)
);
}
if ($class === static::STANDARD_AUTOLOADER) {
$autoloader->setOptions($autoloaderOptions);
} else {
$autoloader = new $class($autoloaderOptions);
}
$autoloader->register();
static::$loaders[$class] = $autoloader;
} else {
static::$loaders[$class]->setOptions($autoloaderOptions);
}
}
}
/**
* Get a list of all autoloaders registered with the factory
*
* Returns an array of autoloader instances.
*
* @return array
*/
public static function getRegisteredAutoloaders()
{
return static::$loaders;
}
/**
* Retrieves an autoloader by class name
*
* @param string $class
* @return SplAutoloader
* @throws Exception\InvalidArgumentException For non-registered class.
*/
public static function getRegisteredAutoloader($class)
{
if (! isset(static::$loaders[$class])) {
require_once 'Exception/InvalidArgumentException.php';
throw new Exception\InvalidArgumentException(sprintf('Autoloader class "%s" not loaded', $class));
}
return static::$loaders[$class];
}
/**
* Unregisters all autoloaders that have been registered via the factory.
* This will NOT unregister autoloaders registered outside of the fctory.
*
* @return void
*/
public static function unregisterAutoloaders()
{
foreach (static::getRegisteredAutoloaders() as $class => $autoloader) {
spl_autoload_unregister([$autoloader, 'autoload']);
unset(static::$loaders[$class]);
}
}
/**
* Unregister a single autoloader by class name
*
* @param string $autoloaderClass
* @return bool
*/
public static function unregisterAutoloader($autoloaderClass)
{
if (! isset(static::$loaders[$autoloaderClass])) {
return false;
}
$autoloader = static::$loaders[$autoloaderClass];
spl_autoload_unregister([$autoloader, 'autoload']);
unset(static::$loaders[$autoloaderClass]);
return true;
}
/**
* Get an instance of the standard autoloader
*
* Used to attempt to resolve autoloader classes, using the
* StandardAutoloader. The instance is marked as a fallback autoloader, to
* allow resolving autoloaders not under the "Laminas" namespace.
*
* @return SplAutoloader
*/
protected static function getStandardAutoloader()
{
if (null !== static::$standardAutoloader) {
return static::$standardAutoloader;
}
if (! class_exists(static::STANDARD_AUTOLOADER)) {
// Extract the filename from the classname
$stdAutoloader = substr(strrchr(static::STANDARD_AUTOLOADER, '\\'), 1);
require_once __DIR__ . "/$stdAutoloader.php";
}
$loader = new StandardAutoloader();
static::$standardAutoloader = $loader;
return static::$standardAutoloader;
}
/**
* Checks if the object has this class as one of its parents
*
* @deprecated since laminas 2.3 requires PHP >= 5.3.23
*
* @see https://bugs.php.net/bug.php?id=53727
* @see https://github.com/zendframework/zf2/pull/1807
*
* @param string $className
* @param string $type
* @return bool
*/
protected static function isSubclassOf($className, $type)
{
return is_subclass_of($className, $type);
}
}

View File

@@ -1,234 +0,0 @@
<?php
namespace Laminas\Loader;
use Traversable;
use function array_filter;
use function array_values;
use function array_walk;
use function explode;
use function file_exists;
use function gettype;
use function implode;
use function in_array;
use function is_array;
use function is_string;
use function preg_match;
use function realpath;
use function spl_autoload_register;
use function sprintf;
use function str_pad;
use function str_replace;
use function strlen;
use function substr;
// Grab SplAutoloader interface
require_once __DIR__ . '/SplAutoloader.php';
/**
* Class-map autoloader
*
* Utilizes class-map files to lookup classfile locations.
*/
class ClassMapAutoloader implements SplAutoloader
{
/**
* Registry of map files that have already been loaded
*
* @var array
*/
protected $mapsLoaded = [];
/**
* Class name/filename map
*
* @var array
*/
protected $map = [];
/**
* Constructor
*
* Create a new instance, and optionally configure the autoloader.
*
* @param null|array|Traversable $options
*/
public function __construct($options = null)
{
if (null !== $options) {
$this->setOptions($options);
}
}
/**
* Configure the autoloader
*
* Proxies to {@link registerAutoloadMaps()}.
*
* @param array|Traversable $options
* @return ClassMapAutoloader
*/
public function setOptions($options)
{
$this->registerAutoloadMaps($options);
return $this;
}
/**
* Register an autoload map
*
* An autoload map may be either an associative array, or a file returning
* an associative array.
*
* An autoload map should be an associative array containing
* classname/file pairs.
*
* @param string|array $map
* @throws Exception\InvalidArgumentException
* @return ClassMapAutoloader
*/
public function registerAutoloadMap($map)
{
if (is_string($map)) {
$location = $map;
if ($this === ($map = $this->loadMapFromFile($location))) {
return $this;
}
}
if (! is_array($map)) {
require_once __DIR__ . '/Exception/InvalidArgumentException.php';
throw new Exception\InvalidArgumentException(sprintf(
'Map file provided does not return a map. Map file: "%s"',
isset($location) && is_string($location) ? $location : 'unexpected type: ' . gettype($map)
));
}
$this->map = $map + $this->map;
if (isset($location)) {
$this->mapsLoaded[] = $location;
}
return $this;
}
/**
* Register many autoload maps at once
*
* @param array $locations
* @throws Exception\InvalidArgumentException
* @return ClassMapAutoloader
*/
public function registerAutoloadMaps($locations)
{
if (! is_array($locations) && ! $locations instanceof Traversable) {
require_once __DIR__ . '/Exception/InvalidArgumentException.php';
throw new Exception\InvalidArgumentException('Map list must be an array or implement Traversable');
}
foreach ($locations as $location) {
$this->registerAutoloadMap($location);
}
return $this;
}
/**
* Retrieve current autoload map
*
* @return array
*/
public function getAutoloadMap()
{
return $this->map;
}
/**
* {@inheritDoc}
*/
public function autoload($class)
{
if (isset($this->map[$class])) {
require_once $this->map[$class];
return $class;
}
return false;
}
/**
* Register the autoloader with spl_autoload registry
*
* @return void
*/
public function register()
{
spl_autoload_register([$this, 'autoload'], true, true);
}
/**
* Load a map from a file
*
* If the map has been previously loaded, returns the current instance;
* otherwise, returns whatever was returned by calling include() on the
* location.
*
* @param string $location
* @return ClassMapAutoloader|mixed
* @throws Exception\InvalidArgumentException For nonexistent locations.
*/
protected function loadMapFromFile($location)
{
if (! file_exists($location)) {
require_once __DIR__ . '/Exception/InvalidArgumentException.php';
throw new Exception\InvalidArgumentException(sprintf(
'Map file provided does not exist. Map file: "%s"',
is_string($location) ? $location : 'unexpected type: ' . gettype($location)
));
}
if (! $path = static::realPharPath($location)) {
$path = realpath($location);
}
if (in_array($path, $this->mapsLoaded)) {
// Already loaded this map
return $this;
}
return include $path;
}
/**
* Resolve the real_path() to a file within a phar.
*
* @see https://bugs.php.net/bug.php?id=52769
*
* @param string $path
* @return string
*/
public static function realPharPath($path)
{
if (! preg_match('|^phar:(/{2,3})|', $path, $match)) {
return;
}
$prefixLength = 5 + strlen($match[1]);
$parts = explode('/', str_replace(['/', '\\'], '/', substr($path, $prefixLength)));
$parts = array_values(array_filter($parts, function ($p) {
return $p !== '' && $p !== '.';
}));
array_walk($parts, function ($value, $key) use (&$parts) {
if ($value === '..') {
unset($parts[$key], $parts[$key - 1]);
$parts = array_values($parts);
}
});
if (file_exists($realPath = str_pad('phar:', $prefixLength, '/') . implode('/', $parts))) {
return $realPath;
}
}
}

View File

@@ -1,10 +0,0 @@
<?php
namespace Laminas\Loader\Exception;
require_once __DIR__ . '/ExceptionInterface.php';
class BadMethodCallException extends \BadMethodCallException implements
ExceptionInterface
{
}

View File

@@ -1,9 +0,0 @@
<?php
namespace Laminas\Loader\Exception;
require_once __DIR__ . '/ExceptionInterface.php';
class DomainException extends \DomainException implements ExceptionInterface
{
}

View File

@@ -1,7 +0,0 @@
<?php
namespace Laminas\Loader\Exception;
interface ExceptionInterface
{
}

View File

@@ -1,9 +0,0 @@
<?php
namespace Laminas\Loader\Exception;
require_once __DIR__ . '/ExceptionInterface.php';
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@@ -1,11 +0,0 @@
<?php
namespace Laminas\Loader\Exception;
use Exception;
require_once __DIR__ . '/ExceptionInterface.php';
class InvalidPathException extends Exception implements ExceptionInterface
{
}

View File

@@ -1,11 +0,0 @@
<?php
namespace Laminas\Loader\Exception;
use Exception;
require_once __DIR__ . '/ExceptionInterface.php';
class MissingResourceNamespaceException extends Exception implements ExceptionInterface
{
}

View File

@@ -1,12 +0,0 @@
<?php
namespace Laminas\Loader\Exception;
require_once __DIR__ . '/DomainException.php';
/**
* Plugin class loader exceptions
*/
class PluginLoaderException extends DomainException
{
}

View File

@@ -1,9 +0,0 @@
<?php
namespace Laminas\Loader\Exception;
require_once __DIR__ . '/ExceptionInterface.php';
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -1,9 +0,0 @@
<?php
namespace Laminas\Loader\Exception;
require_once __DIR__ . '/DomainException.php';
class SecurityException extends DomainException
{
}

View File

@@ -1,445 +0,0 @@
<?php
namespace Laminas\Loader;
// Grab SplAutoloader interface
require_once __DIR__ . '/SplAutoloader.php';
use GlobIterator;
use InvalidArgumentException;
use Phar;
use PharFileInfo;
use SplFileInfo;
use Traversable;
use function array_map;
use function class_exists;
use function count;
use function extension_loaded;
use function getcwd;
use function gettype;
use function implode;
use function in_array;
use function is_array;
use function is_string;
use function pathinfo;
use function preg_match;
use function realpath;
use function rtrim;
use function spl_autoload_register;
use function spl_autoload_unregister;
use function sprintf;
use function str_replace;
use function strpos;
use function substr;
use const DIRECTORY_SEPARATOR;
class ModuleAutoloader implements SplAutoloader
{
/** @var array An array of module paths to scan */
protected $paths = [];
/** @var array An array of modulename => path */
protected $explicitPaths = [];
/** @var array An array of namespaceName => namespacePath */
protected $namespacedPaths = [];
/** @var string Will contain the absolute phar:// path to the executable when packaged as phar file */
protected $pharBasePath = "";
/** @var array An array of supported phar extensions (filled on constructor) */
protected $pharExtensions = [];
/** @var array An array of module classes to their containing files */
protected $moduleClassMap = [];
/**
* Constructor
*
* Allow configuration of the autoloader via the constructor.
*
* @param null|array|Traversable $options
*/
public function __construct($options = null)
{
if (extension_loaded('phar')) {
$this->pharBasePath = Phar::running(true);
$this->pharExtensions = [
'phar',
'phar.tar',
'tar',
];
// ext/zlib enabled -> phar can read gzip & zip compressed files
if (extension_loaded('zlib')) {
$this->pharExtensions[] = 'phar.gz';
$this->pharExtensions[] = 'phar.tar.gz';
$this->pharExtensions[] = 'tar.gz';
$this->pharExtensions[] = 'phar.zip';
$this->pharExtensions[] = 'zip';
}
// ext/bzip2 enabled -> phar can read bz2 compressed files
if (extension_loaded('bzip2')) {
$this->pharExtensions[] = 'phar.bz2';
$this->pharExtensions[] = 'phar.tar.bz2';
$this->pharExtensions[] = 'tar.bz2';
}
}
if (null !== $options) {
$this->setOptions($options);
}
}
/**
* Configure the autoloader
*
* In most cases, $options should be either an associative array or
* Traversable object.
*
* @param array|Traversable $options
* @return ModuleAutoloader
*/
public function setOptions($options)
{
$this->registerPaths($options);
return $this;
}
/**
* Retrieves the class map for all loaded modules.
*
* @return array
*/
public function getModuleClassMap()
{
return $this->moduleClassMap;
}
/**
* Sets the class map used to speed up the module autoloading.
*
* @param array $classmap
* @return ModuleAutoloader
*/
public function setModuleClassMap(array $classmap)
{
$this->moduleClassMap = $classmap;
return $this;
}
/**
* Autoload a class
*
* @param string $class
* @return mixed
* False [if unable to load $class]
* get_class($class) [if $class is successfully loaded]
*/
public function autoload($class)
{
// Limit scope of this autoloader
if (substr($class, -7) !== '\Module') {
return false;
}
if (isset($this->moduleClassMap[$class])) {
require_once $this->moduleClassMap[$class];
return $class;
}
$moduleName = substr($class, 0, -7);
if (isset($this->explicitPaths[$moduleName])) {
$classLoaded = $this->loadModuleFromDir($this->explicitPaths[$moduleName], $class);
if ($classLoaded) {
return $classLoaded;
}
$classLoaded = $this->loadModuleFromPhar($this->explicitPaths[$moduleName], $class);
if ($classLoaded) {
return $classLoaded;
}
}
if (count($this->namespacedPaths) >= 1) {
foreach ($this->namespacedPaths as $namespace => $path) {
if (false === strpos($moduleName, $namespace)) {
continue;
}
$moduleNameBuffer = str_replace($namespace . "\\", "", $moduleName);
$path .= DIRECTORY_SEPARATOR . $moduleNameBuffer . DIRECTORY_SEPARATOR;
$classLoaded = $this->loadModuleFromDir($path, $class);
if ($classLoaded) {
return $classLoaded;
}
$classLoaded = $this->loadModuleFromPhar($path, $class);
if ($classLoaded) {
return $classLoaded;
}
}
}
$moduleClassPath = str_replace('\\', DIRECTORY_SEPARATOR, $moduleName);
$pharSuffixPattern = null;
if ($this->pharExtensions) {
$pharSuffixPattern = '(' . implode('|', array_map('preg_quote', $this->pharExtensions)) . ')';
}
foreach ($this->paths as $path) {
$path .= $moduleClassPath;
if ($path === '.' || substr($path, 0, 2) === './' || substr($path, 0, 2) === '.\\') {
if (! $basePath = $this->pharBasePath) {
$basePath = realpath('.');
}
if (false === $basePath) {
$basePath = getcwd();
}
$path = rtrim($basePath, '\/\\') . substr($path, 1);
}
$classLoaded = $this->loadModuleFromDir($path, $class);
if ($classLoaded) {
return $classLoaded;
}
// No directory with Module.php, searching for phars
if ($pharSuffixPattern) {
foreach (new GlobIterator($path . '.*') as $entry) {
if ($entry->isDir()) {
continue;
}
if (! preg_match('#.+\.' . $pharSuffixPattern . '$#', $entry->getPathname())) {
continue;
}
$classLoaded = $this->loadModuleFromPhar($entry->getPathname(), $class);
if ($classLoaded) {
return $classLoaded;
}
}
}
}
return false;
}
/**
* loadModuleFromDir
*
* @param string $dirPath
* @param string $class
* @return mixed
* False [if unable to load $class]
* get_class($class) [if $class is successfully loaded]
*/
protected function loadModuleFromDir($dirPath, $class)
{
$modulePath = $dirPath . '/Module.php';
if (substr($modulePath, 0, 7) === 'phar://') {
$file = new PharFileInfo($modulePath);
} else {
$file = new SplFileInfo($modulePath);
}
if ($file->isReadable() && $file->isFile()) {
// Found directory with Module.php in it
$absModulePath = $this->pharBasePath ? (string) $file : $file->getRealPath();
require_once $absModulePath;
if (class_exists($class)) {
$this->moduleClassMap[$class] = $absModulePath;
return $class;
}
}
return false;
}
/**
* loadModuleFromPhar
*
* @param string $pharPath
* @param string $class
* @return mixed
* False [if unable to load $class]
* get_class($class) [if $class is successfully loaded]
*/
protected function loadModuleFromPhar($pharPath, $class)
{
$pharPath = static::normalizePath($pharPath, false);
$file = new SplFileInfo($pharPath);
if (! $file->isReadable() || ! $file->isFile()) {
return false;
}
$fileRealPath = $file->getRealPath();
// Phase 0: Check for executable phar with Module class in stub
if (strpos($fileRealPath, '.phar') !== false) {
// First see if the stub makes the Module class available
require_once $fileRealPath;
if (class_exists($class)) {
$this->moduleClassMap[$class] = $fileRealPath;
return $class;
}
}
// Phase 1: Not executable phar, no stub, or stub did not provide Module class; try Module.php directly
$moduleClassFile = 'phar://' . $fileRealPath . '/Module.php';
$moduleFile = new SplFileInfo($moduleClassFile);
if ($moduleFile->isReadable() && $moduleFile->isFile()) {
require_once $moduleClassFile;
if (class_exists($class)) {
$this->moduleClassMap[$class] = $moduleClassFile;
return $class;
}
}
// Phase 2: Check for nested module directory within archive
// Checks for /path/to/MyModule.tar/MyModule/Module.php
// (shell-integrated zip/tar utilities wrap directories like this)
$pharBaseName = $this->pharFileToModuleName($fileRealPath);
$moduleClassFile = 'phar://' . $fileRealPath . '/' . $pharBaseName . '/Module.php';
$moduleFile = new SplFileInfo($moduleClassFile);
if ($moduleFile->isReadable() && $moduleFile->isFile()) {
require_once $moduleClassFile;
if (class_exists($class)) {
$this->moduleClassMap[$class] = $moduleClassFile;
return $class;
}
}
return false;
}
/**
* Register the autoloader with spl_autoload registry
*
* @return void
*/
public function register()
{
spl_autoload_register([$this, 'autoload']);
}
/**
* Unregister the autoloader with spl_autoload registry
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister([$this, 'autoload']);
}
/**
* registerPaths
*
* @param array|Traversable $paths
* @throws InvalidArgumentException
* @return ModuleAutoloader
*/
public function registerPaths($paths)
{
if (! is_array($paths) && ! $paths instanceof Traversable) {
require_once __DIR__ . '/Exception/InvalidArgumentException.php';
throw new Exception\InvalidArgumentException(
'Parameter to \\Laminas\\Loader\\ModuleAutoloader\'s '
. 'registerPaths method must be an array or '
. 'implement the Traversable interface'
);
}
foreach ($paths as $module => $path) {
if (is_string($module)) {
$this->registerPath($path, $module);
} else {
$this->registerPath($path);
}
}
return $this;
}
/**
* registerPath
*
* @param string $path
* @param bool|string $moduleName
* @throws InvalidArgumentException
* @return ModuleAutoloader
*/
public function registerPath($path, $moduleName = false)
{
if (! is_string($path)) {
require_once __DIR__ . '/Exception/InvalidArgumentException.php';
throw new Exception\InvalidArgumentException(sprintf(
'Invalid path provided; must be a string, received %s',
gettype($path)
));
}
if ($moduleName) {
if (in_array(substr($moduleName, -2), ['\\*', '\\%'])) {
$this->namespacedPaths[substr($moduleName, 0, -2)] = static::normalizePath($path);
} else {
$this->explicitPaths[$moduleName] = static::normalizePath($path);
}
} else {
$this->paths[] = static::normalizePath($path);
}
return $this;
}
/**
* getPaths
*
* This is primarily for unit testing, but could have other uses.
*
* @return array
*/
public function getPaths()
{
return $this->paths;
}
/**
* Returns the base module name from the path to a phar
*
* @param string $pharPath
* @return string
*/
protected function pharFileToModuleName($pharPath)
{
do {
$pathinfo = pathinfo($pharPath);
$pharPath = $pathinfo['filename'];
} while (isset($pathinfo['extension']));
return $pathinfo['filename'];
}
/**
* Normalize a path for insertion in the stack
*
* @param string $path
* @param bool $trailingSlash Whether trailing slash should be included
* @return string
*/
public static function normalizePath($path, $trailingSlash = true)
{
$path = rtrim($path, '/');
$path = rtrim($path, '\\');
if ($trailingSlash) {
$path .= DIRECTORY_SEPARATOR;
}
return $path;
}
}

View File

@@ -1,223 +0,0 @@
<?php // phpcs:disable SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse
namespace Laminas\Loader;
use ArrayIterator;
use IteratorAggregate;
use ReturnTypeWillChange;
use Traversable;
use function array_key_exists;
use function class_exists;
use function is_array;
use function is_int;
use function is_numeric;
use function is_object;
use function is_string;
use function strtolower;
/**
* Plugin class locator interface
*/
class PluginClassLoader implements PluginClassLocator
{
/**
* List of plugin name => class name pairs
*
* @var array
*/
protected $plugins = [];
/**
* Static map allow global seeding of plugin loader
*
* @var array
*/
protected static $staticMap = [];
/**
* Constructor
*
* @param null|array|Traversable $map If provided, seeds the loader with a map
*/
public function __construct($map = null)
{
// Merge in static overrides
if (! empty(static::$staticMap)) {
$this->registerPlugins(static::$staticMap);
}
// Merge in constructor arguments
if ($map !== null) {
$this->registerPlugins($map);
}
}
/**
* Add a static map of plugins
*
* A null value will clear the static map.
*
* @param null|array|Traversable $map
* @throws Exception\InvalidArgumentException
* @return void
*/
public static function addStaticMap($map)
{
if (null === $map) {
static::$staticMap = [];
return;
}
if (! is_array($map) && ! $map instanceof Traversable) {
throw new Exception\InvalidArgumentException('Expects an array or Traversable object');
}
foreach ($map as $key => $value) {
static::$staticMap[$key] = $value;
}
}
/**
* Register a class to a given short name
*
* @param string $shortName
* @param string $className
* @return PluginClassLoader
*/
public function registerPlugin($shortName, $className)
{
$this->plugins[strtolower($shortName)] = $className;
return $this;
}
/**
* Register many plugins at once
*
* If $map is a string, assumes that the map is the class name of a
* Traversable object (likely a ShortNameLocator); it will then instantiate
* this class and use it to register plugins.
*
* If $map is an array or Traversable object, it will iterate it to
* register plugin names/classes.
*
* For all other arguments, or if the string $map is not a class or not a
* Traversable class, an exception will be raised.
*
* @param string|array|Traversable $map
* @return PluginClassLoader
* @throws Exception\InvalidArgumentException
*/
public function registerPlugins($map)
{
if (is_string($map)) {
if (! class_exists($map)) {
throw new Exception\InvalidArgumentException('Map class provided is invalid');
}
$map = new $map();
}
if (is_array($map)) {
$map = new ArrayIterator($map);
}
if (! $map instanceof Traversable) {
throw new Exception\InvalidArgumentException('Map provided is invalid; must be traversable');
}
// iterator_apply doesn't work as expected with IteratorAggregate
if ($map instanceof IteratorAggregate) {
$map = $map->getIterator();
}
foreach ($map as $name => $class) {
if (is_int($name) || is_numeric($name)) {
if (! is_object($class) && class_exists($class)) {
$class = new $class();
}
if ($class instanceof Traversable) {
$this->registerPlugins($class);
continue;
}
}
$this->registerPlugin($name, $class);
}
return $this;
}
/**
* Unregister a short name lookup
*
* @param mixed $shortName
* @return PluginClassLoader
*/
public function unregisterPlugin($shortName)
{
$lookup = strtolower($shortName);
if (array_key_exists($lookup, $this->plugins)) {
unset($this->plugins[$lookup]);
}
return $this;
}
/**
* Get a list of all registered plugins
*
* @return array|Traversable
*/
public function getRegisteredPlugins()
{
return $this->plugins;
}
/**
* Whether or not a plugin by a specific name has been registered
*
* @param string $name
* @return bool
*/
public function isLoaded($name)
{
$lookup = strtolower($name);
return isset($this->plugins[$lookup]);
}
/**
* Return full class name for a named helper
*
* @param string $name
* @return string|false
*/
public function getClassName($name)
{
return $this->load($name);
}
/**
* Load a helper via the name provided
*
* @param string $name
* @return string|false
*/
public function load($name)
{
if (! $this->isLoaded($name)) {
return false;
}
return $this->plugins[strtolower($name)];
}
/**
* Defined by IteratorAggregate
*
* Returns an instance of ArrayIterator, containing a map of
* all plugins
*
* @return ArrayIterator
*/
#[ReturnTypeWillChange]
public function getIterator()
{
return new ArrayIterator($this->plugins);
}
}

View File

@@ -1,36 +0,0 @@
<?php // phpcs:disable WebimpressCodingStandard.NamingConventions.Interface.Suffix
namespace Laminas\Loader;
use IteratorAggregate;
use Traversable;
/**
* Plugin class locator interface
*/
interface PluginClassLocator extends ShortNameLocator, IteratorAggregate
{
/**
* Register a class to a given short name
*
* @param string $shortName
* @param string $className
* @return PluginClassLocator
*/
public function registerPlugin($shortName, $className);
/**
* Unregister a short name lookup
*
* @param mixed $shortName
* @return void
*/
public function unregisterPlugin($shortName);
/**
* Get a list of all registered plugins
*
* @return array|Traversable
*/
public function getRegisteredPlugins();
}

View File

@@ -1,33 +0,0 @@
<?php // phpcs:disable WebimpressCodingStandard.NamingConventions.Interface.Suffix
namespace Laminas\Loader;
/**
* Short name locator interface
*/
interface ShortNameLocator
{
/**
* Whether or not a Helper by a specific name
*
* @param string $name
* @return bool
*/
public function isLoaded($name);
/**
* Return full class name for a named helper
*
* @param string $name
* @return string
*/
public function getClassName($name);
/**
* Load a helper via the name provided
*
* @param string $name
* @return string
*/
public function load($name);
}

View File

@@ -1,60 +0,0 @@
<?php // phpcs:disable WebimpressCodingStandard.NamingConventions.Interface.Suffix
namespace Laminas\Loader;
use Traversable;
use function interface_exists;
if (interface_exists(SplAutoloader::class)) {
return;
}
/**
* Defines an interface for classes that may register with the spl_autoload
* registry
*/
interface SplAutoloader
{
/**
* Constructor
*
* Allow configuration of the autoloader via the constructor.
*
* @param null|array|Traversable $options
*/
public function __construct($options = null);
/**
* Configure the autoloader
*
* In most cases, $options should be either an associative array or
* Traversable object.
*
* @param array|Traversable $options
* @return SplAutoloader
*/
public function setOptions($options);
/**
* Autoload a class
*
* @param string $class
* @return mixed
* False [if unable to load $class]
* get_class($class) [if $class is successfully loaded]
*/
public function autoload($class);
/**
* Register the autoloader with spl_autoload registry
*
* Typically, the body of this will simply be:
* <code>
* spl_autoload_register(array($this, 'autoload'));
* </code>
*
* @return void
*/
public function register();
}

View File

@@ -1,332 +0,0 @@
<?php
namespace Laminas\Loader;
use Traversable;
use function dirname;
use function file_exists;
use function in_array;
use function is_array;
use function preg_match;
use function rtrim;
use function spl_autoload_register;
use function str_replace;
use function stream_resolve_include_path;
use function strlen;
use function strpos;
use function substr;
use const DIRECTORY_SEPARATOR;
// Grab SplAutoloader interface
require_once __DIR__ . '/SplAutoloader.php';
/**
* PSR-0 compliant autoloader
*
* Allows autoloading both namespaced and vendor-prefixed classes. Class
* lookups are performed on the filesystem. If a class file for the referenced
* class is not found, a PHP warning will be raised by include().
*/
class StandardAutoloader implements SplAutoloader
{
public const NS_SEPARATOR = '\\';
public const PREFIX_SEPARATOR = '_';
public const LOAD_NS = 'namespaces';
public const LOAD_PREFIX = 'prefixes';
public const ACT_AS_FALLBACK = 'fallback_autoloader';
/** @deprecated Use AUTOREGISTER_LAMINAS instead */
public const AUTOREGISTER_ZF = 'autoregister_laminas';
public const AUTOREGISTER_LAMINAS = 'autoregister_laminas';
/** @var array Namespace/directory pairs to search; Laminas library added by default */
protected $namespaces = [];
/** @var array Prefix/directory pairs to search */
protected $prefixes = [];
/** @var bool Whether or not the autoloader should also act as a fallback autoloader */
protected $fallbackAutoloaderFlag = false;
/**
* Constructor
*
* @param null|array|Traversable $options
*/
public function __construct($options = null)
{
if (null !== $options) {
$this->setOptions($options);
}
}
/**
* Configure autoloader
*
* Allows specifying both "namespace" and "prefix" pairs, using the
* following structure:
* <code>
* array(
* 'namespaces' => array(
* 'Laminas' => '/path/to/Laminas/library',
* 'Doctrine' => '/path/to/Doctrine/library',
* ),
* 'prefixes' => array(
* 'Phly_' => '/path/to/Phly/library',
* ),
* 'fallback_autoloader' => true,
* )
* </code>
*
* @param array|Traversable $options
* @throws Exception\InvalidArgumentException
* @return StandardAutoloader
*/
public function setOptions($options)
{
if (! is_array($options) && ! $options instanceof Traversable) {
require_once __DIR__ . '/Exception/InvalidArgumentException.php';
throw new Exception\InvalidArgumentException('Options must be either an array or Traversable');
}
foreach ($options as $type => $pairs) {
switch ($type) {
case self::AUTOREGISTER_LAMINAS:
if ($pairs) {
$this->registerNamespace('Laminas', dirname(__DIR__));
}
break;
case self::LOAD_NS:
if (is_array($pairs) || $pairs instanceof Traversable) {
$this->registerNamespaces($pairs);
}
break;
case self::LOAD_PREFIX:
if (is_array($pairs) || $pairs instanceof Traversable) {
$this->registerPrefixes($pairs);
}
break;
case self::ACT_AS_FALLBACK:
$this->setFallbackAutoloader($pairs);
break;
default:
// ignore
}
}
return $this;
}
/**
* Set flag indicating fallback autoloader status
*
* @param bool $flag
* @return StandardAutoloader
*/
public function setFallbackAutoloader($flag)
{
$this->fallbackAutoloaderFlag = (bool) $flag;
return $this;
}
/**
* Is this autoloader acting as a fallback autoloader?
*
* @return bool
*/
public function isFallbackAutoloader()
{
return $this->fallbackAutoloaderFlag;
}
/**
* Register a namespace/directory pair
*
* @param string $namespace
* @param string $directory
* @return StandardAutoloader
*/
public function registerNamespace($namespace, $directory)
{
$namespace = rtrim($namespace, self::NS_SEPARATOR) . self::NS_SEPARATOR;
$this->namespaces[$namespace] = $this->normalizeDirectory($directory);
return $this;
}
/**
* Register many namespace/directory pairs at once
*
* @param array $namespaces
* @throws Exception\InvalidArgumentException
* @return StandardAutoloader
*/
public function registerNamespaces($namespaces)
{
if (! is_array($namespaces) && ! $namespaces instanceof Traversable) {
require_once __DIR__ . '/Exception/InvalidArgumentException.php';
throw new Exception\InvalidArgumentException('Namespace pairs must be either an array or Traversable');
}
foreach ($namespaces as $namespace => $directory) {
$this->registerNamespace($namespace, $directory);
}
return $this;
}
/**
* Register a prefix/directory pair
*
* @param string $prefix
* @param string $directory
* @return StandardAutoloader
*/
public function registerPrefix($prefix, $directory)
{
$prefix = rtrim($prefix, self::PREFIX_SEPARATOR) . self::PREFIX_SEPARATOR;
$this->prefixes[$prefix] = $this->normalizeDirectory($directory);
return $this;
}
/**
* Register many namespace/directory pairs at once
*
* @param array $prefixes
* @throws Exception\InvalidArgumentException
* @return StandardAutoloader
*/
public function registerPrefixes($prefixes)
{
if (! is_array($prefixes) && ! $prefixes instanceof Traversable) {
require_once __DIR__ . '/Exception/InvalidArgumentException.php';
throw new Exception\InvalidArgumentException('Prefix pairs must be either an array or Traversable');
}
foreach ($prefixes as $prefix => $directory) {
$this->registerPrefix($prefix, $directory);
}
return $this;
}
/**
* Defined by Autoloadable; autoload a class
*
* @param string $class
* @return false|string
*/
public function autoload($class)
{
$isFallback = $this->isFallbackAutoloader();
if (false !== strpos($class, self::NS_SEPARATOR)) {
if ($this->loadClass($class, self::LOAD_NS)) {
return $class;
} elseif ($isFallback) {
return $this->loadClass($class, self::ACT_AS_FALLBACK);
}
return false;
}
if (false !== strpos($class, self::PREFIX_SEPARATOR)) {
if ($this->loadClass($class, self::LOAD_PREFIX)) {
return $class;
} elseif ($isFallback) {
return $this->loadClass($class, self::ACT_AS_FALLBACK);
}
return false;
}
if ($isFallback) {
return $this->loadClass($class, self::ACT_AS_FALLBACK);
}
return false;
}
/**
* Register the autoloader with spl_autoload
*
* @return void
*/
public function register()
{
spl_autoload_register([$this, 'autoload']);
}
/**
* Transform the class name to a filename
*
* @param string $class
* @param string $directory
* @return string
*/
protected function transformClassNameToFilename($class, $directory)
{
// $class may contain a namespace portion, in which case we need
// to preserve any underscores in that portion.
$matches = [];
preg_match('/(?P<namespace>.+\\\)?(?P<class>[^\\\]+$)/', $class, $matches);
$class = $matches['class'] ?? '';
$namespace = $matches['namespace'] ?? '';
return $directory
. str_replace(self::NS_SEPARATOR, '/', $namespace)
. str_replace(self::PREFIX_SEPARATOR, '/', $class)
. '.php';
}
/**
* Load a class, based on its type (namespaced or prefixed)
*
* @param string $class
* @param string $type
* @return bool|string
* @throws Exception\InvalidArgumentException
*/
protected function loadClass($class, $type)
{
if (! in_array($type, [self::LOAD_NS, self::LOAD_PREFIX, self::ACT_AS_FALLBACK])) {
require_once __DIR__ . '/Exception/InvalidArgumentException.php';
throw new Exception\InvalidArgumentException();
}
// Fallback autoloading
if ($type === self::ACT_AS_FALLBACK) {
// create filename
$filename = $this->transformClassNameToFilename($class, '');
$resolvedName = stream_resolve_include_path($filename);
if ($resolvedName !== false) {
return include $resolvedName;
}
return false;
}
// Namespace and/or prefix autoloading
foreach ($this->$type as $leader => $path) {
if (0 === strpos($class, $leader)) {
// Trim off leader (namespace or prefix)
$trimmedClass = substr($class, strlen($leader));
// create filename
$filename = $this->transformClassNameToFilename($trimmedClass, $path);
if (file_exists($filename)) {
return include $filename;
}
}
}
return false;
}
/**
* Normalize the directory to include a trailing directory separator
*
* @param string $directory
* @return string
*/
protected function normalizeDirectory($directory)
{
$last = $directory[strlen($directory) - 1];
if (in_array($last, ['/', '\\'])) {
$directory[strlen($directory) - 1] = DIRECTORY_SEPARATOR;
return $directory;
}
$directory .= DIRECTORY_SEPARATOR;
return $directory;
}
}

View File

@@ -1 +0,0 @@
Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. (https://getlaminas.org/)

View File

@@ -1,26 +0,0 @@
Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
- Neither the name of Laminas Foundation nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,40 +0,0 @@
# laminas-servicemanager
[![Build Status](https://github.com/laminas/laminas-servicemanager/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/laminas/laminas-servicemanager/actions/workflows/continuous-integration.yml)
[![Psalm coverage](https://shepherd.dev/github/laminas/laminas-servicemanager/coverage.svg?)](https://shepherd.dev/github/laminas/laminas-servicemanager)
> ## 🇷🇺 Русским гражданам
>
> Мы, участники Laminas, родились и живем в разных странах. У многих из нас есть друзья, родственники и коллеги как в России, так и в Украине. Некоторые из нас родились в России. Некоторые из нас живут в России. У некоторых бабушки и дедушки сражались с фашистами во Второй мировой войне. Здесь никто не поддерживает фашизм.
>
> У одного из нас есть украинская родственница, которая спаслась из дома вместе с сыном. Поезд задержался из-за бомбежки на дороге впереди. У нас есть друзья, которые прячутся в бомбоубежищах. Мы с тревогой ждем весточки от них после воздушных налетов, которые беспорядочно наносят удары и попадают по больницам, школам, детским садам и домам. Мы не берем это из каких-либо СМИ. Мы наблюдаем это напрямую.
>
> Вы доверяете нам достаточно, чтоб использовать наши программы, и мы просим вас довериться нам вновь. Мы нуждаемся в помощи. Выходите и протестуйте против этой бесполезной войны. Остановите кровопролитие. Скажите "Нет войне!"
>
> ## 🇺🇸 To Citizens of Russia
>
> We at Laminas come from all over the world. Many of us have friends, family and colleagues in both Russia and Ukraine. Some of us were born in Russia. Some of us currently live in Russia. Some have grandparents who fought Nazis in World War II. Nobody here supports fascism.
>
> One team member has a Ukrainian relative who fled her home with her son. The train was delayed due to bombing on the road ahead. We have friends who are hiding in bomb shelters. We anxiously follow up on them after the air raids, which indiscriminately fire at hospitals, schools, kindergartens and houses. We're not taking this from any media. These are our actual experiences.
>
> You trust us enough to use our software. We ask that you trust us to say the truth on this. We need your help. Go out and protest this unnecessary war. Stop the bloodshed. Say "stop the war!"
The Service Locator design pattern is implemented by the `Laminas\ServiceManager`
component. The Service Locator is a service/object locator, tasked with
retrieving other objects.
- File issues at https://github.com/laminas/laminas-servicemanager/issues
- [Online documentation](https://docs.laminas.dev/laminas-servicemanager)
- [Documentation source files](docs/book/)
## Benchmarks
We provide scripts for benchmarking laminas-servicemanager using the
[PHPBench](https://github.com/phpbench/phpbench) framework; these can be
found in the `benchmarks/` directory.
To execute the benchmarks you can run the following command:
```bash
$ vendor/bin/phpbench run --report=aggregate
```

View File

@@ -1,22 +0,0 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
namespace Laminas\ServiceManager;
// Setup/verify autoloading
if (file_exists($a = getcwd() . '/vendor/autoload.php')) {
require $a;
} elseif (file_exists($a = __DIR__ . '/../../../autoload.php')) {
require $a;
} elseif (file_exists($a = __DIR__ . '/../vendor/autoload.php')) {
require $a;
} else {
fwrite(STDERR, 'Cannot locate autoloader; please run "composer install"' . PHP_EOL);
exit(1);
}
$command = new Tool\ConfigDumperCommand($argv[0]);
$status = $command(array_slice($argv, 1));
exit($status);

View File

@@ -1,22 +0,0 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
namespace Laminas\ServiceManager;
// Setup/verify autoloading
if (file_exists($a = getcwd() . '/vendor/autoload.php')) {
require $a;
} elseif (file_exists($a = __DIR__ . '/../../../autoload.php')) {
require $a;
} elseif (file_exists($a = __DIR__ . '/../vendor/autoload.php')) {
require $a;
} else {
fwrite(STDERR, 'Cannot locate autoloader; please run "composer install"' . PHP_EOL);
exit(1);
}
$command = new Tool\FactoryCreatorCommand($argv[0]);
$status = $command(array_slice($argv, 1));
exit($status);

View File

@@ -1,98 +0,0 @@
{
"name": "laminas/laminas-servicemanager",
"description": "Factory-Driven Dependency Injection Container",
"license": "BSD-3-Clause",
"keywords": [
"laminas",
"di",
"dic",
"dependency-injection",
"psr-11",
"servicemanager",
"service-manager"
],
"homepage": "https://laminas.dev",
"support": {
"issues": "https://github.com/laminas/laminas-servicemanager/issues",
"forum": "https://discourse.laminas.dev",
"chat": "https://laminas.dev/chat",
"source": "https://github.com/laminas/laminas-servicemanager",
"docs": "https://docs.laminas.dev/laminas-servicemanager/",
"rss": "https://github.com/laminas/laminas-servicemanager/releases.atom"
},
"require": {
"php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"laminas/laminas-stdlib": "^3.19",
"psr/container": "^1.0"
},
"require-dev": {
"composer/package-versions-deprecated": "^1.11.99.5",
"friendsofphp/proxy-manager-lts": "^1.0.18",
"laminas/laminas-code": "^4.16.0",
"laminas/laminas-coding-standard": "~2.5.0",
"laminas/laminas-container-config-test": "^0.8",
"mikey179/vfsstream": "^1.6.12",
"phpbench/phpbench": "^1.4.1",
"phpunit/phpunit": "^10.5.58",
"psalm/plugin-phpunit": "^0.18.4",
"vimeo/psalm": "^5.26.1"
},
"replace": {
"container-interop/container-interop": "^1.2.0"
},
"conflict": {
"ext-psr": "*",
"laminas/laminas-code": "<4.10.0",
"zendframework/zend-code": "<3.3.1",
"zendframework/zend-servicemanager": "*"
},
"provide": {
"psr/container-implementation": "^1.0"
},
"suggest": {
"friendsofphp/proxy-manager-lts": "ProxyManager ^2.1.1 to handle lazy initialization of services"
},
"autoload": {
"psr-4": {
"Laminas\\ServiceManager\\": "src/"
},
"files": [
"src/autoload.php"
]
},
"autoload-dev": {
"psr-4": {
"LaminasTest\\ServiceManager\\": "test/",
"LaminasBench\\ServiceManager\\": "benchmarks/"
},
"files": [
"test/autoload.php"
]
},
"bin": [
"bin/generate-deps-for-config-factory",
"bin/generate-factory-for-class"
],
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"composer/package-versions-deprecated": true
},
"platform": {
"php": "8.2.99"
},
"sort-packages": true
},
"scripts": {
"benchmark": "phpbench run --revs=2 --iterations=2 --report=aggregate",
"check": [
"@cs-check",
"@test"
],
"cs-check": "phpcs",
"cs-fix": "phpcbf",
"static-analysis": "psalm --shepherd --stats",
"test": "phpunit --colors=always",
"test-coverage": "phpunit --colors=always --coverage-clover clover.xml"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,78 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\ServiceManager\AbstractFactory;
use ArrayObject;
use Laminas\ServiceManager\Exception\ServiceNotCreatedException;
use Laminas\ServiceManager\Factory\AbstractFactoryInterface;
use Psr\Container\ContainerInterface;
use function array_key_exists;
use function array_map;
use function array_values;
use function is_array;
use function json_encode;
final class ConfigAbstractFactory implements AbstractFactoryInterface
{
/**
* Factory can create the service if there is a key for it in the config
*
* {@inheritdoc}
*/
public function canCreate(ContainerInterface $container, $requestedName)
{
if (! $container->has('config')) {
return false;
}
$config = $container->get('config');
if (! isset($config[self::class])) {
return false;
}
$dependencies = $config[self::class];
return is_array($dependencies) && array_key_exists($requestedName, $dependencies);
}
/** {@inheritDoc} */
public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null)
{
if (! $container->has('config')) {
throw new ServiceNotCreatedException('Cannot find a config array in the container');
}
$config = $container->get('config');
if (! (is_array($config) || $config instanceof ArrayObject)) {
throw new ServiceNotCreatedException('Config must be an array or an instance of ArrayObject');
}
if (! isset($config[self::class])) {
throw new ServiceNotCreatedException('Cannot find a `' . self::class . '` key in the config array');
}
$dependencies = $config[self::class];
if (
! is_array($dependencies)
|| ! array_key_exists($requestedName, $dependencies)
|| ! is_array($dependencies[$requestedName])
) {
throw new ServiceNotCreatedException('Service dependencies config must exist and be an array');
}
$serviceDependencies = $dependencies[$requestedName];
if ($serviceDependencies !== array_values(array_map('strval', $serviceDependencies))) {
$problem = json_encode(array_map('gettype', $serviceDependencies));
throw new ServiceNotCreatedException(
'Service dependencies config must be an array of strings, ' . $problem . ' given'
);
}
$arguments = array_map([$container, 'get'], $serviceDependencies);
return new $requestedName(...$arguments);
}
}

View File

@@ -1,245 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\ServiceManager\AbstractFactory;
use Laminas\ServiceManager\Exception\ServiceNotFoundException;
use Laminas\ServiceManager\Factory\AbstractFactoryInterface;
use Psr\Container\ContainerInterface;
use ReflectionClass;
use ReflectionNamedType;
use ReflectionParameter;
use function array_map;
use function class_exists;
use function interface_exists;
use function is_string;
use function sprintf;
/**
* Reflection-based factory.
*
* To ease development, this factory may be used for classes with
* type-hinted arguments that resolve to services in the application
* container; this allows omitting the step of writing a factory for
* each controller.
*
* You may use it as either an abstract factory:
*
* <code>
* 'service_manager' => [
* 'abstract_factories' => [
* ReflectionBasedAbstractFactory::class,
* ],
* ],
* </code>
*
* Or as a factory, mapping a class name to it:
*
* <code>
* 'service_manager' => [
* 'factories' => [
* MyClassWithDependencies::class => ReflectionBasedAbstractFactory::class,
* ],
* ],
* </code>
*
* The latter approach is more explicit, and also more performant.
*
* The factory has the following constraints/features:
*
* - A parameter named `$config` typehinted as an array will receive the
* application "config" service (i.e., the merged configuration).
* - Parameters type-hinted against array, but not named `$config` will
* be injected with an empty array.
* - Scalar parameters will result in an exception being thrown, unless
* a default value is present; if the default is present, that will be used.
* - If a service cannot be found for a given typehint, the factory will
* raise an exception detailing this.
* - Some services provided by Laminas components do not have
* entries based on their class name (for historical reasons); the
* factory allows defining a map of these class/interface names to the
* corresponding service name to allow them to resolve.
*
* `$options` passed to the factory are ignored in all cases, as we cannot
* make assumptions about which argument(s) they might replace.
*
* Based on the LazyControllerAbstractFactory from laminas-mvc.
*/
class ReflectionBasedAbstractFactory implements AbstractFactoryInterface
{
/**
* Maps known classes/interfaces to the service that provides them; only
* required for those services with no entry based on the class/interface
* name.
*
* Extend the class if you wish to add to the list.
*
* Example:
*
* <code>
* [
* \Laminas\Filter\FilterPluginManager::class => 'FilterManager',
* \Laminas\Validator\ValidatorPluginManager::class => 'ValidatorManager',
* ]
* </code>
*
* @var string[]
*/
protected $aliases = [];
/**
* Allows overriding the internal list of aliases. These should be of the
* form `class name => well-known service name`; see the documentation for
* the `$aliases` property for details on what is accepted.
*
* @param string[] $aliases
*/
public function __construct(array $aliases = [])
{
if (! empty($aliases)) {
$this->aliases = $aliases;
}
}
/**
* {@inheritDoc}
*
* @return DispatchableInterface
*/
public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null)
{
$reflectionClass = new ReflectionClass($requestedName);
if (null === ($constructor = $reflectionClass->getConstructor())) {
return new $requestedName();
}
$reflectionParameters = $constructor->getParameters();
if (empty($reflectionParameters)) {
return new $requestedName();
}
$resolver = $container->has('config')
? $this->resolveParameterWithConfigService($container, $requestedName)
: $this->resolveParameterWithoutConfigService($container, $requestedName);
$parameters = array_map($resolver, $reflectionParameters);
return new $requestedName(...$parameters);
}
/** {@inheritDoc} */
public function canCreate(ContainerInterface $container, $requestedName)
{
return class_exists($requestedName) && $this->canCallConstructor($requestedName);
}
private function canCallConstructor(string $requestedName): bool
{
$constructor = (new ReflectionClass($requestedName))->getConstructor();
return $constructor === null || $constructor->isPublic();
}
/**
* Resolve a parameter to a value.
*
* Returns a callback for resolving a parameter to a value, but without
* allowing mapping array `$config` arguments to the `config` service.
*
* @param string $requestedName
* @return callable
*/
private function resolveParameterWithoutConfigService(ContainerInterface $container, $requestedName)
{
/**
* @param ReflectionParameter $parameter
* @return mixed
* @throws ServiceNotFoundException If type-hinted parameter cannot be
* resolved to a service in the container.
* @psalm-suppress MissingClosureReturnType
*/
return fn(ReflectionParameter $parameter) => $this->resolveParameter($parameter, $container, $requestedName);
}
/**
* Returns a callback for resolving a parameter to a value, including mapping 'config' arguments.
*
* Unlike resolveParameter(), this version will detect `$config` array
* arguments and have them return the 'config' service.
*
* @param string $requestedName
* @return callable
*/
private function resolveParameterWithConfigService(ContainerInterface $container, $requestedName)
{
/**
* @param ReflectionParameter $parameter
* @return mixed
* @throws ServiceNotFoundException If type-hinted parameter cannot be
* resolved to a service in the container.
*/
return function (ReflectionParameter $parameter) use ($container, $requestedName) {
if ($parameter->getName() === 'config') {
$type = $parameter->getType();
if ($type instanceof ReflectionNamedType && $type->getName() === 'array') {
return $container->get('config');
}
}
return $this->resolveParameter($parameter, $container, $requestedName);
};
}
/**
* Logic common to all parameter resolution.
*
* @param string $requestedName
* @return mixed
* @throws ServiceNotFoundException If type-hinted parameter cannot be
* resolved to a service in the container.
*/
private function resolveParameter(ReflectionParameter $parameter, ContainerInterface $container, $requestedName)
{
$type = $parameter->getType();
$type = $type instanceof ReflectionNamedType ? $type->getName() : null;
if ($type === 'array') {
return [];
}
if ($type === null || (is_string($type) && ! class_exists($type) && ! interface_exists($type))) {
if (! $parameter->isDefaultValueAvailable()) {
throw new ServiceNotFoundException(sprintf(
'Unable to create service "%s"; unable to resolve parameter "%s" '
. 'to a class, interface, or array type',
$requestedName,
$parameter->getName()
));
}
return $parameter->getDefaultValue();
}
$type = $this->aliases[$type] ?? $type;
if ($container->has($type)) {
return $container->get($type);
}
if (! $parameter->isOptional()) {
throw new ServiceNotFoundException(sprintf(
'Unable to create service "%s"; unable to resolve parameter "%s" using type hint "%s"',
$requestedName,
$parameter->getName(),
$type
));
}
// Type not available in container, but the value is optional and has a
// default defined.
return $parameter->getDefaultValue();
}
}

View File

@@ -1,53 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\ServiceManager;
/**
* Backwards-compatibility shim for AbstractFactoryInterface.
*
* Implementations should update to implement only Laminas\ServiceManager\Factory\AbstractFactoryInterface.
*
* If upgrading from v2, take the following steps:
*
* - rename the method `canCreateServiceWithName()` to `canCreate()`, and:
* - rename the `$serviceLocator` argument to `$container`, and change the
* typehint to `Psr\Container\ContainerInterface`
* - merge the `$name` and `$requestedName` arguments
* - rename the method `createServiceWithName()` to `__invoke()`, and:
* - rename the `$serviceLocator` argument to `$container`, and change the
* typehint to `Psr\Container\ContainerInterface`
* - merge the `$name` and `$requestedName` arguments
* - add the optional `array $options = null` argument.
* - create a `canCreateServiceWithName()` method as defined in this interface, and have it
* proxy to `canCreate()`, passing `$requestedName` as the second argument.
* - create a `createServiceWithName()` method as defined in this interface, and have it
* proxy to `__invoke()`, passing `$requestedName` as the second argument.
*
* Once you have tested your code, you can then update your class to only implement
* Laminas\ServiceManager\Factory\AbstractFactoryInterface, and remove the `canCreateServiceWithName()`
* and `createServiceWithName()` methods.
*
* @deprecated Use Laminas\ServiceManager\Factory\AbstractFactoryInterface instead.
*/
interface AbstractFactoryInterface extends Factory\AbstractFactoryInterface
{
/**
* Determine if we can create a service with name
*
* @param string $name
* @param string $requestedName
* @return bool
*/
public function canCreateServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName);
/**
* Create service with name
*
* @param string $name
* @param string $requestedName
* @return mixed
*/
public function createServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName);
}

View File

@@ -1,220 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\ServiceManager;
use Laminas\ServiceManager\Exception\ContainerModificationsNotAllowedException;
use Laminas\ServiceManager\Exception\InvalidServiceException;
use Psr\Container\ContainerInterface;
use function class_exists;
use function gettype;
use function is_object;
use function method_exists;
use function sprintf;
use function trigger_error;
use const E_USER_DEPRECATED;
/**
* Abstract plugin manager.
*
* Abstract PluginManagerInterface implementation providing:
*
* - creation context support. The constructor accepts the parent container
* instance, which is then used when creating instances.
* - plugin validation. Implementations may define the `$instanceOf` property
* to indicate what class types constitute valid plugins, omitting the
* requirement to define the `validate()` method.
*
* The implementation extends `ServiceManager`, thus providing the same set
* of capabilities as found in that implementation.
*
* @template InstanceType
* @implements PluginManagerInterface<InstanceType>
* @psalm-import-type ServiceManagerConfiguration from ServiceManager
* @psalm-suppress PropertyNotSetInConstructor
*/
abstract class AbstractPluginManager extends ServiceManager implements PluginManagerInterface
{
/**
* Whether or not to auto-add a FQCN as an invokable if it exists.
*
* @var bool
*/
protected $autoAddInvokableClass = true;
/**
* An object type that the created instance must be instanced of
*
* @var null|string
* @psalm-var null|class-string<InstanceType>
*/
protected $instanceOf;
/**
* Sets the provided $parentLocator as the creation context for all
* factories; for $config, {@see \Laminas\ServiceManager\ServiceManager::configure()}
* for details on its accepted structure.
*
* @param null|ConfigInterface|ContainerInterface $configInstanceOrParentLocator
* @param array $config
* @psalm-param ServiceManagerConfiguration $config
*/
public function __construct($configInstanceOrParentLocator = null, array $config = [])
{
/** @psalm-suppress DocblockTypeContradiction */
if (
null !== $configInstanceOrParentLocator
&& ! $configInstanceOrParentLocator instanceof ConfigInterface
&& ! $configInstanceOrParentLocator instanceof ContainerInterface
) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects a ConfigInterface or ContainerInterface instance as the first argument; received %s',
self::class,
is_object($configInstanceOrParentLocator)
? $configInstanceOrParentLocator::class
: gettype($configInstanceOrParentLocator)
));
}
if ($configInstanceOrParentLocator instanceof ConfigInterface) {
trigger_error(sprintf(
'Usage of %s as a constructor argument for %s is now deprecated',
ConfigInterface::class,
static::class
), E_USER_DEPRECATED);
$config = $configInstanceOrParentLocator->toArray();
}
parent::__construct($config);
if (! $configInstanceOrParentLocator instanceof ContainerInterface) {
trigger_error(sprintf(
'%s now expects a %s instance representing the parent container; please update your code',
__METHOD__,
ContainerInterface::class
), E_USER_DEPRECATED);
}
$this->creationContext = $configInstanceOrParentLocator instanceof ContainerInterface
? $configInstanceOrParentLocator
: $this;
}
/**
* Override configure() to validate service instances.
*
* @param array $config
* @psalm-param ServiceManagerConfiguration $config
* @return self
* @throws InvalidServiceException If an instance passed in the `services` configuration is invalid for the
* plugin manager.
* @throws ContainerModificationsNotAllowedException If the allow override flag has been toggled off, and a
* service instanceexists for a given service.
*/
public function configure(array $config)
{
if (isset($config['services'])) {
foreach ($config['services'] as $service) {
$this->validate($service);
}
}
parent::configure($config);
return $this;
}
/**
* Override setService for additional plugin validation.
*
* {@inheritDoc}
*
* @param string|class-string<InstanceType> $name
* @param InstanceType $service
*/
public function setService($name, $service)
{
$this->validate($service);
parent::setService($name, $service);
}
/**
* @param class-string<InstanceType>|string $name Service name of plugin to retrieve.
* @param null|array<mixed> $options Options to use when creating the instance.
* @return mixed
* @psalm-return ($name is class-string<InstanceType> ? InstanceType : mixed)
* @throws Exception\ServiceNotFoundException If the manager does not have
* a service definition for the instance, and the service is not
* auto-invokable.
* @throws InvalidServiceException If the plugin created is invalid for the
* plugin context.
*/
public function get($name, ?array $options = null)
{
if (! $this->has($name)) {
if (! $this->autoAddInvokableClass || ! class_exists($name)) {
throw new Exception\ServiceNotFoundException(sprintf(
'A plugin by the name "%s" was not found in the plugin manager %s',
$name,
static::class
));
}
$this->setFactory($name, Factory\InvokableFactory::class);
}
$instance = ! $options ? parent::get($name) : $this->build($name, $options);
$this->validate($instance);
return $instance;
}
/**
* {@inheritDoc}
*
* @psalm-assert InstanceType $instance
*/
public function validate(mixed $instance)
{
if (method_exists($this, 'validatePlugin')) {
trigger_error(sprintf(
'%s::validatePlugin() has been deprecated as of 3.0; please define validate() instead',
static::class
), E_USER_DEPRECATED);
$this->validatePlugin($instance);
return;
}
if (empty($this->instanceOf) || $instance instanceof $this->instanceOf) {
return;
}
throw new InvalidServiceException(sprintf(
'Plugin manager "%s" expected an instance of type "%s", but "%s" was received',
self::class,
$this->instanceOf,
is_object($instance) ? $instance::class : gettype($instance)
));
}
/**
* Implemented for backwards compatibility only.
*
* Returns the creation context.
*
* @deprecated since 3.0.0. The creation context should be passed during
* instantiation instead.
*
* @return void
*/
public function setServiceLocator(ContainerInterface $container)
{
trigger_error(sprintf(
'Usage of %s is deprecated since v3.0.0; please pass the container to the constructor instead',
__METHOD__
), E_USER_DEPRECATED);
$this->creationContext = $container;
}
}

View File

@@ -1,102 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\ServiceManager;
use Laminas\Stdlib\ArrayUtils;
use function array_keys;
/**
* Object for defining configuration and configuring an existing service manager instance.
*
* In order to provide configuration merging capabilities, this class implements
* the same functionality as `Laminas\Stdlib\ArrayUtils::merge()`. That routine
* allows developers to specifically shape how values are merged:
*
* - A value which is an instance of `MergeRemoveKey` indicates the value should
* be removed during merge.
* - A value that is an instance of `MergeReplaceKeyInterface` indicates that the
* value it contains should be used to replace any previous versions.
*
* These features are advanced, and not typically used. If you wish to use them,
* you will need to require the laminas-stdlib package in your application.
*
* @deprecated Class will be removed as of v4.0
*
* @psalm-import-type ServiceManagerConfigurationType from ConfigInterface
*/
class Config implements ConfigInterface
{
/** @var array<string,bool> */
private array $allowedKeys = [
'abstract_factories' => true,
'aliases' => true,
'delegators' => true,
'factories' => true,
'initializers' => true,
'invokables' => true,
'lazy_services' => true,
'services' => true,
'shared' => true,
];
/**
* @var array<string,array>
* @psalm-var ServiceManagerConfigurationType
*/
protected $config = [
'abstract_factories' => [],
'aliases' => [],
'delegators' => [],
'factories' => [],
'initializers' => [],
'invokables' => [],
'lazy_services' => [],
'services' => [],
'shared' => [],
];
/**
* @psalm-param ServiceManagerConfigurationType $config
*/
public function __construct(array $config = [])
{
// Only merge keys we're interested in
foreach (array_keys($config) as $key) {
if (! isset($this->allowedKeys[$key])) {
unset($config[$key]);
}
}
/** @psalm-suppress ArgumentTypeCoercion */
$this->config = $this->merge($this->config, $config);
}
/**
* @inheritDoc
*/
public function configureServiceManager(ServiceManager $serviceManager)
{
return $serviceManager->configure($this->config);
}
/**
* @inheritDoc
*/
public function toArray()
{
return $this->config;
}
/**
* @psalm-param ServiceManagerConfigurationType $a
* @psalm-param ServiceManagerConfigurationType $b
* @psalm-return ServiceManagerConfigurationType
*/
private function merge(array $a, array $b)
{
return ArrayUtils::merge($a, $b);
}
}

View File

@@ -1,93 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\ServiceManager;
use ArrayAccess;
use Psr\Container\ContainerInterface;
/**
* @deprecated Interface will be removed as of v4.0
*
* @see ContainerInterface
* @see ArrayAccess
*
* @psalm-type AbstractFactoriesConfigurationType = array<
* array-key,
* (class-string<Factory\AbstractFactoryInterface>|Factory\AbstractFactoryInterface)
* >
* @psalm-type DelegatorsConfigurationType = array<
* string,
* array<
* array-key,
* (class-string<Factory\DelegatorFactoryInterface>|Factory\DelegatorFactoryInterface)
* |callable(ContainerInterface,string,callable():object,array<mixed>|null):object
* >
* >
* @psalm-type FactoriesConfigurationType = array<
* string,
* (class-string<Factory\FactoryInterface>|Factory\FactoryInterface)
* |callable(ContainerInterface,?string,?array<mixed>|null):object
* >
* @psalm-type InitializersConfigurationType = array<
* array-key,
* (class-string<Initializer\InitializerInterface>|Initializer\InitializerInterface)
* |callable(ContainerInterface,object):void
* >
* @psalm-type LazyServicesConfigurationType = array{
* class_map?:array<string,class-string>,
* proxies_namespace?:non-empty-string,
* proxies_target_dir?:non-empty-string,
* write_proxy_files?:bool
* }
* @psalm-type ServiceManagerConfigurationType = array{
* abstract_factories?: AbstractFactoriesConfigurationType,
* aliases?: array<string,string>,
* delegators?: DelegatorsConfigurationType,
* factories?: FactoriesConfigurationType,
* initializers?: InitializersConfigurationType,
* invokables?: array<string,string>,
* lazy_services?: LazyServicesConfigurationType,
* services?: array<string,object|array>,
* shared?:array<string,bool>,
* ...
* }
*/
interface ConfigInterface
{
/**
* Configure a service manager.
*
* Implementations should pull configuration from somewhere (typically
* local properties) and pass it to a ServiceManager's withConfig() method,
* returning a new instance.
*
* @return ServiceManager
*/
public function configureServiceManager(ServiceManager $serviceManager);
/**
* Return configuration for a service manager instance as an array.
*
* Implementations MUST return an array compatible with ServiceManager::configure,
* containing one or more of the following keys:
*
* - abstract_factories
* - aliases
* - delegators
* - factories
* - initializers
* - invokables
* - lazy_services
* - services
* - shared
*
* In other words, this should return configuration that can be used to instantiate
* a service manager or plugin manager, or pass to its `withConfig()` method.
*
* @return array
* @psalm-return ServiceManagerConfigurationType
*/
public function toArray();
}

View File

@@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\ServiceManager;
/**
* Backwards-compatibility shim for DelegatorFactoryInterface.
*
* Implementations should update to implement only Laminas\ServiceManager\Factory\DelegatorFactoryInterface.
*
* If upgrading from v2, take the following steps:
*
* - rename the method `createDelegatorWithName()` to `__invoke()`, and:
* - rename the `$serviceLocator` argument to `$container`, and change the
* typehint to `Psr\Container\ContainerInterface`
* - merge the `$name` and `$requestedName` arguments
* - add the `callable` typehint to the `$callback` argument
* - add the optional `array $options = null` argument as a final argument
* - create a `createDelegatorWithName()` method as defined in this interface, and have it
* proxy to `__invoke()`, passing `$requestedName` as the second argument.
*
* Once you have tested your code, you can then update your class to only implement
* Laminas\ServiceManager\Factory\DelegatorFactoryInterface, and remove the `createDelegatorWithName()`
* method.
*
* @deprecated Use Laminas\ServiceManager\Factory\DelegatorFactoryInterface instead.
*/
interface DelegatorFactoryInterface extends Factory\DelegatorFactoryInterface
{
/**
* A factory that creates delegates of a given service
*
* @param ServiceLocatorInterface $serviceLocator the service locator which requested the service
* @param string $name the normalized service name
* @param string $requestedName the requested service name
* @param callable $callback the callback that is responsible for creating the service
* @return mixed
*/
public function createDelegatorWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName, $callback);
}

View File

@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\ServiceManager\Exception;
use DomainException;
use function sprintf;
class ContainerModificationsNotAllowedException extends DomainException implements ExceptionInterface
{
/**
* @param string $service Name of service that already exists.
*/
public static function fromExistingService(string $service): self
{
return new self(sprintf(
'The container does not allow replacing or updating a service'
. ' with existing instances; the following service'
. ' already exists in the container: %s',
$service
));
}
}

View File

@@ -1,152 +0,0 @@
<?php
declare(strict_types=1);
namespace Laminas\ServiceManager\Exception;
use function array_filter;
use function array_keys;
use function array_map;
use function array_values;
use function implode;
use function reset;
use function serialize;
use function sort;
use function sprintf;
class CyclicAliasException extends InvalidArgumentException
{
/**
* @param string $alias conflicting alias key
* @param string[] $aliases map of referenced services, indexed by alias name (string)
*/
public static function fromCyclicAlias(string $alias, array $aliases): self
{
$cycle = $alias;
$cursor = $alias;
while (isset($aliases[$cursor]) && $aliases[$cursor] !== $alias) {
$cursor = $aliases[$cursor];
$cycle .= ' -> ' . $cursor;
}
$cycle .= ' -> ' . $alias . "\n";
return new self(sprintf(
"A cycle was detected within the aliases definitions:\n%s",
$cycle
));
}
/**
* @param string[] $aliases map of referenced services, indexed by alias name (string)
* @return self
*/
public static function fromAliasesMap(array $aliases)
{
$detectedCycles = array_filter(array_map(
static fn($alias): ?array => self::getCycleFor($aliases, $alias),
array_keys($aliases)
));
if (! $detectedCycles) {
return new self(sprintf(
"A cycle was detected within the following aliases map:\n\n%s",
self::printReferencesMap($aliases)
));
}
return new self(sprintf(
"Cycles were detected within the provided aliases:\n\n%s\n\n"
. "The cycle was detected in the following alias map:\n\n%s",
self::printCycles(self::deDuplicateDetectedCycles($detectedCycles)),
self::printReferencesMap($aliases)
));
}
/**
* Retrieves the cycle detected for the given $alias, or `null` if no cycle was detected
*
* @param string[] $aliases
* @param string $alias
* @return array|null
*/
private static function getCycleFor(array $aliases, $alias)
{
$cycleCandidate = [];
$targetName = $alias;
while (isset($aliases[$targetName])) {
if (isset($cycleCandidate[$targetName])) {
return $cycleCandidate;
}
$cycleCandidate[$targetName] = true;
$targetName = $aliases[$targetName];
}
return null;
}
/**
* @param string[] $aliases
* @return string
*/
private static function printReferencesMap(array $aliases)
{
$map = [];
foreach ($aliases as $alias => $reference) {
$map[] = '"' . $alias . '" => "' . $reference . '"';
}
return "[\n" . implode("\n", $map) . "\n]";
}
/**
* @param string[][] $detectedCycles
* @return string
*/
private static function printCycles(array $detectedCycles)
{
return "[\n" . implode("\n", array_map([self::class, 'printCycle'], $detectedCycles)) . "\n]";
}
/**
* @param string[] $detectedCycle
* @return string
* @phpcsSuppress SlevomatCodingStandard.Classes.UnusedPrivateElements.UnusedMethod
*/
private static function printCycle(array $detectedCycle)
{
$fullCycle = array_keys($detectedCycle);
$fullCycle[] = reset($fullCycle);
return implode(
' => ',
array_map(
static fn($cycle): string => '"' . $cycle . '"',
$fullCycle
)
);
}
/**
* @param bool[][] $detectedCycles
* @return bool[][] de-duplicated
*/
private static function deDuplicateDetectedCycles(array $detectedCycles)
{
$detectedCyclesByHash = [];
foreach ($detectedCycles as $detectedCycle) {
$cycleAliases = array_keys($detectedCycle);
sort($cycleAliases);
$hash = serialize($cycleAliases);
$detectedCyclesByHash[$hash] ??= $detectedCycle;
}
return array_values($detectedCyclesByHash);
}
}

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