Allow PHP-8.2 and up Compatibility instead of just PHP-8.4

This commit is contained in:
johnnyq
2026-06-12 17:06:10 -04:00
parent 2204bd52f4
commit d3a93652f3
220 changed files with 7198 additions and 2635 deletions

View File

@@ -54,6 +54,10 @@ final class MockClock implements ClockInterface
public function sleep(float|int $seconds): void
{
if (0 >= $seconds) {
return;
}
$now = (float) $this->now->format('Uu') + $seconds * 1e6;
$now = substr_replace(\sprintf('@%07.0F', $now), '.', -6, 0);
$timezone = $this->now->getTimezone();

View File

@@ -25,7 +25,7 @@
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-main": "3.6-dev"
"dev-main": "3.7-dev"
},
"thanks": {
"name": "symfony/contracts",

View File

@@ -25,7 +25,7 @@ class_exists(AcceptHeaderItem::class);
class AcceptHeader
{
/**
* @var AcceptHeaderItem[]
* @var array<string, AcceptHeaderItem>
*/
private array $items = [];
@@ -46,18 +46,15 @@ class AcceptHeader
*/
public static function fromString(?string $headerValue): self
{
$parts = HeaderUtils::split($headerValue ?? '', ',;=');
$items = [];
foreach (HeaderUtils::split($headerValue ?? '', ',;=') as $i => $parts) {
$part = array_shift($parts);
$item = new AcceptHeaderItem($part[0], HeaderUtils::combine($parts));
return new self(array_map(function ($subParts) {
static $index = 0;
$part = array_shift($subParts);
$attributes = HeaderUtils::combine($subParts);
$items[] = $item->setIndex($i);
}
$item = new AcceptHeaderItem($part[0], $attributes);
$item->setIndex($index++);
return $item;
}, $parts));
return new self($items);
}
/**
@@ -73,7 +70,9 @@ class AcceptHeader
*/
public function has(string $value): bool
{
return isset($this->items[$value]);
$canonicalKey = $this->getCanonicalKey(AcceptHeaderItem::fromString($value));
return isset($this->items[$canonicalKey]);
}
/**
@@ -81,7 +80,26 @@ class AcceptHeader
*/
public function get(string $value): ?AcceptHeaderItem
{
return $this->items[$value] ?? $this->items[explode('/', $value)[0].'/*'] ?? $this->items['*/*'] ?? $this->items['*'] ?? null;
$queryItem = AcceptHeaderItem::fromString($value.';q=1');
$canonicalKey = $this->getCanonicalKey($queryItem);
if (isset($this->items[$canonicalKey])) {
return $this->items[$canonicalKey];
}
// Collect and filter matching candidates
if (!$candidates = array_filter($this->items, fn (AcceptHeaderItem $item) => $this->matches($item, $queryItem))) {
return null;
}
usort(
$candidates,
fn ($a, $b) => $this->getSpecificity($b, $queryItem) <=> $this->getSpecificity($a, $queryItem) // Descending specificity
?: $b->getQuality() <=> $a->getQuality() // Descending quality
?: $a->getIndex() <=> $b->getIndex() // Ascending index (stability)
);
return reset($candidates);
}
/**
@@ -91,7 +109,7 @@ class AcceptHeader
*/
public function add(AcceptHeaderItem $item): static
{
$this->items[$item->getValue()] = $item;
$this->items[$this->getCanonicalKey($item)] = $item;
$this->sorted = false;
return $this;
@@ -114,7 +132,7 @@ class AcceptHeader
*/
public function filter(string $pattern): self
{
return new self(array_filter($this->items, fn (AcceptHeaderItem $item) => preg_match($pattern, $item->getValue())));
return new self(array_filter($this->items, static fn ($item) => preg_match($pattern, $item->getValue())));
}
/**
@@ -133,18 +151,154 @@ class AcceptHeader
private function sort(): void
{
if (!$this->sorted) {
uasort($this->items, function (AcceptHeaderItem $a, AcceptHeaderItem $b) {
$qA = $a->getQuality();
$qB = $b->getQuality();
if ($qA === $qB) {
return $a->getIndex() > $b->getIndex() ? 1 : -1;
}
return $qA > $qB ? -1 : 1;
});
uasort($this->items, static fn ($a, $b) => $b->getQuality() <=> $a->getQuality() ?: $a->getIndex() <=> $b->getIndex());
$this->sorted = true;
}
}
/**
* Generates the canonical key for storing/retrieving an item.
*/
private function getCanonicalKey(AcceptHeaderItem $item): string
{
$parts = [];
// Normalize and sort attributes for consistent key generation
$attributes = $this->getMediaParams($item);
ksort($attributes);
foreach ($attributes as $name => $value) {
if (null === $value) {
$parts[] = $name; // Flag parameter (e.g., "flowed")
continue;
}
// Quote values containing spaces, commas, semicolons, or equals per RFC 9110
// This handles cases like 'format="value with space"' or similar.
$quotedValue = \is_string($value) && preg_match('/[\s;,=]/', $value) ? '"'.addcslashes($value, '"\\').'"' : $value;
$parts[] = $name.'='.$quotedValue;
}
return $item->getValue().($parts ? ';'.implode(';', $parts) : '');
}
/**
* Checks if a given header item (range) matches a queried item (value).
*
* @param AcceptHeaderItem $rangeItem The item from the Accept header (e.g., text/*;format=flowed)
* @param AcceptHeaderItem $queryItem The item being queried (e.g., text/plain;format=flowed;charset=utf-8)
*/
private function matches(AcceptHeaderItem $rangeItem, AcceptHeaderItem $queryItem): bool
{
$rangeValue = strtolower($rangeItem->getValue());
$queryValue = strtolower($queryItem->getValue());
// Handle universal wildcard ranges
if ('*' === $rangeValue || '*/*' === $rangeValue) {
return $this->rangeParametersMatch($rangeItem, $queryItem);
}
// Queries for '*' only match wildcard ranges (handled above)
if ('*' === $queryValue) {
return false;
}
// Ensure media vs. non-media consistency
$isQueryMedia = str_contains($queryValue, '/');
$isRangeMedia = str_contains($rangeValue, '/');
if ($isQueryMedia !== $isRangeMedia) {
return false;
}
// Non-media: exact match only (wildcards handled above)
if (!$isQueryMedia) {
return $rangeValue === $queryValue && $this->rangeParametersMatch($rangeItem, $queryItem);
}
// Media type: type/subtype with wildcards
[$queryType, $querySubtype] = explode('/', $queryValue, 2);
[$rangeType, $rangeSubtype] = explode('/', $rangeValue, 2) + [1 => '*'];
if ('*' !== $rangeType && $rangeType !== $queryType) {
return false;
}
if ('*' !== $rangeSubtype && $rangeSubtype !== $querySubtype) {
return false;
}
// Parameters must match
return $this->rangeParametersMatch($rangeItem, $queryItem);
}
/**
* Checks if the parameters of a range item are satisfied by the query item.
*
* Parameters are case-insensitive; range params must be a subset of query params.
*/
private function rangeParametersMatch(AcceptHeaderItem $rangeItem, AcceptHeaderItem $queryItem): bool
{
$queryAttributes = $this->getMediaParams($queryItem);
$rangeAttributes = $this->getMediaParams($rangeItem);
foreach ($rangeAttributes as $name => $rangeValue) {
if (!\array_key_exists($name, $queryAttributes)) {
return false; // Missing required param
}
$queryValue = $queryAttributes[$name];
if (null === $rangeValue) {
return null === $queryValue; // Both flags or neither
}
if (null === $queryValue || strtolower($queryValue) !== strtolower($rangeValue)) {
return false;
}
}
return true;
}
/**
* Calculates a specificity score for sorting: media precision + param count.
*/
private function getSpecificity(AcceptHeaderItem $item, AcceptHeaderItem $queryItem): int
{
$rangeValue = strtolower($item->getValue());
$queryValue = strtolower($queryItem->getValue());
$paramCount = \count($this->getMediaParams($item));
$isQueryMedia = str_contains($queryValue, '/');
$isRangeMedia = str_contains($rangeValue, '/');
if (!$isQueryMedia && !$isRangeMedia) {
return ('*' !== $rangeValue ? 2000 : 1000) + $paramCount;
}
[$rangeType, $rangeSubtype] = explode('/', $rangeValue, 2) + [1 => '*'];
$specificity = match (true) {
'*' !== $rangeSubtype => 3000, // Exact subtype (text/plain)
'*' !== $rangeType => 2000, // Type wildcard (text/*)
default => 1000, // Full wildcard (*/* or *)
};
return $specificity + $paramCount;
}
/**
* Returns normalized attributes: keys lowercased, excluding 'q'.
*/
private function getMediaParams(AcceptHeaderItem $item): array
{
$attributes = array_change_key_case($item->getAttributes(), \CASE_LOWER);
unset($attributes['q']);
return $attributes;
}
}

View File

@@ -268,7 +268,7 @@ class BinaryFileResponse extends Response
if ($start < 0 || $start > $end) {
$this->setStatusCode(416);
$this->headers->set('Content-Range', \sprintf('bytes */%s', $fileSize));
} elseif ($end - $start < $fileSize - 1) {
} else {
$this->maxlen = $end < $fileSize ? $end - $start + 1 : -1;
$this->offset = $start;

View File

@@ -1,6 +1,19 @@
CHANGELOG
=========
7.4
---
* Add `#[WithHttpStatus]` to define status codes: 404 for `SignedUriException` and 403 for `ExpiredSignedUriException`
* Add support for the `QUERY` HTTP method
* Add support for structured MIME suffix
* Add `Request::set/getAllowedHttpMethodOverride()` to list which HTTP methods can be overridden
* Deprecate using `Request::sendHeaders()` after headers have already been sent; use a `StreamedResponse` instead
* Deprecate method `Request::get()`, use properties `->attributes`, `query` or `request` directly instead
* Make `Request::createFromGlobals()` parse the body of PUT, DELETE, PATCH and QUERY requests
* Deprecate HTTP method override for methods GET, HEAD, CONNECT and TRACE; it will be ignored in Symfony 8.0
* Deprecate accepting null `$format` argument to `Request::setFormat()`
7.3
---

View File

@@ -48,7 +48,7 @@ class EventStreamResponse extends StreamedResponse
'Cache-Control' => 'private, no-cache, no-store, must-revalidate, max-age=0',
'X-Accel-Buffering' => 'no',
'Pragma' => 'no-cache',
'Expire' => '0',
'Expires' => '0',
];
parent::__construct($callback, $status, $headers);

View File

@@ -11,9 +11,12 @@
namespace Symfony\Component\HttpFoundation\Exception;
use Symfony\Component\HttpKernel\Attribute\WithHttpStatus;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
#[WithHttpStatus(403)]
final class ExpiredSignedUriException extends SignedUriException
{
/**

View File

@@ -11,9 +11,12 @@
namespace Symfony\Component\HttpFoundation\Exception;
use Symfony\Component\HttpKernel\Attribute\WithHttpStatus;
/**
* @author Kevin Bond <kevinbond@gmail.com>
*/
#[WithHttpStatus(404)]
abstract class SignedUriException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -86,7 +86,7 @@ class File extends \SplFileInfo
{
$target = $this->getTargetFile($directory, $name);
set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
set_error_handler(static function ($type, $msg) use (&$error) { $error = $msg; });
try {
$renamed = rename($this->getPathname(), $target);
} finally {
@@ -96,7 +96,7 @@ class File extends \SplFileInfo
throw new FileException(\sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error)));
}
@chmod($target, 0666 & ~umask());
@chmod($target, 0o666 & ~umask());
return $target;
}
@@ -114,10 +114,11 @@ class File extends \SplFileInfo
protected function getTargetFile(string $directory, ?string $name = null): self
{
if (!is_dir($directory)) {
if (false === @mkdir($directory, 0777, true) && !is_dir($directory)) {
throw new FileException(\sprintf('Unable to create the "%s" directory.', $directory));
if (!is_dir($directory) && !@mkdir($directory, 0o777, true) && !is_dir($directory)) {
if (is_file($directory)) {
throw new FileException(\sprintf('Unable to create the "%s" directory: a similarly-named file exists.', $directory));
}
throw new FileException(\sprintf('Unable to create the "%s" directory.', $directory));
} elseif (!is_writable($directory)) {
throw new FileException(\sprintf('Unable to write in the "%s" directory.', $directory));
}

View File

@@ -187,7 +187,7 @@ class UploadedFile extends File
$target = $this->getTargetFile($directory, $name);
set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
set_error_handler(static function ($type, $msg) use (&$error) { $error = $msg; });
try {
$moved = move_uploaded_file($this->getPathname(), $target);
} finally {
@@ -197,29 +197,37 @@ class UploadedFile extends File
throw new FileException(\sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error)));
}
@chmod($target, 0666 & ~umask());
@chmod($target, 0o666 & ~umask());
return $target;
}
switch ($this->error) {
case \UPLOAD_ERR_INI_SIZE:
throw new IniSizeFileException($this->getErrorMessage());
throw new IniSizeFileException($this->getExceptionMessage());
case \UPLOAD_ERR_FORM_SIZE:
throw new FormSizeFileException($this->getErrorMessage());
throw new FormSizeFileException($this->getExceptionMessage());
case \UPLOAD_ERR_PARTIAL:
throw new PartialFileException($this->getErrorMessage());
throw new PartialFileException($this->getExceptionMessage());
case \UPLOAD_ERR_NO_FILE:
throw new NoFileException($this->getErrorMessage());
throw new NoFileException($this->getExceptionMessage());
case \UPLOAD_ERR_CANT_WRITE:
throw new CannotWriteFileException($this->getErrorMessage());
throw new CannotWriteFileException($this->getExceptionMessage());
case \UPLOAD_ERR_NO_TMP_DIR:
throw new NoTmpDirFileException($this->getErrorMessage());
throw new NoTmpDirFileException($this->getExceptionMessage());
case \UPLOAD_ERR_EXTENSION:
throw new ExtensionFileException($this->getErrorMessage());
throw new ExtensionFileException($this->getExceptionMessage());
}
throw new FileException($this->getErrorMessage());
throw new FileException($this->getExceptionMessage());
}
/**
* Retrieves a user-friendly error message for file upload issues, if any.
*/
public function getErrorMessage(): string
{
return \UPLOAD_ERR_OK !== $this->error ? $this->getExceptionMessage() : '';
}
/**
@@ -268,7 +276,7 @@ class UploadedFile extends File
/**
* Returns an informative upload error message.
*/
public function getErrorMessage(): string
private function getExceptionMessage(): string
{
static $errors = [
\UPLOAD_ERR_INI_SIZE => 'The file "%s" exceeds your upload_max_filesize ini directive (limit is %d KiB).',

View File

@@ -164,7 +164,7 @@ class HeaderUtils
*/
public static function makeDisposition(string $disposition, string $filename, string $filenameFallback = ''): string
{
if (!\in_array($disposition, [self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE])) {
if (!\in_array($disposition, [self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE], true)) {
throw new \InvalidArgumentException(\sprintf('The disposition must be either "%s" or "%s".', self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE));
}

View File

@@ -17,6 +17,8 @@ use Symfony\Component\HttpFoundation\Exception\UnexpectedValueException;
/**
* InputBag is a container for user input values such as $_GET, $_POST, $_REQUEST, and $_COOKIE.
*
* @template TInput of string|int|float|bool|null
*
* @author Saif Eddin Gmati <azjezz@protonmail.com>
*/
final class InputBag extends ParameterBag
@@ -24,7 +26,11 @@ final class InputBag extends ParameterBag
/**
* Returns a scalar input value by name.
*
* @param string|int|float|bool|null $default The default value if the input key does not exist
* @template TDefault of string|int|float|bool|null
*
* @param TDefault $default The default value if the input key does not exist
*
* @return TDefault|TInput
*
* @throws BadRequestException if the input contains a non-scalar value
*/

View File

@@ -29,8 +29,13 @@ class IpUtils
'::1/128', // Loopback
'fc00::/7', // Unique Local Address
'fe80::/10', // Link Local Address
'::ffff:0:0/96', // IPv4 translations
'::ffff:0:0/96', // IPv4-mapped IPv6 addresses (RFC 4291 section 2.5.5.2)
'::/128', // Unspecified address
'::/96', // IPv4-compatible IPv6 addresses (RFC 4291 section 2.5.5.1)
'2002::/16', // 6to4 (RFC 3056)
'2001::/32', // Teredo tunneling (RFC 4380)
'64:ff9b::/96', // NAT64 well-known prefix (RFC 6052)
'64:ff9b:1::/48', // NAT64 local-use prefix (RFC 8215)
];
private static array $checkedIps = [];
@@ -212,7 +217,7 @@ class IpUtils
$ip = substr($ip, 1, -1);
}
$mappedIpV4MaskGenerator = function (string $mask, int $bytesToAnonymize) {
$mappedIpV4MaskGenerator = static function (string $mask, int $bytesToAnonymize) {
$mask .= str_repeat('ff', 4 - $bytesToAnonymize);
$mask .= str_repeat('00', $bytesToAnonymize);

View File

@@ -23,6 +23,9 @@ use Symfony\Component\HttpFoundation\Exception\UnexpectedValueException;
*/
class ParameterBag implements \IteratorAggregate, \Countable
{
/**
* @param array<string, mixed> $parameters
*/
public function __construct(
protected array $parameters = [],
) {
@@ -31,7 +34,11 @@ class ParameterBag implements \IteratorAggregate, \Countable
/**
* Returns the parameters.
*
* @param string|null $key The name of the parameter to return or null to get them all
* @template TKey of string|null
*
* @param TKey $key The name of the parameter to return or null to get them all
*
* @return (TKey is null ? array<string, mixed> : array<mixed>)
*
* @throws BadRequestException if the value is not an array
*/
@@ -50,6 +57,8 @@ class ParameterBag implements \IteratorAggregate, \Countable
/**
* Returns the parameter keys.
*
* @return list<string>
*/
public function keys(): array
{
@@ -58,6 +67,8 @@ class ParameterBag implements \IteratorAggregate, \Countable
/**
* Replaces the current parameters by a new set.
*
* @param array<string, mixed> $parameters
*/
public function replace(array $parameters = []): void
{
@@ -66,6 +77,8 @@ class ParameterBag implements \IteratorAggregate, \Countable
/**
* Adds parameters.
*
* @param array<string, mixed> $parameters
*/
public function add(array $parameters = []): void
{
@@ -188,7 +201,7 @@ class ParameterBag implements \IteratorAggregate, \Countable
try {
return $class::from($value);
} catch (\ValueError|\TypeError $e) {
throw new UnexpectedValueException(\sprintf('Parameter "%s" cannot be converted to enum: %s.', $key, $e->getMessage()), $e->getCode(), $e);
throw new UnexpectedValueException(\sprintf('Parameter "%s" cannot be converted to enum: ', $key).$e->getMessage().'.', $e->getCode(), $e);
}
}

View File

@@ -62,6 +62,7 @@ class Request
public const METHOD_OPTIONS = 'OPTIONS';
public const METHOD_TRACE = 'TRACE';
public const METHOD_CONNECT = 'CONNECT';
public const METHOD_QUERY = 'QUERY';
/**
* @var string[]
@@ -80,6 +81,13 @@ class Request
protected static bool $httpMethodParameterOverride = false;
/**
* The HTTP methods that can be overridden.
*
* @var uppercase-string[]|null
*/
protected static ?array $allowedHttpMethodOverride = null;
/**
* Custom parameters.
*/
@@ -94,6 +102,8 @@ class Request
/**
* Query string parameters ($_GET).
*
* @var InputBag<string>
*/
public InputBag $query;
@@ -109,6 +119,8 @@ class Request
/**
* Cookies ($_COOKIE).
*
* @var InputBag<string>
*/
public InputBag $cookies;
@@ -194,6 +206,28 @@ class Request
self::HEADER_X_FORWARDED_PREFIX => 'X_FORWARDED_PREFIX',
];
/**
* This mapping is used when no exact MIME match is found in $formats.
*
* It enables mappings like application/soap+xml -> xml.
*
* @see https://datatracker.ietf.org/doc/html/rfc6839
* @see https://datatracker.ietf.org/doc/html/rfc7303
* @see https://www.iana.org/assignments/media-types/media-types.xhtml
*/
private const STRUCTURED_SUFFIX_FORMATS = [
'json' => 'json',
'xml' => 'xml',
'xhtml' => 'html',
'cbor' => 'cbor',
'zip' => 'zip',
'ber' => 'asn1',
'der' => 'asn1',
'tlv' => 'tlv',
'wbxml' => 'xml',
'yaml' => 'yaml',
];
private bool $isIisRewrite = false;
/**
@@ -251,16 +285,30 @@ class Request
*/
public static function createFromGlobals(): static
{
$request = self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER);
if (str_starts_with($request->headers->get('CONTENT_TYPE', ''), 'application/x-www-form-urlencoded')
&& \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH'], true)
) {
parse_str($request->getContent(), $data);
$request->request = new InputBag($data);
if (!\in_array($_SERVER['REQUEST_METHOD'] ?? null, ['PUT', 'DELETE', 'PATCH', 'QUERY'], true)) {
return self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER);
}
return $request;
if (\PHP_VERSION_ID < 80400) {
if (!isset($_SERVER['CONTENT_TYPE']) || str_starts_with($_SERVER['CONTENT_TYPE'], 'application/x-www-form-urlencoded')) {
$content = file_get_contents('php://input');
parse_str($content, $post);
} else {
$content = null;
$post = $_POST;
}
return self::createRequestFromFactory($_GET, $post, [], $_COOKIE, $_FILES, $_SERVER, $content);
}
try {
[$post, $files] = request_parse_body();
} catch (\RequestParseBodyException) {
$post = $_POST;
$files = $_FILES;
}
return self::createRequestFromFactory($_GET, $post, [], $_COOKIE, $files, $_SERVER);
}
/**
@@ -353,14 +401,23 @@ class Request
$server['PHP_AUTH_PW'] = $components['pass'];
}
if (!isset($components['path'])) {
if ('' === $path = $components['path'] ?? '') {
$components['path'] = '/';
} elseif (!isset($components['scheme']) && !isset($components['host']) && '/' !== $path[0]) {
if (false !== $pos = strpos($path, '/')) {
$path = substr($path, 0, $pos);
}
if (str_contains($path, ':')) {
throw new BadRequestException('Invalid URI: Path is malformed.');
}
}
switch (strtoupper($method)) {
case 'POST':
case 'PUT':
case 'DELETE':
case 'QUERY':
if (!isset($server['CONTENT_TYPE'])) {
$server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded';
}
@@ -451,8 +508,8 @@ class Request
$dup->method = null;
$dup->format = null;
if (!$dup->get('_format') && $this->get('_format')) {
$dup->attributes->set('_format', $this->get('_format'));
if (!$dup->attributes->has('_format') && $this->attributes->has('_format')) {
$dup->attributes->set('_format', $this->attributes->get('_format'));
}
if (!$dup->getRequestFormat(null)) {
@@ -596,7 +653,7 @@ class Request
*/
public static function setTrustedHosts(array $hostPatterns): void
{
self::$trustedHostPatterns = array_map(fn ($hostPattern) => \sprintf('{%s}i', $hostPattern), $hostPatterns);
self::$trustedHostPatterns = array_map(static fn ($hostPattern) => \sprintf('{%s}i', $hostPattern), $hostPatterns);
// we need to reset trusted hosts on trusted host patterns change
self::$trustedHosts = [];
}
@@ -653,6 +710,34 @@ class Request
return self::$httpMethodParameterOverride;
}
/**
* Sets the list of HTTP methods that can be overridden.
*
* Set to null to allow all methods to be overridden (default). Set to an
* empty array to disallow overrides entirely. Otherwise, provide the list
* of uppercased method names that are allowed.
*
* @param uppercase-string[]|null $methods
*/
public static function setAllowedHttpMethodOverride(?array $methods): void
{
if (array_intersect($methods ?? [], ['GET', 'HEAD', 'CONNECT', 'TRACE'])) {
throw new \InvalidArgumentException('The HTTP methods "GET", "HEAD", "CONNECT", and "TRACE" cannot be overridden.');
}
self::$allowedHttpMethodOverride = $methods;
}
/**
* Gets the list of HTTP methods that can be overridden.
*
* @return uppercase-string[]|null
*/
public static function getAllowedHttpMethodOverride(): ?array
{
return self::$allowedHttpMethodOverride;
}
/**
* Gets a "parameter" value from any bag.
*
@@ -662,10 +747,12 @@ class Request
*
* Order of precedence: PATH (routing placeholders or custom attributes), GET, POST
*
* @internal use explicit input sources instead
* @deprecated since Symfony 7.4, use properties `->attributes`, `query` or `request` directly instead
*/
public function get(string $key, mixed $default = null): mixed
{
trigger_deprecation('symfony/http-foundation', '7.4', 'Request::get() is deprecated, use properties ->attributes, query or request directly instead.');
if ($this !== $result = $this->attributes->get($key, $this)) {
return $result;
}
@@ -770,10 +857,6 @@ class Request
* being the original client, and each successive proxy that passed the request
* adding the IP address where it received the request from.
*
* If your reverse proxy uses a different header name than "X-Forwarded-For",
* ("Client-Ip" for instance), configure it via the $trustedHeaderSet
* argument of the Request::setTrustedProxies() method instead.
*
* @see getClientIps()
* @see https://wikipedia.org/wiki/X-Forwarded-For
*/
@@ -797,7 +880,7 @@ class Request
*
* Suppose this request is instantiated from /mysite on localhost:
*
* * http://localhost/mysite returns an empty string
* * http://localhost/mysite returns '/'
* * http://localhost/mysite/about returns '/about'
* * http://localhost/mysite/enco%20ded returns '/enco%20ded'
* * http://localhost/mysite/about?var=1 returns '/about'
@@ -1075,7 +1158,7 @@ class Request
$https = $this->server->get('HTTPS');
return $https && 'off' !== strtolower($https);
return $https && (!\is_string($https) || 'off' !== strtolower($https));
}
/**
@@ -1092,10 +1175,8 @@ class Request
{
if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_X_FORWARDED_HOST)) {
$host = $host[0];
} elseif (!$host = $this->headers->get('HOST')) {
if (!$host = $this->server->get('SERVER_NAME')) {
$host = $this->server->get('SERVER_ADDR', '');
}
} else {
$host = $this->headers->get('HOST') ?: $this->server->get('SERVER_NAME') ?: $this->server->get('SERVER_ADDR', '');
}
// trim and remove port number from host
@@ -1168,7 +1249,7 @@ class Request
$this->method = strtoupper($this->server->get('REQUEST_METHOD', 'GET'));
if ('POST' !== $this->method) {
if ('POST' !== $this->method || !(self::$allowedHttpMethodOverride ?? true)) {
return $this->method;
}
@@ -1184,11 +1265,15 @@ class Request
$method = strtoupper($method);
if (\in_array($method, ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'PATCH', 'PURGE', 'TRACE'], true)) {
return $this->method = $method;
if (\in_array($method, ['GET', 'HEAD', 'CONNECT', 'TRACE'], true)) {
trigger_deprecation('symfony/http-foundation', '7.4', 'HTTP method override is deprecated for methods GET, HEAD, CONNECT and TRACE; it will be ignored in Symfony 8.0.', $method);
}
if (!preg_match('/^[A-Z]++$/D', $method)) {
if (self::$allowedHttpMethodOverride && !\in_array($method, self::$allowedHttpMethodOverride, true)) {
return $this->method;
}
if (\strlen($method) !== strspn($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')) {
throw new SuspiciousOperationException('Invalid HTTP method override.');
}
@@ -1233,9 +1318,22 @@ class Request
/**
* Gets the format associated with the mime type.
*
* Resolution order:
* 1) Exact match on the full MIME type (e.g. "application/json").
* 2) Match on the canonical MIME type (i.e. before the first ";" parameter).
* 3) If the type is "application/*+suffix", use the structured syntax suffix
* mapping (e.g. "application/foo+json" → "json"), when available.
* 4) If $subtypeFallback is true and no match was found:
* - return the MIME subtype (without "x-" prefix), provided it does not
* contain a "+" (e.g. "application/x-yaml" → "yaml", "text/csv" → "csv").
*
* @param string|null $mimeType The mime type to check
* @param bool $subtypeFallback Whether to fall back to the subtype if no exact match is found
*/
public function getFormat(?string $mimeType): ?string
public function getFormat(?string $mimeType/* , bool $subtypeFallback = false */): ?string
{
$subtypeFallback = 2 <= \func_num_args() ? func_get_arg(1) : false;
$canonicalMimeType = null;
if ($mimeType && false !== $pos = strpos($mimeType, ';')) {
$canonicalMimeType = trim(substr($mimeType, 0, $pos));
@@ -1261,21 +1359,48 @@ class Request
return $format;
}
if (!$canonicalMimeType ??= $mimeType) {
return null;
}
if (str_starts_with($canonicalMimeType, 'application/') && str_contains($canonicalMimeType, '+')) {
$suffix = substr(strrchr($canonicalMimeType, '+'), 1);
if (isset(self::STRUCTURED_SUFFIX_FORMATS[$suffix])) {
return self::STRUCTURED_SUFFIX_FORMATS[$suffix];
}
}
if ($subtypeFallback && str_contains($canonicalMimeType, '/')) {
[, $subtype] = explode('/', $canonicalMimeType, 2);
if (str_starts_with($subtype, 'x-')) {
$subtype = substr($subtype, 2);
}
if (!str_contains($subtype, '+')) {
return $subtype;
}
}
return null;
}
/**
* Associates a format with mime types.
*
* @param string $format The format to set
* @param string|string[] $mimeTypes The associated mime types (the preferred one must be the first as it will be used as the content type)
*/
public function setFormat(?string $format, string|array $mimeTypes): void
{
if (null === $format) {
trigger_deprecation('symfony/http-foundation', '7.4', 'Passing "null" as the first argument of "%s()" is deprecated. The argument will be non-nullable in Symfony 8.0.', __METHOD__);
$format = '';
}
if (null === static::$formats) {
static::initializeFormats();
}
static::$formats[$format ?? ''] = (array) $mimeTypes;
static::$formats[$format] = (array) $mimeTypes;
}
/**
@@ -1367,7 +1492,7 @@ class Request
*/
public function isMethodSafe(): bool
{
return \in_array($this->getMethod(), ['GET', 'HEAD', 'OPTIONS', 'TRACE']);
return \in_array($this->getMethod(), ['GET', 'HEAD', 'OPTIONS', 'TRACE', 'QUERY'], true);
}
/**
@@ -1375,7 +1500,7 @@ class Request
*/
public function isMethodIdempotent(): bool
{
return \in_array($this->getMethod(), ['HEAD', 'GET', 'PUT', 'DELETE', 'TRACE', 'OPTIONS', 'PURGE']);
return \in_array($this->getMethod(), ['HEAD', 'GET', 'PUT', 'DELETE', 'TRACE', 'OPTIONS', 'PURGE', 'QUERY'], true);
}
/**
@@ -1385,7 +1510,7 @@ class Request
*/
public function isMethodCacheable(): bool
{
return \in_array($this->getMethod(), ['GET', 'HEAD']);
return \in_array($this->getMethod(), ['GET', 'HEAD', 'QUERY'], true);
}
/**
@@ -1421,10 +1546,8 @@ class Request
*/
public function getContent(bool $asResource = false)
{
$currentContentIsResource = \is_resource($this->content);
if (true === $asResource) {
if ($currentContentIsResource) {
if ($asResource) {
if (\is_resource($this->content)) {
rewind($this->content);
return $this->content;
@@ -1444,7 +1567,7 @@ class Request
return fopen('php://input', 'r');
}
if ($currentContentIsResource) {
if (\is_resource($this->content)) {
rewind($this->content);
return stream_get_contents($this->content);
@@ -1932,6 +2055,14 @@ class Request
'atom' => ['application/atom+xml'],
'rss' => ['application/rss+xml'],
'form' => ['application/x-www-form-urlencoded', 'multipart/form-data'],
'soap' => ['application/soap+xml'],
'problem' => ['application/problem+json'],
'hal' => ['application/hal+json', 'application/hal+xml'],
'jsonapi' => ['application/vnd.api+json'],
'yaml' => ['text/yaml', 'application/x-yaml'],
'wbxml' => ['application/vnd.wap.wbxml'],
'pdf' => ['application/pdf'],
'csv' => ['text/csv'],
];
}

View File

@@ -261,7 +261,7 @@ class Response
}
// Fix Content-Type
$charset = $this->charset ?: 'UTF-8';
$charset = $this->charset ?: 'utf-8';
if (!$headers->has('Content-Type')) {
$headers->set('Content-Type', 'text/html; charset='.$charset);
} elseif (0 === stripos($headers->get('Content-Type') ?? '', 'text/') && false === stripos($headers->get('Content-Type') ?? '', 'charset')) {
@@ -317,6 +317,12 @@ class Response
{
// headers have already been sent by the developer
if (headers_sent()) {
if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
$statusCode ??= $this->statusCode;
trigger_deprecation('symfony/http-foundation', '7.4', 'Trying to use "%s::sendHeaders()" after headers have already been sent is deprecated and will throw a PHP warning in 8.0. Use a "StreamedResponse" instead.', static::class);
// header(\sprintf('HTTP/%s %s %s', $this->version, $statusCode, $this->statusText), true, $statusCode);
}
return $this;
}
@@ -539,7 +545,7 @@ class Response
*/
public function isCacheable(): bool
{
if (!\in_array($this->statusCode, [200, 203, 300, 301, 302, 404, 410])) {
if (!\in_array($this->statusCode, [200, 203, 300, 301, 302, 404, 410], true)) {
return false;
}
@@ -1248,7 +1254,7 @@ class Response
*/
public function isRedirect(?string $location = null): bool
{
return \in_array($this->statusCode, [201, 301, 302, 303, 307, 308]) && (null === $location ?: $location == $this->headers->get('Location'));
return \in_array($this->statusCode, [201, 301, 302, 303, 307, 308], true) && (null === $location ?: $location == $this->headers->get('Location'));
}
/**
@@ -1258,7 +1264,7 @@ class Response
*/
public function isEmpty(): bool
{
return \in_array($this->statusCode, [204, 304]);
return \in_array($this->statusCode, [204, 304], true);
}
/**

View File

@@ -194,7 +194,7 @@ class ResponseHeaderBag extends HeaderBag
*/
public function getCookies(string $format = self::COOKIES_FLAT): array
{
if (!\in_array($format, [self::COOKIES_FLAT, self::COOKIES_ARRAY])) {
if (!\in_array($format, [self::COOKIES_FLAT, self::COOKIES_ARRAY], true)) {
throw new \InvalidArgumentException(\sprintf('Format "%s" invalid (%s).', $format, implode(', ', [self::COOKIES_FLAT, self::COOKIES_ARRAY])));
}

View File

@@ -72,6 +72,16 @@ abstract class AbstractSessionHandler implements \SessionHandlerInterface, \Sess
return $data;
}
public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool
{
$this->igbinaryEmptyData ??= \function_exists('igbinary_serialize') ? igbinary_serialize([]) : '';
if ('' === $data || $this->igbinaryEmptyData === $data) {
return $this->destroy($sessionId);
}
return true;
}
public function write(#[\SensitiveParameter] string $sessionId, string $data): bool
{
// see https://github.com/igbinary/igbinary/issues/146

View File

@@ -41,7 +41,7 @@ class NativeFileSessionHandler extends \SessionHandler
$baseDir = ltrim(strrchr($savePath, ';'), ';');
}
if ($baseDir && !is_dir($baseDir) && !@mkdir($baseDir, 0777, true) && !is_dir($baseDir)) {
if ($baseDir && !is_dir($baseDir) && !@mkdir($baseDir, 0o777, true) && !is_dir($baseDir)) {
throw new \RuntimeException(\sprintf('Session Storage was not able to create directory "%s".', $baseDir));
}

View File

@@ -197,7 +197,6 @@ class PdoSessionHandler extends AbstractSessionHandler
$table->addColumn($this->dataCol, Types::BLOB)->setNotnull(true);
$table->addColumn($this->lifetimeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true);
$table->addColumn($this->timeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true);
$table->addOption('collate', 'utf8mb4_bin');
$table->addOption('engine', 'InnoDB');
break;
case 'sqlite':
@@ -259,7 +258,7 @@ class PdoSessionHandler extends AbstractSessionHandler
// - trailing space removal
// - case-insensitivity
// - language processing like é == e
'mysql' => "CREATE TABLE $this->table ($this->idCol VARBINARY(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED NOT NULL, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8mb4_bin, ENGINE = InnoDB",
'mysql' => "CREATE TABLE $this->table ($this->idCol VARBINARY(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED NOT NULL, $this->timeCol INTEGER UNSIGNED NOT NULL) ENGINE = InnoDB",
'sqlite' => "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)",
'pgsql' => "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)",
'oci' => "CREATE TABLE $this->table ($this->idCol VARCHAR2(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)",
@@ -802,6 +801,12 @@ class PdoSessionHandler extends AbstractSessionHandler
rewind($data);
$sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, EMPTY_BLOB(), :expiry, :time) RETURNING $this->dataCol into :data";
break;
case 'sqlsrv':
$data = fopen('php://memory', 'r+');
fwrite($data, $sessionData);
rewind($data);
$sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time)";
break;
default:
$data = $sessionData;
$sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time)";
@@ -829,6 +834,12 @@ class PdoSessionHandler extends AbstractSessionHandler
rewind($data);
$sql = "UPDATE $this->table SET $this->dataCol = EMPTY_BLOB(), $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id RETURNING $this->dataCol into :data";
break;
case 'sqlsrv':
$data = fopen('php://memory', 'r+');
fwrite($data, $sessionData);
rewind($data);
$sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id";
break;
default:
$data = $sessionData;
$sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id";
@@ -876,12 +887,16 @@ class PdoSessionHandler extends AbstractSessionHandler
$mergeStmt = $this->pdo->prepare($mergeSql);
if ('sqlsrv' === $this->driver) {
$dataStream = fopen('php://memory', 'r+');
fwrite($dataStream, $data);
rewind($dataStream);
$mergeStmt->bindParam(1, $sessionId, \PDO::PARAM_STR);
$mergeStmt->bindParam(2, $sessionId, \PDO::PARAM_STR);
$mergeStmt->bindParam(3, $data, \PDO::PARAM_LOB);
$mergeStmt->bindParam(3, $dataStream, \PDO::PARAM_LOB);
$mergeStmt->bindValue(4, time() + $maxlifetime, \PDO::PARAM_INT);
$mergeStmt->bindValue(5, time(), \PDO::PARAM_INT);
$mergeStmt->bindParam(6, $data, \PDO::PARAM_LOB);
$mergeStmt->bindParam(6, $dataStream, \PDO::PARAM_LOB);
$mergeStmt->bindValue(7, time() + $maxlifetime, \PDO::PARAM_INT);
$mergeStmt->bindValue(8, time(), \PDO::PARAM_INT);
} else {

View File

@@ -64,6 +64,7 @@ class SessionHandlerFactory
throw new \InvalidArgumentException('Unsupported Redis or Memcached DSN. Try running "composer require symfony/cache".');
}
$handlerClass = str_starts_with($connection, 'memcached:') ? MemcachedSessionHandler::class : RedisSessionHandler::class;
$connection = preg_replace('/([?&])prefix=[^&]*+&?/', '\1', $connection);
$connection = AbstractAdapter::createConnection($connection, ['lazy' => true]);
return new $handlerClass($connection, array_intersect_key($options, ['prefix' => 1, 'ttl' => 1]));

View File

@@ -32,12 +32,14 @@ class MetadataBag implements SessionBagInterface
private int $lastUsed;
/**
* @param string $storageKey The key used to store bag in the session
* @param int $updateThreshold The time to wait between two UPDATED updates
* @param string $storageKey The key used to store bag in the session
* @param int $updateThreshold The time to wait between two UPDATED updates
* @param int|null $cookieLifetime The configured cookie lifetime; null to read from php.ini
*/
public function __construct(
private string $storageKey = '_sf2_meta',
private int $updateThreshold = 0,
private ?int $cookieLifetime = null,
) {
}
@@ -126,6 +128,6 @@ class MetadataBag implements SessionBagInterface
{
$timeStamp = time();
$this->meta[self::CREATED] = $this->meta[self::UPDATED] = $this->lastUsed = $timeStamp;
$this->meta[self::LIFETIME] = $lifetime ?? (int) \ini_get('session.cookie_lifetime');
$this->meta[self::LIFETIME] = $lifetime ?? $this->cookieLifetime ?? (int) \ini_get('session.cookie_lifetime');
}
}

View File

@@ -34,7 +34,7 @@ class MockFileSessionStorage extends MockArraySessionStorage
{
$savePath ??= sys_get_temp_dir();
if (!is_dir($savePath) && !@mkdir($savePath, 0777, true) && !is_dir($savePath)) {
if (!is_dir($savePath) && !@mkdir($savePath, 0o777, true) && !is_dir($savePath)) {
throw new \RuntimeException(\sprintf('Session Storage was not able to create directory "%s".', $savePath));
}
@@ -103,7 +103,7 @@ class MockFileSessionStorage extends MockArraySessionStorage
$this->data = $data;
}
// this is needed when the session object is re-used across multiple requests
// this is needed when the session object is reused across multiple requests
// in functional tests.
$this->started = false;
}

View File

@@ -94,7 +94,7 @@ class StreamedJsonResponse extends StreamedResponse
{
$generators = [];
array_walk_recursive($data, function (&$item, $key) use (&$generators) {
array_walk_recursive($data, static function (&$item, $key) use (&$generators) {
if (self::PLACEHOLDER === $key) {
// if the placeholder is already in the structure it should be replaced with a new one that explode
// works like expected for the structure

View File

@@ -121,19 +121,12 @@ class UriSigner
$uri = self::normalize($uri);
$status = $this->doVerify($uri);
if (self::STATUS_VALID === $status) {
return;
}
if (self::STATUS_MISSING === $status) {
throw new UnsignedUriException();
}
if (self::STATUS_INVALID === $status) {
throw new UnverifiedSignedUriException();
}
throw new ExpiredSignedUriException();
match ($status) {
self::STATUS_VALID => null,
self::STATUS_INVALID => throw new UnverifiedSignedUriException(),
self::STATUS_EXPIRED => throw new ExpiredSignedUriException(),
default => throw new UnsignedUriException(),
};
}
private function computeHash(string $uri): string

View File

@@ -17,20 +17,19 @@
],
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3.0",
"symfony/polyfill-mbstring": "~1.1",
"symfony/polyfill-php83": "^1.27"
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-mbstring": "^1.1"
},
"require-dev": {
"doctrine/dbal": "^3.6|^4",
"predis/predis": "^1.1|^2.0",
"symfony/cache": "^6.4.12|^7.1.5",
"symfony/clock": "^6.4|^7.0",
"symfony/dependency-injection": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/mime": "^6.4|^7.0",
"symfony/expression-language": "^6.4|^7.0",
"symfony/rate-limiter": "^6.4|^7.0"
"symfony/cache": "^6.4.12|^7.1.5|^8.0",
"symfony/clock": "^6.4|^7.0|^8.0",
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
"symfony/http-kernel": "^6.4|^7.0|^8.0",
"symfony/mime": "^6.4|^7.0|^8.0",
"symfony/expression-language": "^6.4|^7.0|^8.0",
"symfony/rate-limiter": "^6.4|^7.0|^8.0"
},
"conflict": {
"doctrine/dbal": "<3.6",

View File

@@ -1,11 +1,6 @@
CHANGELOG
=========
8.0
---
* Replace `__sleep/wakeup()` by `__(un)serialize()` on `AbstractPart` implementations
7.4
---

View File

@@ -65,11 +65,52 @@ abstract class AbstractPart
public function __serialize(): array
{
return ['headers' => $this->headers];
if (!method_exists($this, '__sleep')) {
return ['headers' => $this->headers];
}
trigger_deprecation('symfony/mime', '7.4', 'Implementing "%s::__sleep()" is deprecated, use "__serialize()" instead.', get_debug_type($this));
$data = [];
foreach ($this->__sleep() as $key) {
try {
if (($r = new \ReflectionProperty($this, $key))->isInitialized($this)) {
$data[$key] = $r->getValue($this);
}
} catch (\ReflectionException) {
$data[$key] = $this->$key;
}
}
return $data;
}
public function __unserialize(array $data): void
{
$this->headers = $data['headers'];
if ($wakeup = method_exists($this, '__wakeup') && self::class === (new \ReflectionMethod($this, '__unserialize'))->class) {
trigger_deprecation('symfony/mime', '7.4', 'Implementing "%s::__wakeup()" is deprecated, use "__unserialize()" instead.', get_debug_type($this));
}
if (['headers'] === array_keys($data)) {
$this->headers = $data['headers'];
if ($wakeup) {
$this->__wakeup();
}
return;
}
trigger_deprecation('symfony/mime', '7.4', 'Passing more than just key "headers" to "%s::__unserialize()" is deprecated, populate properties in "%s::__unserialize()" instead.', self::class, get_debug_type($this));
\Closure::bind(function ($data) use ($wakeup) {
foreach ($data as $key => $value) {
$this->{("\0" === $key[0] ?? '') ? substr($key, 1 + strrpos($key, "\0")) : $key} = $value;
}
if ($wakeup) {
$this->__wakeup();
}
}, $this, static::class)($data);
}
}

View File

@@ -19,6 +19,9 @@ use Symfony\Component\Mime\Header\Headers;
*/
class DataPart extends TextPart
{
/** @internal, to be removed in 8.0 */
protected array $_parent;
private ?string $filename = null;
private string $mediaType;
private ?string $cid = null;
@@ -128,24 +131,118 @@ class DataPart extends TextPart
public function __serialize(): array
{
$parent = parent::__serialize();
$headers = $parent['_headers'];
unset($parent['_headers']);
if (self::class === (new \ReflectionMethod($this, '__sleep'))->class || self::class !== (new \ReflectionMethod($this, '__serialize'))->class) {
$parent = parent::__serialize();
$headers = $parent['_headers'];
unset($parent['_headers']);
return [
'_headers' => $headers,
'_parent' => $parent,
'filename' => $this->filename,
'mediaType' => $this->mediaType,
'cid' => $this->cid,
];
return [
'_headers' => $headers,
'_parent' => $parent,
'filename' => $this->filename,
'mediaType' => $this->mediaType,
'cid' => $this->cid,
];
}
trigger_deprecation('symfony/mime', '7.4', 'Implementing "%s::__sleep()" is deprecated, use "__serialize()" instead.', get_debug_type($this));
$data = [];
foreach ($this->__sleep() as $key) {
try {
if (($r = new \ReflectionProperty($this, $key))->isInitialized($this)) {
$data[$key] = $r->getValue($this);
}
} catch (\ReflectionException) {
$data[$key] = $this->$key;
}
}
return $data;
}
public function __unserialize(array $data): void
{
parent::__unserialize(['_headers' => $data['_headers'] ?? $data["\0*\0_headers"], ...$data['_parent'] ?? $data["\0*\0_parent"]]);
$this->filename = $data['filename'] ?? $data["\0".self::class."\0filename"] ?? null;
$this->mediaType = $data['mediaType'] ?? $data["\0".self::class."\0mediaType"];
$this->cid = $data['cid'] ?? $data["\0".self::class."\0cid"] ?? null;
if ($wakeup = self::class !== (new \ReflectionMethod($this, '__wakeup'))->class && self::class === (new \ReflectionMethod($this, '__unserialize'))->class) {
trigger_deprecation('symfony/mime', '7.4', 'Implementing "%s::__wakeup()" is deprecated, use "__unserialize()" instead.', get_debug_type($this));
}
if (['_headers', '_parent', 'filename', 'mediaType'] === array_keys($data) || ['_headers', '_parent', 'filename', 'mediaType', 'cid'] === array_keys($data)) {
parent::__unserialize(['_headers' => $data['_headers'], ...$data['_parent']]);
$this->filename = $data['filename'];
$this->mediaType = $data['mediaType'];
$this->cid = $data['cid'] ?? null;
if ($wakeup) {
$this->__wakeup();
}
return;
}
if (["\0*\0_headers", "\0*\0_parent", "\0".self::class."\0filename", "\0".self::class."\0mediaType"] === array_keys($data)) {
parent::__unserialize(['_headers' => $data["\0*\0_headers"], ...$data["\0*\0_parent"]]);
$this->filename = $data["\0".self::class."\0filename"];
$this->mediaType = $data["\0".self::class."\0mediaType"];
if ($wakeup) {
$this->__wakeup();
}
return;
}
trigger_deprecation('symfony/mime', '7.4', 'Passing extra keys to "%s::__unserialize()" is deprecated, populate properties in "%s::__unserialize()" instead.', self::class, get_debug_type($this));
\Closure::bind(function ($data) use ($wakeup) {
foreach ($data as $key => $value) {
$this->{("\0" === $key[0] ?? '') ? substr($key, 1 + strrpos($key, "\0")) : $key} = $value;
}
if ($wakeup) {
$this->__wakeup();
}
}, $this, static::class)($data);
}
/**
* @deprecated since Symfony 7.4, will be replaced by `__serialize()` in 8.0
*/
public function __sleep(): array
{
trigger_deprecation('symfony/mime', '7.4', 'Calling "%s::__sleep()" is deprecated, use "__serialize()" instead.', get_debug_type($this));
// converts the body to a string
parent::__sleep();
$this->_parent = [];
foreach (['body', 'charset', 'subtype', 'disposition', 'name', 'encoding'] as $name) {
$r = new \ReflectionProperty(TextPart::class, $name);
$this->_parent[$name] = $r->getValue($this);
}
$this->_headers = $this->getHeaders();
return ['_headers', '_parent', 'filename', 'mediaType', 'cid'];
}
/**
* @deprecated since Symfony 7.4, will be replaced by `__unserialize()` in 8.0
*/
public function __wakeup(): void
{
$r = new \ReflectionProperty(AbstractPart::class, 'headers');
$r->setValue($this, $this->_headers);
unset($this->_headers);
if (!\is_array($this->_parent)) {
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
foreach (['body', 'charset', 'subtype', 'disposition', 'name', 'encoding'] as $name) {
if (null !== $this->_parent[$name] && !\is_string($this->_parent[$name]) && !$this->_parent[$name] instanceof File) {
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
$r = new \ReflectionProperty(TextPart::class, $name);
$r->setValue($this, $this->_parent[$name]);
}
unset($this->_parent);
}
}

View File

@@ -18,6 +18,9 @@ use Symfony\Component\Mime\Header\Headers;
*/
class SMimePart extends AbstractPart
{
/** @internal, to be removed in 8.0 */
protected Headers $_headers;
public function __construct(
private iterable|string $body,
private string $type,
@@ -83,18 +86,35 @@ class SMimePart extends AbstractPart
public function __serialize(): array
{
// convert iterables to strings for serialization
if (is_iterable($this->body)) {
$this->body = $this->bodyToString();
if (self::class === (new \ReflectionMethod($this, '__sleep'))->class || self::class !== (new \ReflectionMethod($this, '__serialize'))->class) {
// convert iterables to strings for serialization
if (is_iterable($this->body)) {
$this->body = $this->bodyToString();
}
return [
'_headers' => $this->getHeaders(),
'body' => $this->body,
'type' => $this->type,
'subtype' => $this->subtype,
'parameters' => $this->parameters,
];
}
return [
'_headers' => $this->getHeaders(),
'body' => $this->body,
'type' => $this->type,
'subtype' => $this->subtype,
'parameters' => $this->parameters,
];
trigger_deprecation('symfony/mime', '7.4', 'Implementing "%s::__sleep()" is deprecated, use "__serialize()" instead.', get_debug_type($this));
$data = [];
foreach ($this->__sleep() as $key) {
try {
if (($r = new \ReflectionProperty($this, $key))->isInitialized($this)) {
$data[$key] = $r->getValue($this);
}
} catch (\ReflectionException) {
$data[$key] = $this->$key;
}
}
return $data;
}
public function __unserialize(array $data): void
@@ -105,10 +125,81 @@ class SMimePart extends AbstractPart
}
}
parent::__unserialize(['headers' => $data['_headers'] ?? $data["\0*\0_headers"]]);
$this->body = $data['body'] ?? $data["\0".self::class."\0body"];
$this->type = $data['type'] ?? $data["\0".self::class."\0type"];
$this->subtype = $data['subtype'] ?? $data["\0".self::class."\0subtype"];
$this->parameters = $data['parameters'] ?? $data["\0".self::class."\0parameters"];
if ($wakeup = self::class !== (new \ReflectionMethod($this, '__wakeup'))->class && self::class === (new \ReflectionMethod($this, '__unserialize'))->class) {
trigger_deprecation('symfony/mime', '7.4', 'Implementing "%s::__wakeup()" is deprecated, use "__unserialize()" instead.', get_debug_type($this));
}
if (['_headers', 'body', 'type', 'subtype', 'parameters'] === array_keys($data)) {
parent::__unserialize(['headers' => $data['_headers']]);
$this->body = $data['body'];
$this->type = $data['type'];
$this->subtype = $data['subtype'];
$this->parameters = $data['parameters'];
if ($wakeup) {
$this->__wakeup();
}
return;
}
$p = "\0".self::class."\0";
if (["\0*\0_headers", $p.'body', $p.'type', $p.'subtype', $p.'parameters'] === array_keys($data)) {
$r = new \ReflectionProperty(parent::class, 'headers');
$r->setValue($this, $data["\0*\0_headers"]);
$this->body = $data[$p.'body'];
$this->type = $data[$p.'type'];
$this->subtype = $data[$p.'subtype'];
$this->parameters = $data[$p.'parameters'];
if ($wakeup) {
$this->_headers = $data["\0*\0_headers"];
$this->__wakeup();
}
return;
}
trigger_deprecation('symfony/mime', '7.4', 'Passing extra keys to "%s::__unserialize()" is deprecated, populate properties in "%s::__unserialize()" instead.', self::class, get_debug_type($this));
\Closure::bind(function ($data) use ($wakeup) {
foreach ($data as $key => $value) {
$this->{("\0" === $key[0] ?? '') ? substr($key, 1 + strrpos($key, "\0")) : $key} = $value;
}
if ($wakeup) {
$this->__wakeup();
}
}, $this, static::class)($data);
}
/**
* @deprecated since Symfony 7.4, will be replaced by `__serialize()` in 8.0
*/
public function __sleep(): array
{
trigger_deprecation('symfony/mime', '7.4', 'Calling "%s::__sleep()" is deprecated, use "__serialize()" instead.', get_debug_type($this));
// convert iterables to strings for serialization
if (is_iterable($this->body)) {
$this->body = $this->bodyToString();
}
$this->_headers = $this->getHeaders();
return ['_headers', 'body', 'type', 'subtype', 'parameters'];
}
/**
* @deprecated since Symfony 7.4, will be replaced by `__unserialize()` in 8.0
*/
public function __wakeup(): void
{
trigger_deprecation('symfony/mime', '7.4', 'Calling "%s::__wakeup()" is deprecated, use "__unserialize()" instead.', get_debug_type($this));
$r = new \ReflectionProperty(AbstractPart::class, 'headers');
$r->setValue($this, $this->_headers);
unset($this->_headers);
}
}

View File

@@ -25,6 +25,9 @@ class TextPart extends AbstractPart
{
private const DEFAULT_ENCODERS = ['quoted-printable', 'base64', '8bit'];
/** @internal, to be removed in 8.0 */
protected Headers $_headers;
private static array $encoders = [];
/** @var resource|string|File */
@@ -237,21 +240,38 @@ class TextPart extends AbstractPart
public function __serialize(): array
{
// convert resources to strings for serialization
if (null !== $this->seekable) {
$this->body = $this->getBody();
$this->seekable = null;
if (self::class === (new \ReflectionMethod($this, '__sleep'))->class || self::class !== (new \ReflectionMethod($this, '__serialize'))->class) {
// convert resources to strings for serialization
if (null !== $this->seekable) {
$this->body = $this->getBody();
$this->seekable = null;
}
return [
'_headers' => $this->getHeaders(),
'body' => $this->body,
'charset' => $this->charset,
'subtype' => $this->subtype,
'disposition' => $this->disposition,
'name' => $this->name,
'encoding' => $this->encoding,
];
}
return [
'_headers' => $this->getHeaders(),
'body' => $this->body,
'charset' => $this->charset,
'subtype' => $this->subtype,
'disposition' => $this->disposition,
'name' => $this->name,
'encoding' => $this->encoding,
];
trigger_deprecation('symfony/mime', '7.4', 'Implementing "%s::__sleep()" is deprecated, use "__serialize()" instead.', get_debug_type($this));
$data = [];
foreach ($this->__sleep() as $key) {
try {
if (($r = new \ReflectionProperty($this, $key))->isInitialized($this)) {
$data[$key] = $r->getValue($this);
}
} catch (\ReflectionException) {
$data[$key] = $this->$key;
}
}
return $data;
}
public function __unserialize(array $data): void
@@ -262,19 +282,91 @@ class TextPart extends AbstractPart
}
}
if ($wakeup = self::class !== (new \ReflectionMethod($this, '__wakeup'))->class && self::class === (new \ReflectionMethod($this, '__unserialize'))->class) {
trigger_deprecation('symfony/mime', '7.4', 'Implementing "%s::__wakeup()" is deprecated, use "__unserialize()" instead.', get_debug_type($this));
}
if ($headers = $data['_headers'] ?? $data["\0*\0_headers"] ?? null) {
unset($data['_headers'], $data["\0*\0_headers"]);
parent::__unserialize(['headers' => $headers]);
}
$this->body = $data['body'] ?? $data["\0".self::class."\0body"];
$this->charset = $data['charset'] ?? $data["\0".self::class."\0charset"] ?? null;
$this->subtype = $data['subtype'] ?? $data["\0".self::class."\0subtype"];
$this->disposition = $data['disposition'] ?? $data["\0".self::class."\0disposition"] ?? null;
$this->name = $data['name'] ?? $data["\0".self::class."\0name"] ?? null;
$this->encoding = $data['encoding'] ?? $data["\0".self::class."\0encoding"];
if (['body', 'charset', 'subtype', 'disposition', 'name', 'encoding'] === array_keys($data)) {
parent::__unserialize(['headers' => $headers]);
$this->body = $data['body'];
$this->charset = $data['charset'];
$this->subtype = $data['subtype'];
$this->disposition = $data['disposition'];
$this->name = $data['name'];
$this->encoding = $data['encoding'];
if (!\is_string($this->body) && !$this->body instanceof File) {
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
if ($wakeup) {
$this->__wakeup();
} elseif (!\is_string($this->body) && !$this->body instanceof File) {
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
return;
}
if (["\0".self::class."\0body", "\0".self::class."\0charset", "\0".self::class."\0subtype", "\0".self::class."\0disposition", "\0".self::class."\0name", "\0".self::class."\0encoding"] === array_keys($data)) {
$this->body = $data["\0".self::class."\0body"];
$this->charset = $data["\0".self::class."\0charset"];
$this->subtype = $data["\0".self::class."\0subtype"];
$this->disposition = $data["\0".self::class."\0disposition"];
$this->name = $data["\0".self::class."\0name"];
$this->encoding = $data["\0".self::class."\0encoding"];
if ($wakeup) {
$this->_headers = $headers;
$this->__wakeup();
} elseif (!\is_string($this->body) && !$this->body instanceof File) {
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
return;
}
trigger_deprecation('symfony/mime', '7.4', 'Passing extra keys to "%s::__unserialize()" is deprecated, populate properties in "%s::__unserialize()" instead.', self::class, get_debug_type($this));
\Closure::bind(function ($data) use ($wakeup) {
foreach ($data as $key => $value) {
$this->{("\0" === $key[0] ?? '') ? substr($key, 1 + strrpos($key, "\0")) : $key} = $value;
}
if ($wakeup) {
$this->__wakeup();
}
}, $this, static::class)($data);
}
/**
* @deprecated since Symfony 7.4, will be replaced by `__serialize()` in 8.0
*/
public function __sleep(): array
{
trigger_deprecation('symfony/mime', '7.4', 'Calling "%s::__sleep()" is deprecated, use "__serialize()" instead.', get_debug_type($this));
// convert resources to strings for serialization
if (null !== $this->seekable) {
$this->body = $this->getBody();
$this->seekable = null;
}
$this->_headers = $this->getHeaders();
return ['_headers', 'body', 'charset', 'subtype', 'disposition', 'name', 'encoding'];
}
/**
* @deprecated since Symfony 7.4, will be replaced by `__unserialize()` in 8.0
*/
public function __wakeup(): void
{
trigger_deprecation('symfony/mime', '7.4', 'Calling "%s::__wakeup()" is deprecated, use "__unserialize()" instead.', get_debug_type($this));
$r = new \ReflectionProperty(AbstractPart::class, 'headers');
$r->setValue($this, $this->_headers);
unset($this->_headers);
}
}

View File

@@ -3,13 +3,6 @@ MIME Component
The MIME component allows manipulating MIME messages.
Sponsor
-------
This package is looking for a [backer][1].
Help Symfony by [sponsoring][3] its development!
Resources
---------
@@ -18,6 +11,3 @@ Resources
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
[1]: https://symfony.com/backers
[3]: https://symfony.com/sponsor

View File

@@ -16,7 +16,8 @@
}
],
"require": {
"php": ">=8.4.1",
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-intl-idn": "^1.10",
"symfony/polyfill-mbstring": "^1.0"
},
@@ -24,16 +25,18 @@
"egulias/email-validator": "^2.1.10|^3.1|^4",
"league/html-to-markdown": "^5.0",
"phpdocumentor/reflection-docblock": "^5.2|^6.0",
"symfony/dependency-injection": "^7.4|^8.0",
"symfony/process": "^7.4|^8.0",
"symfony/property-access": "^7.4|^8.0",
"symfony/property-info": "^7.4|^8.0",
"symfony/serializer": "^7.4|^8.0"
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
"symfony/process": "^6.4|^7.0|^8.0",
"symfony/property-access": "^6.4|^7.0|^8.0",
"symfony/property-info": "^6.4|^7.0|^8.0",
"symfony/serializer": "^6.4.3|^7.0.3|^8.0"
},
"conflict": {
"egulias/email-validator": "~3.0.0",
"phpdocumentor/reflection-docblock": "<5.2|>=7",
"phpdocumentor/type-resolver": "<1.5.1"
"phpdocumentor/type-resolver": "<1.5.1",
"symfony/mailer": "<6.4",
"symfony/serializer": "<6.4.3|>7.0,<7.0.3"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Mime\\": "" },

View File

@@ -82,6 +82,7 @@ final class Mbstring
private static $encodingList = ['ASCII', 'UTF-8'];
private static $language = 'neutral';
private static $internalEncoding = 'UTF-8';
private static $iconvSupportsIgnore;
public static function mb_convert_encoding($s, $toEncoding, $fromEncoding = null)
{
@@ -116,24 +117,53 @@ final class Mbstring
$fromEncoding = 'Windows-1252';
}
if ('UTF-8' !== $fromEncoding) {
$s = iconv($fromEncoding, 'UTF-8//IGNORE', $s);
$s = self::iconv($fromEncoding, 'UTF-8', $s);
}
return preg_replace_callback('/[\x80-\xFF]+/', [__CLASS__, 'html_encoding_callback'], $s);
}
if ('HTML-ENTITIES' === $fromEncoding) {
$s = html_entity_decode($s, \ENT_COMPAT, 'UTF-8');
$decodeControlChars = static function ($m) {
$code = '' !== ($m[2] ?? '') ? hexdec($m[2]) : (int) $m[1];
if ($code < 32 || 127 === $code) {
return \chr($code);
}
if (128 <= $code && $code <= 159) {
return "\xC2".\chr(0x80 | ($code & 0x3F));
}
return $m[0];
};
if (\PHP_VERSION_ID >= 70400) {
$s = html_entity_decode($s, \ENT_QUOTES, 'UTF-8');
// html_entity_decode() leaves numeric entities for C0/C1 control
// characters as-is (HTML spec), but mb_convert_encoding() decodes
// them. Catch what html_entity_decode() missed.
if (false !== strpos($s, '&#')) {
$s = preg_replace_callback('/&#(?:0*([0-9]++)|[xX]0*([0-9a-fA-F]++));/', $decodeControlChars, $s);
}
} else {
// PHP < 7.4: html_entity_decode() truncates strings at NUL bytes,
// so decode the control character entities first then call
// html_entity_decode() on each NUL-delimited chunk independently.
$s = preg_replace_callback('/&#(?:0*([0-9]++)|[xX]0*([0-9a-fA-F]++));/', $decodeControlChars, $s);
$s = implode("\0", array_map(static function ($chunk) {
return html_entity_decode($chunk, \ENT_QUOTES, 'UTF-8');
}, explode("\0", $s)));
}
$fromEncoding = 'UTF-8';
}
return iconv($fromEncoding, $toEncoding.'//IGNORE', $s);
return self::iconv($fromEncoding, $toEncoding, $s);
}
public static function mb_convert_variables($toEncoding, $fromEncoding, &...$vars)
{
$ok = true;
array_walk_recursive($vars, function (&$v) use (&$ok, $toEncoding, $fromEncoding) {
array_walk_recursive($vars, static function (&$v) use (&$ok, $toEncoding, $fromEncoding) {
if (false === $v = self::mb_convert_encoding($v, $toEncoding, $fromEncoding)) {
$ok = false;
}
@@ -180,10 +210,10 @@ final class Mbstring
if ('UTF-8' === $encoding) {
$encoding = null;
if (!preg_match('//u', $s)) {
$s = @iconv('UTF-8', 'UTF-8//IGNORE', $s);
$s = @self::iconv('UTF-8', 'UTF-8', $s);
}
} else {
$s = iconv($encoding, 'UTF-8//IGNORE', $s);
$s = self::iconv($encoding, 'UTF-8', $s);
}
$cnt = floor(\count($convmap) / 4) * 4;
@@ -194,7 +224,7 @@ final class Mbstring
$convmap[$i + 1] += $convmap[$i + 2];
}
$s = preg_replace_callback('/&#(?:0*([0-9]+)|x0*([0-9a-fA-F]+))(?!&);?/', function (array $m) use ($cnt, $convmap) {
$s = preg_replace_callback('/&#(?:0*([0-9]+)|x0*([0-9a-fA-F]+))'.(\PHP_VERSION_ID >= 80200 ? '' : '(?!&)').';?/', static function (array $m) use ($cnt, $convmap) {
$c = isset($m[2]) ? (int) hexdec($m[2]) : $m[1];
for ($i = 0; $i < $cnt; $i += 4) {
if ($c >= $convmap[$i] && $c <= $convmap[$i + 1]) {
@@ -209,7 +239,7 @@ final class Mbstring
return $s;
}
return iconv('UTF-8', $encoding.'//IGNORE', $s);
return self::iconv('UTF-8', $encoding, $s);
}
public static function mb_encode_numericentity($s, $convmap, $encoding = null, $is_hex = false)
@@ -246,10 +276,10 @@ final class Mbstring
if ('UTF-8' === $encoding) {
$encoding = null;
if (!preg_match('//u', $s)) {
$s = @iconv('UTF-8', 'UTF-8//IGNORE', $s);
$s = @self::iconv('UTF-8', 'UTF-8', $s);
}
} else {
$s = iconv($encoding, 'UTF-8//IGNORE', $s);
$s = self::iconv($encoding, 'UTF-8', $s);
}
static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4];
@@ -268,7 +298,7 @@ final class Mbstring
for ($j = 0; $j < $cnt; $j += 4) {
if ($c >= $convmap[$j] && $c <= $convmap[$j + 1]) {
$cOffset = ($c + $convmap[$j + 2]) & $convmap[$j + 3];
$result .= $is_hex ? sprintf('&#x%X;', $cOffset) : '&#'.$cOffset.';';
$result .= $is_hex ? \sprintf('&#x%X;', $cOffset) : '&#'.$cOffset.';';
continue 2;
}
}
@@ -279,7 +309,7 @@ final class Mbstring
return $result;
}
return iconv('UTF-8', $encoding.'//IGNORE', $result);
return self::iconv('UTF-8', $encoding, $result);
}
public static function mb_convert_case($s, $mode, $encoding = null)
@@ -294,10 +324,10 @@ final class Mbstring
if ('UTF-8' === $encoding) {
$encoding = null;
if (!preg_match('//u', $s)) {
$s = @iconv('UTF-8', 'UTF-8//IGNORE', $s);
$s = @self::iconv('UTF-8', 'UTF-8', $s);
}
} else {
$s = iconv($encoding, 'UTF-8//IGNORE', $s);
$s = self::iconv($encoding, 'UTF-8', $s);
}
if (\MB_CASE_TITLE == $mode) {
@@ -361,7 +391,7 @@ final class Mbstring
return $s;
}
return iconv('UTF-8', $encoding.'//IGNORE', $s);
return self::iconv('UTF-8', $encoding, $s);
}
public static function mb_internal_encoding($encoding = null)
@@ -382,7 +412,7 @@ final class Mbstring
return false;
}
throw new \ValueError(sprintf('Argument #1 ($encoding) must be a valid encoding, "%s" given', $encoding));
throw new \ValueError(\sprintf('Argument #1 ($encoding) must be a valid encoding, "%s" given', $encoding));
}
public static function mb_language($lang = null)
@@ -403,7 +433,7 @@ final class Mbstring
return false;
}
throw new \ValueError(sprintf('Argument #1 ($language) must be a valid language, "%s" given', $lang));
throw new \ValueError(\sprintf('Argument #1 ($language) must be a valid language, "%s" given', $lang));
}
public static function mb_list_encodings()
@@ -519,7 +549,15 @@ final class Mbstring
return \strlen($s);
}
return @iconv_strlen($s, $encoding);
if (false !== $len = @iconv_strlen($s, $encoding)) {
return $len;
}
if ('UTF-8' !== $encoding) {
return $len;
}
return preg_match_all('/[\x00-\x7F]|[\xC0-\xDF][\x80-\xBF]?|[\xE0-\xEF][\x80-\xBF]{0,2}|[\xF0-\xF7][\x80-\xBF]{0,3}|[\xF8-\xFB][\x80-\xBF]{0,4}|[\xFC-\xFD][\x80-\xBF]{0,5}|[\x80-\xBF\xFE\xFF]/s', $s);
}
public static function mb_strpos($haystack, $needle, $offset = 0, $encoding = null)
@@ -773,7 +811,7 @@ final class Mbstring
$encoding = self::getEncoding($encoding);
if ('UTF-8' !== $encoding) {
$s = iconv($encoding, 'UTF-8//IGNORE', $s);
$s = self::iconv($encoding, 'UTF-8', $s);
}
$s = preg_replace('/[\x{1100}-\x{115F}\x{2329}\x{232A}\x{2E80}-\x{303E}\x{3040}-\x{A4CF}\x{AC00}-\x{D7A3}\x{F900}-\x{FAFF}\x{FE10}-\x{FE19}\x{FE30}-\x{FE6F}\x{FF00}-\x{FF60}\x{FFE0}-\x{FFE6}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}]/u', '', $s, -1, $wide);
@@ -834,22 +872,47 @@ final class Mbstring
return $code;
}
public static function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = \STR_PAD_RIGHT, ?string $encoding = null): string
/** @return string|false */
public static function mb_scrub(?string $string, ?string $encoding = null): string
{
if (!\in_array($pad_type, [\STR_PAD_RIGHT, \STR_PAD_LEFT, \STR_PAD_BOTH], true)) {
throw new \ValueError('mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH');
}
if (null === $encoding) {
$encoding = self::mb_internal_encoding();
} else {
self::assertEncoding($encoding, 'mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given');
} elseif (!self::assertEncoding($encoding, 'mb_scrub(): Argument #2 ($encoding) must be a valid encoding, "%s" given')) {
return false;
}
return self::mb_convert_encoding((string) $string, $encoding, $encoding);
}
/** @return string|false */
public static function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = \STR_PAD_RIGHT, ?string $encoding = null)
{
if (null === $encoding) {
$encoding = self::mb_internal_encoding();
} elseif (!self::assertEncoding($encoding, 'mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given')) {
return false;
}
if (self::mb_strlen($pad_string, $encoding) <= 0) {
if (\PHP_VERSION_ID < 80000) {
trigger_error('mb_str_pad(): Argument #3 ($pad_string) must be a non-empty string', \E_USER_WARNING);
return false;
}
throw new \ValueError('mb_str_pad(): Argument #3 ($pad_string) must be a non-empty string');
}
if (!\in_array($pad_type, [\STR_PAD_RIGHT, \STR_PAD_LEFT, \STR_PAD_BOTH], true)) {
if (\PHP_VERSION_ID < 80000) {
trigger_error('mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH', \E_USER_WARNING);
return false;
}
throw new \ValueError('mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH');
}
$paddingRequired = $length - self::mb_strlen($string, $encoding);
if ($paddingRequired < 1) {
@@ -869,12 +932,13 @@ final class Mbstring
}
}
public static function mb_ucfirst(string $string, ?string $encoding = null): string
/** @return string|false */
public static function mb_ucfirst(string $string, ?string $encoding = null)
{
if (null === $encoding) {
$encoding = self::mb_internal_encoding();
} else {
self::assertEncoding($encoding, 'mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given');
} elseif (!self::assertEncoding($encoding, 'mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given')) {
return false;
}
$firstChar = mb_substr($string, 0, 1, $encoding);
@@ -883,12 +947,13 @@ final class Mbstring
return $firstChar.mb_substr($string, 1, null, $encoding);
}
public static function mb_lcfirst(string $string, ?string $encoding = null): string
/** @return string|false */
public static function mb_lcfirst(string $string, ?string $encoding = null)
{
if (null === $encoding) {
$encoding = self::mb_internal_encoding();
} else {
self::assertEncoding($encoding, 'mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given');
} elseif (!self::assertEncoding($encoding, 'mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given')) {
return false;
}
$firstChar = mb_substr($string, 0, 1, $encoding);
@@ -897,6 +962,24 @@ final class Mbstring
return $firstChar.mb_substr($string, 1, null, $encoding);
}
/** @return string|false */
public static function mb_trim(string $string, ?string $characters = null, ?string $encoding = null)
{
return self::mb_internal_trim('{^[%s]+|[%1$s]+$}Du', $string, $characters, $encoding, __FUNCTION__);
}
/** @return string|false */
public static function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null)
{
return self::mb_internal_trim('{^[%s]+}Du', $string, $characters, $encoding, __FUNCTION__);
}
/** @return string|false */
public static function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null)
{
return self::mb_internal_trim('{[%s]+$}Du', $string, $characters, $encoding, __FUNCTION__);
}
private static function getSubpart($pos, $part, $haystack, $encoding)
{
if (false === $pos) {
@@ -968,30 +1051,35 @@ final class Mbstring
return 'UTF-8';
}
if ('UTF-32' === $encoding) {
return 'UTF-32BE';
}
if ('UTF-16' === $encoding) {
return 'UTF-16BE';
}
return $encoding;
}
public static function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string
private static function iconv($fromEncoding, $toEncoding, $s)
{
return self::mb_internal_trim('{^[%s]+|[%1$s]+$}Du', $string, $characters, $encoding, __FUNCTION__);
if (null === self::$iconvSupportsIgnore) {
self::$iconvSupportsIgnore = false !== @iconv('UTF-8', 'UTF-8//IGNORE', '');
}
return self::$iconvSupportsIgnore
? iconv($fromEncoding, $toEncoding.'//IGNORE', $s)
: iconv($fromEncoding, $toEncoding, $s);
}
public static function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string
{
return self::mb_internal_trim('{^[%s]+}Du', $string, $characters, $encoding, __FUNCTION__);
}
public static function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string
{
return self::mb_internal_trim('{[%s]+$}Du', $string, $characters, $encoding, __FUNCTION__);
}
private static function mb_internal_trim(string $regex, string $string, ?string $characters, ?string $encoding, string $function): string
/** @return string|false */
private static function mb_internal_trim(string $regex, string $string, ?string $characters, ?string $encoding, string $function)
{
if (null === $encoding) {
$encoding = self::mb_internal_encoding();
} else {
self::assertEncoding($encoding, $function.'(): Argument #3 ($encoding) must be a valid encoding, "%s" given');
} elseif (!self::assertEncoding($encoding, $function.'(): Argument #3 ($encoding) must be a valid encoding, "%s" given')) {
return false;
}
if ('' === $characters) {
@@ -1001,16 +1089,16 @@ final class Mbstring
if ('UTF-8' === $encoding) {
$encoding = null;
if (!preg_match('//u', $string)) {
$string = @iconv('UTF-8', 'UTF-8//IGNORE', $string);
$string = @self::iconv('UTF-8', 'UTF-8', $string);
}
if (null !== $characters && !preg_match('//u', $characters)) {
$characters = @iconv('UTF-8', 'UTF-8//IGNORE', $characters);
$characters = @self::iconv('UTF-8', 'UTF-8', $characters);
}
} else {
$string = iconv($encoding, 'UTF-8//IGNORE', $string);
$string = self::iconv($encoding, 'UTF-8', $string);
if (null !== $characters) {
$characters = iconv($encoding, 'UTF-8//IGNORE', $characters);
$characters = self::iconv($encoding, 'UTF-8', $characters);
}
}
@@ -1020,26 +1108,31 @@ final class Mbstring
$characters = preg_quote($characters);
}
$string = preg_replace(sprintf($regex, $characters), '', $string);
$string = preg_replace(\sprintf($regex, $characters), '', $string);
if (null === $encoding) {
return $string;
}
return iconv('UTF-8', $encoding.'//IGNORE', $string);
return self::iconv('UTF-8', $encoding, $string);
}
private static function assertEncoding(string $encoding, string $errorFormat): void
private static function assertEncoding(string $encoding, string $errorFormat): bool
{
try {
$validEncoding = @self::mb_check_encoding('', $encoding);
} catch (\ValueError $e) {
throw new \ValueError(sprintf($errorFormat, $encoding));
throw new \ValueError(\sprintf($errorFormat, $encoding));
}
// BC for PHP 7.3 and lower
if (!$validEncoding) {
throw new \ValueError(sprintf($errorFormat, $encoding));
if (80000 > \PHP_VERSION_ID) {
trigger_error(\sprintf($errorFormat, $encoding), \E_USER_WARNING);
} else {
throw new \ValueError(\sprintf($errorFormat, $encoding));
}
}
return $validEncoding;
}
}

View File

@@ -9,164 +9,8 @@
* file that was distributed with this source code.
*/
use Symfony\Polyfill\Mbstring as p;
if (\PHP_VERSION_ID >= 80000) {
return require __DIR__.'/bootstrap80.php';
}
if (!function_exists('mb_convert_encoding')) {
function mb_convert_encoding($string, $to_encoding, $from_encoding = null) { return p\Mbstring::mb_convert_encoding($string, $to_encoding, $from_encoding); }
}
if (!function_exists('mb_decode_mimeheader')) {
function mb_decode_mimeheader($string) { return p\Mbstring::mb_decode_mimeheader($string); }
}
if (!function_exists('mb_encode_mimeheader')) {
function mb_encode_mimeheader($string, $charset = null, $transfer_encoding = null, $newline = "\r\n", $indent = 0) { return p\Mbstring::mb_encode_mimeheader($string, $charset, $transfer_encoding, $newline, $indent); }
}
if (!function_exists('mb_decode_numericentity')) {
function mb_decode_numericentity($string, $map, $encoding = null) { return p\Mbstring::mb_decode_numericentity($string, $map, $encoding); }
}
if (!function_exists('mb_encode_numericentity')) {
function mb_encode_numericentity($string, $map, $encoding = null, $hex = false) { return p\Mbstring::mb_encode_numericentity($string, $map, $encoding, $hex); }
}
if (!function_exists('mb_convert_case')) {
function mb_convert_case($string, $mode, $encoding = null) { return p\Mbstring::mb_convert_case($string, $mode, $encoding); }
}
if (!function_exists('mb_internal_encoding')) {
function mb_internal_encoding($encoding = null) { return p\Mbstring::mb_internal_encoding($encoding); }
}
if (!function_exists('mb_language')) {
function mb_language($language = null) { return p\Mbstring::mb_language($language); }
}
if (!function_exists('mb_list_encodings')) {
function mb_list_encodings() { return p\Mbstring::mb_list_encodings(); }
}
if (!function_exists('mb_encoding_aliases')) {
function mb_encoding_aliases($encoding) { return p\Mbstring::mb_encoding_aliases($encoding); }
}
if (!function_exists('mb_check_encoding')) {
function mb_check_encoding($value = null, $encoding = null) { return p\Mbstring::mb_check_encoding($value, $encoding); }
}
if (!function_exists('mb_detect_encoding')) {
function mb_detect_encoding($string, $encodings = null, $strict = false) { return p\Mbstring::mb_detect_encoding($string, $encodings, $strict); }
}
if (!function_exists('mb_detect_order')) {
function mb_detect_order($encoding = null) { return p\Mbstring::mb_detect_order($encoding); }
}
if (!function_exists('mb_parse_str')) {
function mb_parse_str($string, &$result = []) { parse_str($string, $result); return (bool) $result; }
}
if (!function_exists('mb_strlen')) {
function mb_strlen($string, $encoding = null) { return p\Mbstring::mb_strlen($string, $encoding); }
}
if (!function_exists('mb_strpos')) {
function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strpos($haystack, $needle, $offset, $encoding); }
}
if (!function_exists('mb_strtolower')) {
function mb_strtolower($string, $encoding = null) { return p\Mbstring::mb_strtolower($string, $encoding); }
}
if (!function_exists('mb_strtoupper')) {
function mb_strtoupper($string, $encoding = null) { return p\Mbstring::mb_strtoupper($string, $encoding); }
}
if (!function_exists('mb_substitute_character')) {
function mb_substitute_character($substitute_character = null) { return p\Mbstring::mb_substitute_character($substitute_character); }
}
if (!function_exists('mb_substr')) {
function mb_substr($string, $start, $length = 2147483647, $encoding = null) { return p\Mbstring::mb_substr($string, $start, $length, $encoding); }
}
if (!function_exists('mb_stripos')) {
function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_stripos($haystack, $needle, $offset, $encoding); }
}
if (!function_exists('mb_stristr')) {
function mb_stristr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_stristr($haystack, $needle, $before_needle, $encoding); }
}
if (!function_exists('mb_strrchr')) {
function mb_strrchr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrchr($haystack, $needle, $before_needle, $encoding); }
}
if (!function_exists('mb_strrichr')) {
function mb_strrichr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrichr($haystack, $needle, $before_needle, $encoding); }
}
if (!function_exists('mb_strripos')) {
function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strripos($haystack, $needle, $offset, $encoding); }
}
if (!function_exists('mb_strrpos')) {
function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strrpos($haystack, $needle, $offset, $encoding); }
}
if (!function_exists('mb_strstr')) {
function mb_strstr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strstr($haystack, $needle, $before_needle, $encoding); }
}
if (!function_exists('mb_get_info')) {
function mb_get_info($type = 'all') { return p\Mbstring::mb_get_info($type); }
}
if (!function_exists('mb_http_output')) {
function mb_http_output($encoding = null) { return p\Mbstring::mb_http_output($encoding); }
}
if (!function_exists('mb_strwidth')) {
function mb_strwidth($string, $encoding = null) { return p\Mbstring::mb_strwidth($string, $encoding); }
}
if (!function_exists('mb_substr_count')) {
function mb_substr_count($haystack, $needle, $encoding = null) { return p\Mbstring::mb_substr_count($haystack, $needle, $encoding); }
}
if (!function_exists('mb_output_handler')) {
function mb_output_handler($string, $status) { return p\Mbstring::mb_output_handler($string, $status); }
}
if (!function_exists('mb_http_input')) {
function mb_http_input($type = null) { return p\Mbstring::mb_http_input($type); }
}
if (!function_exists('mb_convert_variables')) {
function mb_convert_variables($to_encoding, $from_encoding, &...$vars) { return p\Mbstring::mb_convert_variables($to_encoding, $from_encoding, ...$vars); }
}
if (!function_exists('mb_ord')) {
function mb_ord($string, $encoding = null) { return p\Mbstring::mb_ord($string, $encoding); }
}
if (!function_exists('mb_chr')) {
function mb_chr($codepoint, $encoding = null) { return p\Mbstring::mb_chr($codepoint, $encoding); }
}
if (!function_exists('mb_scrub')) {
function mb_scrub($string, $encoding = null) { $encoding = null === $encoding ? mb_internal_encoding() : $encoding; return mb_convert_encoding($string, $encoding, $encoding); }
}
if (!function_exists('mb_str_split')) {
function mb_str_split($string, $length = 1, $encoding = null) { return p\Mbstring::mb_str_split($string, $length, $encoding); }
}
if (!function_exists('mb_str_pad')) {
function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); }
}
if (!function_exists('mb_ucfirst')) {
function mb_ucfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_ucfirst($string, $encoding); }
}
if (!function_exists('mb_lcfirst')) {
function mb_lcfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_lcfirst($string, $encoding); }
}
if (!function_exists('mb_trim')) {
function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_trim($string, $characters, $encoding); }
}
if (!function_exists('mb_ltrim')) {
function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_ltrim($string, $characters, $encoding); }
}
if (!function_exists('mb_rtrim')) {
function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_rtrim($string, $characters, $encoding); }
}
if (extension_loaded('mbstring')) {
return;
}
if (!defined('MB_CASE_UPPER')) {
define('MB_CASE_UPPER', 0);
}
if (!defined('MB_CASE_LOWER')) {
define('MB_CASE_LOWER', 1);
}
if (!defined('MB_CASE_TITLE')) {
define('MB_CASE_TITLE', 2);
}
return require __DIR__.'/bootstrap72.php';

View File

@@ -0,0 +1,173 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Polyfill\Mbstring as p;
if (!function_exists('mb_convert_encoding')) {
function mb_convert_encoding($string, $to_encoding, $from_encoding = null) { return p\Mbstring::mb_convert_encoding($string, $to_encoding, $from_encoding); }
}
if (!function_exists('mb_decode_mimeheader')) {
function mb_decode_mimeheader($string) { return p\Mbstring::mb_decode_mimeheader($string); }
}
if (!function_exists('mb_encode_mimeheader')) {
function mb_encode_mimeheader($string, $charset = null, $transfer_encoding = null, $newline = "\r\n", $indent = 0) { return p\Mbstring::mb_encode_mimeheader($string, $charset, $transfer_encoding, $newline, $indent); }
}
if (!function_exists('mb_decode_numericentity')) {
function mb_decode_numericentity($string, $map, $encoding = null) { return p\Mbstring::mb_decode_numericentity($string, $map, $encoding); }
}
if (!function_exists('mb_encode_numericentity')) {
function mb_encode_numericentity($string, $map, $encoding = null, $hex = false) { return p\Mbstring::mb_encode_numericentity($string, $map, $encoding, $hex); }
}
if (!function_exists('mb_convert_case')) {
function mb_convert_case($string, $mode, $encoding = null) { return p\Mbstring::mb_convert_case($string, $mode, $encoding); }
}
if (!function_exists('mb_internal_encoding')) {
function mb_internal_encoding($encoding = null) { return p\Mbstring::mb_internal_encoding($encoding); }
}
if (!function_exists('mb_language')) {
function mb_language($language = null) { return p\Mbstring::mb_language($language); }
}
if (!function_exists('mb_list_encodings')) {
function mb_list_encodings() { return p\Mbstring::mb_list_encodings(); }
}
if (!function_exists('mb_encoding_aliases')) {
function mb_encoding_aliases($encoding) { return p\Mbstring::mb_encoding_aliases($encoding); }
}
if (!function_exists('mb_check_encoding')) {
function mb_check_encoding($value = null, $encoding = null) { return p\Mbstring::mb_check_encoding($value, $encoding); }
}
if (!function_exists('mb_detect_encoding')) {
function mb_detect_encoding($string, $encodings = null, $strict = false) { return p\Mbstring::mb_detect_encoding($string, $encodings, $strict); }
}
if (!function_exists('mb_detect_order')) {
function mb_detect_order($encoding = null) { return p\Mbstring::mb_detect_order($encoding); }
}
if (!function_exists('mb_parse_str')) {
function mb_parse_str($string, &$result = []) { parse_str($string, $result); return (bool) $result; }
}
if (!function_exists('mb_strlen')) {
function mb_strlen($string, $encoding = null) { return p\Mbstring::mb_strlen($string, $encoding); }
}
if (!function_exists('mb_strpos')) {
function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strpos($haystack, $needle, $offset, $encoding); }
}
if (!function_exists('mb_strtolower')) {
function mb_strtolower($string, $encoding = null) { return p\Mbstring::mb_strtolower($string, $encoding); }
}
if (!function_exists('mb_strtoupper')) {
function mb_strtoupper($string, $encoding = null) { return p\Mbstring::mb_strtoupper($string, $encoding); }
}
if (!function_exists('mb_substitute_character')) {
function mb_substitute_character($substitute_character = null) { return p\Mbstring::mb_substitute_character($substitute_character); }
}
if (!function_exists('mb_substr')) {
function mb_substr($string, $start, $length = 2147483647, $encoding = null) { return p\Mbstring::mb_substr($string, $start, $length, $encoding); }
}
if (!function_exists('mb_stripos')) {
function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_stripos($haystack, $needle, $offset, $encoding); }
}
if (!function_exists('mb_stristr')) {
function mb_stristr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_stristr($haystack, $needle, $before_needle, $encoding); }
}
if (!function_exists('mb_strrchr')) {
function mb_strrchr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrchr($haystack, $needle, $before_needle, $encoding); }
}
if (!function_exists('mb_strrichr')) {
function mb_strrichr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrichr($haystack, $needle, $before_needle, $encoding); }
}
if (!function_exists('mb_strripos')) {
function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strripos($haystack, $needle, $offset, $encoding); }
}
if (!function_exists('mb_strrpos')) {
function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strrpos($haystack, $needle, $offset, $encoding); }
}
if (!function_exists('mb_strstr')) {
function mb_strstr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strstr($haystack, $needle, $before_needle, $encoding); }
}
if (!function_exists('mb_get_info')) {
function mb_get_info($type = 'all') { return p\Mbstring::mb_get_info($type); }
}
if (!function_exists('mb_http_output')) {
function mb_http_output($encoding = null) { return p\Mbstring::mb_http_output($encoding); }
}
if (!function_exists('mb_strwidth')) {
function mb_strwidth($string, $encoding = null) { return p\Mbstring::mb_strwidth($string, $encoding); }
}
if (!function_exists('mb_substr_count')) {
function mb_substr_count($haystack, $needle, $encoding = null) { return p\Mbstring::mb_substr_count($haystack, $needle, $encoding); }
}
if (!function_exists('mb_output_handler')) {
function mb_output_handler($string, $status) { return p\Mbstring::mb_output_handler($string, $status); }
}
if (!function_exists('mb_http_input')) {
function mb_http_input($type = null) { return p\Mbstring::mb_http_input($type); }
}
if (!function_exists('mb_convert_variables')) {
function mb_convert_variables($to_encoding, $from_encoding, &...$vars) { return p\Mbstring::mb_convert_variables($to_encoding, $from_encoding, ...$vars); }
}
if (!function_exists('mb_ord')) {
function mb_ord($string, $encoding = null) { return p\Mbstring::mb_ord($string, $encoding); }
}
if (!function_exists('mb_chr')) {
function mb_chr($codepoint, $encoding = null) { return p\Mbstring::mb_chr($codepoint, $encoding); }
}
if (!function_exists('mb_scrub')) {
function mb_scrub($string, $encoding = null) { return p\Mbstring::mb_scrub($string, $encoding); }
}
if (!function_exists('mb_str_split')) {
function mb_str_split($string, $length = 1, $encoding = null) { return p\Mbstring::mb_str_split($string, $length, $encoding); }
}
if (!function_exists('mb_str_pad')) {
/** @return string|false */
function mb_str_pad(?string $string, ?int $length, ?string $pad_string = ' ', ?int $pad_type = STR_PAD_RIGHT, ?string $encoding = null) { return p\Mbstring::mb_str_pad((string) $string, (int) $length, (string) $pad_string, (int) $pad_type, $encoding); }
}
if (!function_exists('mb_ucfirst')) {
/** @return string|false */
function mb_ucfirst(?string $string, ?string $encoding = null) { return p\Mbstring::mb_ucfirst((string) $string, $encoding); }
}
if (!function_exists('mb_lcfirst')) {
/** @return string|false */
function mb_lcfirst(?string $string, ?string $encoding = null) { return p\Mbstring::mb_lcfirst((string) $string, $encoding); }
}
if (!function_exists('mb_trim')) {
/** @return string|false */
function mb_trim(?string $string, ?string $characters = null, ?string $encoding = null) { return p\Mbstring::mb_trim((string) $string, $characters, $encoding); }
}
if (!function_exists('mb_ltrim')) {
/** @return string|false */
function mb_ltrim(?string $string, ?string $characters = null, ?string $encoding = null) { return p\Mbstring::mb_ltrim((string) $string, $characters, $encoding); }
}
if (!function_exists('mb_rtrim')) {
/** @return string|false */
function mb_rtrim(?string $string, ?string $characters = null, ?string $encoding = null) { return p\Mbstring::mb_rtrim((string) $string, $characters, $encoding); }
}
if (extension_loaded('mbstring')) {
return;
}
if (!defined('MB_CASE_UPPER')) {
define('MB_CASE_UPPER', 0);
}
if (!defined('MB_CASE_LOWER')) {
define('MB_CASE_LOWER', 1);
}
if (!defined('MB_CASE_TITLE')) {
define('MB_CASE_TITLE', 2);
}

View File

@@ -122,34 +122,34 @@ if (!function_exists('mb_chr')) {
function mb_chr(?int $codepoint, ?string $encoding = null): string|false { return p\Mbstring::mb_chr((int) $codepoint, $encoding); }
}
if (!function_exists('mb_scrub')) {
function mb_scrub(?string $string, ?string $encoding = null): string { $encoding ??= mb_internal_encoding(); return mb_convert_encoding((string) $string, $encoding, $encoding); }
function mb_scrub(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_scrub($string, $encoding); }
}
if (!function_exists('mb_str_split')) {
function mb_str_split(?string $string, ?int $length = 1, ?string $encoding = null): array { return p\Mbstring::mb_str_split((string) $string, (int) $length, $encoding); }
}
if (!function_exists('mb_str_pad')) {
function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); }
function mb_str_pad(?string $string, ?int $length, ?string $pad_string = ' ', ?int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad((string) $string, (int) $length, (string) $pad_string, (int) $pad_type, $encoding); }
}
if (!function_exists('mb_ucfirst')) {
function mb_ucfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_ucfirst($string, $encoding); }
function mb_ucfirst(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_ucfirst((string) $string, $encoding); }
}
if (!function_exists('mb_lcfirst')) {
function mb_lcfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_lcfirst($string, $encoding); }
function mb_lcfirst(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_lcfirst((string) $string, $encoding); }
}
if (!function_exists('mb_trim')) {
function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_trim($string, $characters, $encoding); }
function mb_trim(?string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_trim((string) $string, $characters, $encoding); }
}
if (!function_exists('mb_ltrim')) {
function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_ltrim($string, $characters, $encoding); }
function mb_ltrim(?string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_ltrim((string) $string, $characters, $encoding); }
}
if (!function_exists('mb_rtrim')) {
function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_rtrim($string, $characters, $encoding); }
function mb_rtrim(?string $string, ?string $characters = null, ?string $encoding = null): string { return p\Mbstring::mb_rtrim((string) $string, $characters, $encoding); }
}
if (extension_loaded('mbstring')) {

View File

@@ -32,7 +32,7 @@ final class Php83
}
if ($depth > self::JSON_MAX_DEPTH) {
throw new \ValueError(sprintf('json_validate(): Argument #2 ($depth) must be less than %d', self::JSON_MAX_DEPTH));
throw new \ValueError(\sprintf('json_validate(): Argument #2 ($depth) must be less than %d', self::JSON_MAX_DEPTH));
}
json_decode($json, true, $depth, $flags);
@@ -40,29 +40,38 @@ final class Php83
return \JSON_ERROR_NONE === json_last_error();
}
public static function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = \STR_PAD_RIGHT, ?string $encoding = null): string
/** @return string|false */
public static function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = \STR_PAD_RIGHT, ?string $encoding = null)
{
if (!\in_array($pad_type, [\STR_PAD_RIGHT, \STR_PAD_LEFT, \STR_PAD_BOTH], true)) {
throw new \ValueError('mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH');
}
if (null === $encoding) {
$encoding = mb_internal_encoding();
}
$errorToTrigger = null;
try {
$validEncoding = @mb_check_encoding('', $encoding);
if (!@mb_check_encoding('', $encoding)) {
$errorToTrigger = \sprintf('mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given', $encoding);
}
} catch (\ValueError $e) {
throw new \ValueError(sprintf('mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given', $encoding));
$errorToTrigger = \sprintf('mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given', $encoding);
}
// BC for PHP 7.3 and lower
if (!$validEncoding) {
throw new \ValueError(sprintf('mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given', $encoding));
if (null === $errorToTrigger && mb_strlen($pad_string, $encoding) <= 0) {
$errorToTrigger = 'mb_str_pad(): Argument #3 ($pad_string) must be a non-empty string';
}
if (mb_strlen($pad_string, $encoding) <= 0) {
throw new \ValueError('mb_str_pad(): Argument #3 ($pad_string) must be a non-empty string');
if (null === $errorToTrigger && !\in_array($pad_type, [\STR_PAD_RIGHT, \STR_PAD_LEFT, \STR_PAD_BOTH], true)) {
$errorToTrigger = 'mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH';
}
if (null !== $errorToTrigger) {
if (80000 > \PHP_VERSION_ID) {
trigger_error($errorToTrigger, \E_USER_WARNING);
return false;
}
throw new \ValueError($errorToTrigger);
}
$paddingRequired = $length - mb_strlen($string, $encoding);
@@ -94,34 +103,33 @@ final class Php83
throw new \ValueError('str_increment(): Argument #1 ($string) must be composed only of alphanumeric ASCII characters');
}
if (is_numeric($string)) {
$offset = stripos($string, 'e');
if (false !== $offset) {
$char = $string[$offset];
++$char;
$string[$offset] = $char;
++$string;
for ($i = \strlen($string) - 1; $i >= 0; --$i) {
$char = $string[$i];
switch ($string[$offset]) {
case 'f':
$string[$offset] = 'e';
break;
case 'F':
$string[$offset] = 'E';
break;
case 'g':
$string[$offset] = 'f';
break;
case 'G':
$string[$offset] = 'F';
break;
}
return $string;
if ('z' === $char) {
$string[$i] = 'a';
continue;
}
if ('Z' === $char) {
$string[$i] = 'A';
continue;
}
if ('9' === $char) {
$string[$i] = '0';
continue;
}
$string[$i] = \chr(\ord($char) + 1);
return $string;
}
return ++$string;
switch ($string[0]) {
case 'a': return 'a'.$string;
case 'A': return 'A'.$string;
}
return '1'.$string;
}
public static function str_decrement(string $string): string
@@ -135,7 +143,7 @@ final class Php83
}
if (preg_match('/\A(?:0[aA0]?|[aA])\z/', $string)) {
throw new \ValueError(sprintf('str_decrement(): Argument #1 ($string) "%s" is out of decrement range', $string));
throw new \ValueError(\sprintf('str_decrement(): Argument #1 ($string) "%s" is out of decrement range', $string));
}
if (!\in_array(substr($string, -1), ['A', 'a', '0'], true)) {

View File

@@ -19,12 +19,6 @@ if (!function_exists('json_validate')) {
function json_validate(string $json, int $depth = 512, int $flags = 0): bool { return p\Php83::json_validate($json, $depth, $flags); }
}
if (extension_loaded('mbstring')) {
if (!function_exists('mb_str_pad')) {
function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Php83::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); }
}
}
if (!function_exists('stream_context_set_options')) {
function stream_context_set_options($context, array $options): bool { return stream_context_set_option($context, $options); }
}
@@ -37,12 +31,22 @@ if (!function_exists('str_decrement')) {
function str_decrement(string $string): string { return p\Php83::str_decrement($string); }
}
if (\PHP_VERSION_ID < 80000) {
require __DIR__.'/bootstrap72.php';
}
if (extension_loaded('mbstring')) {
if (!function_exists('mb_str_pad')) {
function mb_str_pad(?string $string, ?int $length, ?string $pad_string = ' ', ?int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Php83::mb_str_pad((string) $string, (int) $length, (string) $pad_string, (int) $pad_type, $encoding); }
}
}
if (\PHP_VERSION_ID >= 80100) {
return require __DIR__.'/bootstrap81.php';
}
if (!function_exists('ldap_exop_sync') && function_exists('ldap_exop')) {
function ldap_exop_sync($ldap, string $request_oid, ?string $request_data = null, ?array $controls = null, &$response_data = null, &$response_oid = null): bool { return ldap_exop($ldap, $request_oid, $request_data, $controls, $response_data, $response_oid); }
function ldap_exop_sync($ldap, string $request_oid, ?string $request_data = null, ?array $controls = null, &$response_data = null, &$response_oid = null): bool { return ldap_exop($ldap, $request_oid, $request_data, $response_data, $response_oid); }
}
if (!function_exists('ldap_connect_wallet') && function_exists('ldap_connect')) {

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Polyfill\Php83 as p;
if (extension_loaded('mbstring')) {
if (!function_exists('mb_str_pad')) {
/** @return string|false */
function mb_str_pad(?string $string, ?int $length, ?string $pad_string = ' ', ?int $pad_type = STR_PAD_RIGHT, ?string $encoding = null) { return p\Php83::mb_str_pad((string) $string, (int) $length, (string) $pad_string, (int) $pad_type, $encoding); }
}
}

View File

@@ -9,12 +9,30 @@
* file that was distributed with this source code.
*/
use Symfony\Polyfill\Php83 as p;
if (\PHP_VERSION_ID >= 80300) {
return;
}
if (!function_exists('json_validate')) {
function json_validate(string $json, int $depth = 512, int $flags = 0): bool { return p\Php83::json_validate($json, $depth, $flags); }
}
if (!function_exists('str_increment')) {
function str_increment(string $string): string { return p\Php83::str_increment($string); }
}
if (!function_exists('str_decrement')) {
function str_decrement(string $string): string { return p\Php83::str_decrement($string); }
}
if (extension_loaded('mbstring') && !function_exists('mb_str_pad')) {
function mb_str_pad(?string $string, ?int $length, ?string $pad_string = ' ', ?int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Php83::mb_str_pad((string) $string, (int) $length, (string) $pad_string, (int) $pad_type, $encoding); }
}
if (!function_exists('ldap_exop_sync') && function_exists('ldap_exop')) {
function ldap_exop_sync(\LDAP\Connection $ldap, string $request_oid, ?string $request_data = null, ?array $controls = null, &$response_data = null, &$response_oid = null): bool { return ldap_exop($ldap, $request_oid, $request_data, $controls, $response_data, $response_oid); }
function ldap_exop_sync(\LDAP\Connection $ldap, string $request_oid, ?string $request_data = null, ?array $controls = null, &$response_data = null, &$response_oid = null): bool { return ldap_exop($ldap, $request_oid, $request_data, $response_data, $response_oid); }
}
if (!function_exists('ldap_connect_wallet') && function_exists('ldap_connect')) {

View File

@@ -19,7 +19,8 @@ namespace Symfony\Polyfill\Php84;
*/
final class Php84
{
public static function mb_ucfirst(string $string, ?string $encoding = null): string
/** @return string|false */
public static function mb_ucfirst(string $string, ?string $encoding = null)
{
if (null === $encoding) {
$encoding = mb_internal_encoding();
@@ -28,12 +29,17 @@ final class Php84
try {
$validEncoding = @mb_check_encoding('', $encoding);
} catch (\ValueError $e) {
throw new \ValueError(sprintf('mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding));
throw new \ValueError(\sprintf('mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding));
}
// BC for PHP 7.3 and lower
if (!$validEncoding) {
throw new \ValueError(sprintf('mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding));
if (80000 > \PHP_VERSION_ID) {
trigger_error(\sprintf('mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding), \E_USER_WARNING);
return false;
}
throw new \ValueError(\sprintf('mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding));
}
$firstChar = mb_substr($string, 0, 1, $encoding);
@@ -42,7 +48,8 @@ final class Php84
return $firstChar.mb_substr($string, 1, null, $encoding);
}
public static function mb_lcfirst(string $string, ?string $encoding = null): string
/** @return string|false */
public static function mb_lcfirst(string $string, ?string $encoding = null)
{
if (null === $encoding) {
$encoding = mb_internal_encoding();
@@ -51,12 +58,17 @@ final class Php84
try {
$validEncoding = @mb_check_encoding('', $encoding);
} catch (\ValueError $e) {
throw new \ValueError(sprintf('mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding));
throw new \ValueError(\sprintf('mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding));
}
// BC for PHP 7.3 and lower
if (!$validEncoding) {
throw new \ValueError(sprintf('mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding));
if (80000 > \PHP_VERSION_ID) {
trigger_error(\sprintf('mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding), \E_USER_WARNING);
return false;
}
throw new \ValueError(\sprintf('mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding));
}
$firstChar = mb_substr($string, 0, 1, $encoding);
@@ -114,22 +126,26 @@ final class Php84
return $num ** $exponent;
}
public static function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string
/** @return string|false */
public static function mb_trim(string $string, ?string $characters = null, ?string $encoding = null)
{
return self::mb_internal_trim('{^[%s]+|[%1$s]+$}Du', $string, $characters, $encoding, __FUNCTION__);
}
public static function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string
/** @return string|false */
public static function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null)
{
return self::mb_internal_trim('{^[%s]+}Du', $string, $characters, $encoding, __FUNCTION__);
}
public static function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string
/** @return string|false */
public static function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null)
{
return self::mb_internal_trim('{[%s]+$}Du', $string, $characters, $encoding, __FUNCTION__);
}
private static function mb_internal_trim(string $regex, string $string, ?string $characters, ?string $encoding, string $function): string
/** @return string|false */
private static function mb_internal_trim(string $regex, string $string, ?string $characters, ?string $encoding, string $function)
{
if (null === $encoding) {
$encoding = mb_internal_encoding();
@@ -138,12 +154,17 @@ final class Php84
try {
$validEncoding = @mb_check_encoding('', $encoding);
} catch (\ValueError $e) {
throw new \ValueError(sprintf('%s(): Argument #3 ($encoding) must be a valid encoding, "%s" given', $function, $encoding));
throw new \ValueError(\sprintf('%s(): Argument #3 ($encoding) must be a valid encoding, "%s" given', $function, $encoding));
}
// BC for PHP 7.3 and lower
if (!$validEncoding) {
throw new \ValueError(sprintf('%s(): Argument #3 ($encoding) must be a valid encoding, "%s" given', $function, $encoding));
if (80000 > \PHP_VERSION_ID) {
trigger_error(\sprintf('%s(): Argument #3 ($encoding) must be a valid encoding, "%s" given', $function, $encoding), \E_USER_WARNING);
return false;
}
throw new \ValueError(\sprintf('%s(): Argument #3 ($encoding) must be a valid encoding, "%s" given', $function, $encoding));
}
if ('' === $characters) {
@@ -166,7 +187,7 @@ final class Php84
$characters = preg_quote($characters);
}
$string = preg_replace(sprintf($regex, $characters), '', $string);
$string = preg_replace(\sprintf($regex, $characters), '', $string);
if ('UTF-8' === $encoding) {
return $string;
@@ -185,11 +206,11 @@ final class Php84
return [];
}
$regex = ((float) \PCRE_VERSION < 10 ? (float) \PCRE_VERSION >= 8.32 : (float) \PCRE_VERSION >= 10.39)
$regex = ((float) \PCRE_VERSION >= 10.44)
? '\X'
: '(?:\r\n|(?:[ -~\x{200C}\x{200D}]|[ᆨ-ᇹ]+|[ᄀ-]*(?:[가개갸걔거게겨계고과괘괴교구궈궤귀규그긔기까깨꺄꺠꺼께껴꼐꼬꽈꽤꾀꾜꾸꿔꿰뀌뀨끄끠끼나내냐냬너네녀녜노놔놰뇌뇨누눠눼뉘뉴느늬니다대댜댸더데뎌뎨도돠돼되됴두둬뒈뒤듀드듸디따때땨떄떠떼뗘뗴또똬뙈뙤뚀뚜뚸뛔뛰뜌뜨띄띠라래랴럐러레려례로롸뢔뢰료루뤄뤠뤼류르릐리마매먀먜머메며몌모뫄뫠뫼묘무뭐뭬뮈뮤므믜미바배뱌뱨버베벼볘보봐봬뵈뵤부붜붸뷔뷰브븨비빠빼뺘뺴뻐뻬뼈뼤뽀뽜뽸뾔뾰뿌뿨쀄쀠쀼쁘쁴삐사새샤섀서세셔셰소솨쇄쇠쇼수숴쉐쉬슈스싀시싸쌔쌰썌써쎄쎠쎼쏘쏴쐐쐬쑈쑤쒀쒜쒸쓔쓰씌씨아애야얘어에여예오와왜외요우워웨위유으의이자재쟈쟤저제져졔조좌좨죄죠주줘줴쥐쥬즈즤지짜째쨔쨰쩌쩨쪄쪠쪼쫘쫴쬐쬬쭈쭤쮀쮜쮸쯔쯰찌차채챠챼처체쳐쳬초촤쵀최쵸추춰췌취츄츠츼치카캐캬컈커케켜켸코콰쾌쾨쿄쿠쿼퀘퀴큐크킈키타태탸턔터테텨톄토톼퇘퇴툐투퉈퉤튀튜트틔티파패퍄퍠퍼페펴폐포퐈퐤푀표푸풔풰퓌퓨프픠피하해햐햬허헤혀혜호화홰회효후훠훼휘휴흐희히]?[-ᆢ]+|[가-힣])[ᆨ-ᇹ]*|[ᄀ-]+|[^\p{Cc}\p{Cf}\p{Zl}\p{Zp}])[\p{Mn}\p{Me}\x{09BE}\x{09D7}\x{0B3E}\x{0B57}\x{0BBE}\x{0BD7}\x{0CC2}\x{0CD5}\x{0CD6}\x{0D3E}\x{0D57}\x{0DCF}\x{0DDF}\x{200C}\x{200D}\x{1D165}\x{1D16E}-\x{1D172}]*|[\p{Cc}\p{Cf}\p{Zl}\p{Zp}])';
: '(?:\r\n|[\x{1F1E6}-\x{1F1FF}][\x{1F1E6}-\x{1F1FF}]?|(?:[ -~\x{200C}\x{200D}]|[ᆨ-ᇹ]+|[ᄀ-]*(?:[가개갸걔거게겨계고과괘괴교구궈궤귀규그긔기까깨꺄꺠꺼께껴꼐꼬꽈꽤꾀꾜꾸꿔꿰뀌뀨끄끠끼나내냐냬너네녀녜노놔놰뇌뇨누눠눼뉘뉴느늬니다대댜댸더데뎌뎨도돠돼되됴두둬뒈뒤듀드듸디따때땨떄떠떼뗘뗴또똬뙈뙤뚀뚜뚸뛔뛰뜌뜨띄띠라래랴럐러레려례로롸뢔뢰료루뤄뤠뤼류르릐리마매먀먜머메며몌모뫄뫠뫼묘무뭐뭬뮈뮤므믜미바배뱌뱨버베벼볘보봐봬뵈뵤부붜붸뷔뷰브븨비빠빼뺘뺴뻐뻬뼈뼤뽀뽜뽸뾔뾰뿌뿨쀄쀠쀼쁘쁴삐사새샤섀서세셔셰소솨쇄쇠쇼수숴쉐쉬슈스싀시싸쌔쌰썌써쎄쎠쎼쏘쏴쐐쐬쑈쑤쒀쒜쒸쓔쓰씌씨아애야얘어에여예오와왜외요우워웨위유으의이자재쟈쟤저제져졔조좌좨죄죠주줘줴쥐쥬즈즤지짜째쨔쨰쩌쩨쪄쪠쪼쫘쫴쬐쬬쭈쭤쮀쮜쮸쯔쯰찌차채챠챼처체쳐쳬초촤쵀최쵸추춰췌취츄츠츼치카캐캬컈커케켜켸코콰쾌쾨쿄쿠쿼퀘퀴큐크킈키타태탸턔터테텨톄토톼퇘퇴툐투퉈퉤튀튜트틔티파패퍄퍠퍼페펴폐포퐈퐤푀표푸풔풰퓌퓨프픠피하해햐햬허헤혀혜호화홰회효후훠훼휘휴흐희히]?[-ᆢ]+|[가-힣])[ᆨ-ᇹ]*|[ᄀ-]+|[^\p{Cc}\p{Cf}\p{Zl}\p{Zp}])[\p{Mn}\p{Mc}\p{Me}\x{09BE}\x{09D7}\x{0B3E}\x{0B57}\x{0BBE}\x{0BD7}\x{0CC2}\x{0CD5}\x{0CD6}\x{0D3E}\x{0D57}\x{0DCF}\x{0DDF}\x{200C}\x{1D165}\x{1D16E}-\x{1D172}\x{1F3FB}-\x{1F3FF}\x{FE0E}-\x{FE0F}\x{E0020}-\x{E007F}]*(?:\x{200D}(?:[\x{1F1E6}-\x{1F1FF}]|[ -~\x{200C}\x{200D}]|[ᆨ-ᇹ]+|[ᄀ-]*(?:[가-힣]?[-ᆢ]+|[가-힣])[ᆨ-ᇹ]*|[ᄀ-]+|[^\p{Cc}\p{Cf}\p{Zl}\p{Zp}])[\p{Mn}\p{Mc}\p{Me}\x{09BE}\x{09D7}\x{0B3E}\x{0B57}\x{0BBE}\x{0BD7}\x{0CC2}\x{0CD5}\x{0CD6}\x{0D3E}\x{0D57}\x{0DCF}\x{0DDF}\x{200C}\x{1D165}\x{1D16E}-\x{1D172}\x{1F3FB}-\x{1F3FF}\x{FE0E}-\x{FE0F}\x{E0020}-\x{E007F}]*)*|[\p{Cc}\p{Cf}\p{Zl}\p{Zp}])';
if (!preg_match_all('/'. $regex .'/u', $string, $matches)) {
if (!preg_match_all('/'.$regex.'/u', $string, $matches)) {
return false;
}
@@ -205,13 +226,219 @@ final class Php84
return $chunks;
}
public static function bcceil(string $num): string
{
if (!is_numeric($num)) {
throw new \ValueError('bcceil(): Argument #1 ($num) is not well-formed');
}
return self::bcround($num, 0, \RoundingMode::PositiveInfinity);
}
public static function bcdivmod(string $num1, string $num2, ?int $scale = null): ?array
{
if (null === $quot = \bcdiv($num1, $num2, 0)) {
return null;
if (null === $quot = @bcdiv($num1, $num2, 0)) {
throw new \DivisionByZeroError('Division by zero');
}
$scale = $scale ?? (\PHP_VERSION_ID >= 70300 ? \bcscale() : (ini_get('bcmath.scale') ?: 0));
$scale = $scale ?? (\PHP_VERSION_ID >= 70300 ? bcscale() : (\ini_get('bcmath.scale') ?: 0));
return [$quot, \bcmod($num1, $num2, $scale)];
return [$quot, bcmod($num1, $num2, $scale)];
}
public static function bcfloor(string $num): string
{
if (!is_numeric($num)) {
throw new \ValueError('bcfloor(): Argument #1 ($num) is not well-formed');
}
return self::bcround($num, 0, \RoundingMode::NegativeInfinity);
}
/**
* @param \RoundingMode|\RoundingMode::* $mode
*/
public static function bcround(string $num, int $precision = 0, $mode = \RoundingMode::HalfAwayFromZero): string
{
if (!is_numeric($num)) {
throw new \ValueError('bcround(): Argument #1 ($num) is not well-formed');
}
$sign = 1;
if ('' !== $num && ('-' === $num[0] || '+' === $num[0])) {
if ('-' === $num[0]) {
$sign = -1;
}
$num = substr($num, 1);
}
if (false !== strpos($num, '.')) {
[$intPart, $fracPart] = array_pad(explode('.', $num, 2), 2, '');
} else {
$intPart = $num;
$fracPart = '';
}
if ('' === $intPart) {
$intPart = '0';
}
$intPart = self::trimLeadingZeros($intPart);
$fracPart = (string) $fracPart;
if ($precision >= 0) {
$fracLength = \strlen($fracPart);
if ($precision <= $fracLength) {
$scaledInt = $intPart.(string) substr($fracPart, 0, $precision);
$scaledFrac = (string) substr($fracPart, $precision);
} else {
$scaledInt = $intPart.$fracPart.str_repeat('0', $precision - $fracLength);
$scaledFrac = '';
}
} else {
$shift = -$precision;
$intLength = \strlen($intPart);
if ($shift <= $intLength) {
$splitPos = $intLength - $shift;
$scaledInt = substr($intPart, 0, $splitPos);
$scaledInt = '' === $scaledInt ? '0' : $scaledInt;
$scaledFrac = substr($intPart, $splitPos).$fracPart;
} else {
$scaledInt = '0';
$scaledFrac = str_repeat('0', $shift - $intLength).$intPart.$fracPart;
}
}
$roundedInt = self::roundIntegerPart($scaledInt, $scaledFrac, $sign, $mode);
$isZero = '' === trim($roundedInt, '0');
$absResult = self::formatRoundedDigits($roundedInt, $precision);
if (-1 === $sign && !$isZero) {
$absResult = '-'.$absResult;
}
return $absResult;
}
private static function roundIntegerPart(string $intPart, string $fracPart, int $sign, $mode): string
{
$intPart = self::trimLeadingZeros($intPart);
if ('' === $fracPart || '' === trim($fracPart, '0')) {
return $intPart;
}
$firstDigit = $fracPart[0];
$tail = (string) substr($fracPart, 1);
$tailNonZero = '' !== trim($tail, '0');
$isGreaterThanHalf = $firstDigit > '5' || ('5' === $firstDigit && $tailNonZero);
$isExactlyHalf = '5' === $firstDigit && !$tailNonZero;
$shouldIncrease = false;
switch ($mode) {
case \RoundingMode::TowardsZero:
break;
case \RoundingMode::AwayFromZero:
$shouldIncrease = true;
break;
case \RoundingMode::PositiveInfinity:
$shouldIncrease = $sign > 0;
break;
case \RoundingMode::NegativeInfinity:
$shouldIncrease = $sign < 0;
break;
case \RoundingMode::HalfAwayFromZero:
$shouldIncrease = $isGreaterThanHalf || $isExactlyHalf;
break;
case \RoundingMode::HalfTowardsZero:
$shouldIncrease = $isGreaterThanHalf;
break;
case \RoundingMode::HalfEven:
if ($isGreaterThanHalf) {
$shouldIncrease = true;
} elseif ($isExactlyHalf && 1 === self::lastDigit($intPart) % 2) {
$shouldIncrease = true;
}
break;
case \RoundingMode::HalfOdd:
if ($isGreaterThanHalf) {
$shouldIncrease = true;
} elseif ($isExactlyHalf && 0 === self::lastDigit($intPart) % 2) {
$shouldIncrease = true;
}
break;
}
if ($shouldIncrease) {
$intPart = self::incrementDigits($intPart);
}
return self::trimLeadingZeros($intPart);
}
private static function formatRoundedDigits(string $roundedInt, int $precision): string
{
if ($precision > 0) {
if (\strlen($roundedInt) <= $precision) {
$roundedInt = str_pad($roundedInt, $precision + 1, '0', \STR_PAD_LEFT);
}
$intDigits = substr($roundedInt, 0, -$precision);
$fracDigits = substr($roundedInt, -$precision);
$intDigits = self::trimLeadingZeros('' === $intDigits ? '0' : $intDigits);
$fracDigits = str_pad($fracDigits, $precision, '0', \STR_PAD_LEFT);
return $intDigits.'.'.$fracDigits;
}
if (0 === $precision) {
return self::trimLeadingZeros($roundedInt);
}
$shift = -$precision;
$digits = $roundedInt.str_repeat('0', $shift);
return self::trimLeadingZeros($digits);
}
private static function incrementDigits(string $digits): string
{
$digits = '' === $digits ? '0' : $digits;
$index = \strlen($digits) - 1;
$result = $digits;
$carry = 1;
while ($index >= 0 && $carry) {
$value = \ord($result[$index]) - 48 + $carry;
$carry = $value >= 10 ? 1 : 0;
$result[$index] = \chr(48 + ($value % 10));
--$index;
}
return $carry ? '1'.$result : $result;
}
private static function trimLeadingZeros(string $digits): string
{
$digits = ltrim($digits, '0');
return '' === $digits ? '0' : $digits;
}
private static function lastDigit(string $digits): int
{
$length = \strlen($digits);
return $length ? \ord($digits[$length - 1]) - 48 : 0;
}
}

View File

@@ -11,6 +11,7 @@ This component provides features added to PHP 8.4 core:
- [`grapheme_str_split`](https://wiki.php.net/rfc/grapheme_str_split)
- [`mb_trim`, `mb_ltrim` and `mb_rtrim`](https://wiki.php.net/rfc/mb_trim)
- [`mb_ucfirst` and `mb_lcfirst`](https://wiki.php.net/rfc/mb_ucfirst)
- [`PDO` driver specific sub-classes](https://wiki.php.net/rfc/pdo_driver_specific_subclasses)
- [`ReflectionConstant`](https://github.com/php/php-src/pull/13669)
More information can be found in the

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80400) {
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION | Attribute::TARGET_CLASS_CONSTANT)]
final class Deprecated
{
public readonly ?string $message;
public readonly ?string $since;
public function __construct(?string $message = null, ?string $since = null)
{
$this->message = $message;
$this->since = $since;
}
}
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80400) {
enum RoundingMode
{
case HalfAwayFromZero;
case HalfTowardsZero;
case HalfEven;
case HalfOdd;
case TowardsZero;
case AwayFromZero;
case NegativeInfinity;
case PositiveInfinity;
}
}

View File

@@ -9,12 +9,19 @@
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80400) {
if (\PHP_VERSION_ID < 80100) {
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION | Attribute::TARGET_CLASS_CONSTANT)]
final class Deprecated
{
public readonly ?string $message;
public readonly ?string $since;
/**
* @readonly
*/
public ?string $message;
/**
* @readonly
*/
public ?string $since;
public function __construct(?string $message = null, ?string $since = null)
{
@@ -22,4 +29,6 @@ if (\PHP_VERSION_ID < 80400) {
$this->since = $since;
}
}
} elseif (\PHP_VERSION_ID < 80400) {
require dirname(__DIR__).'/Deprecated.php';
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Pdo;
if (\PHP_VERSION_ID < 80400 && \extension_loaded('pdo_dblib')) {
class Dblib extends \PDO
{
public const ATTR_CONNECTION_TIMEOUT = \PDO::DBLIB_ATTR_CONNECTION_TIMEOUT;
public const ATTR_QUERY_TIMEOUT = \PDO::DBLIB_ATTR_QUERY_TIMEOUT;
public const ATTR_STRINGIFY_UNIQUEIDENTIFIER = \PDO::DBLIB_ATTR_STRINGIFY_UNIQUEIDENTIFIER;
public const ATTR_VERSION = \PDO::DBLIB_ATTR_VERSION;
public const ATTR_TDS_VERSION = \PHP_VERSION_ID >= 70300 ? \PDO::DBLIB_ATTR_TDS_VERSION : 1004;
public const ATTR_SKIP_EMPTY_ROWSETS = \PHP_VERSION_ID >= 70300 ? \PDO::DBLIB_ATTR_SKIP_EMPTY_ROWSETS : 1005;
public const ATTR_DATETIME_CONVERT = \PHP_VERSION_ID >= 70300 ? \PDO::DBLIB_ATTR_DATETIME_CONVERT : 1006;
public function __construct(string $dsn, ?string $username = null, ?string $password = null, ?array $options = null)
{
parent::__construct($dsn, $username, $password, $options);
if ('dblib' !== $driver = $this->getAttribute(\PDO::ATTR_DRIVER_NAME)) {
throw new \PDOException(\sprintf('Pdo\Dblib::__construct() cannot be used for connecting to the "%s" driver', $driver));
}
}
public static function connect(string $dsn, ?string $username = null, ?string $password = null, ?array $options = null): self
{
try {
return new self($dsn, $username, $password, $options);
} catch (\PDOException $e) {
throw preg_match('/^Pdo\\\\Dblib::__construct\(\) cannot be used for connecting to the "([a-z]+)" driver/', $e->getMessage(), $matches) ? new \PDOException(\sprintf('Pdo\Dblib::connect() cannot be used for connecting to the "%s" driver', $matches[1])) : $e;
}
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Pdo;
if (\PHP_VERSION_ID < 80400 && \extension_loaded('pdo_firebird')) {
class Firebird extends \PDO
{
public const ATTR_DATE_FORMAT = \PDO::FB_ATTR_DATE_FORMAT;
public const ATTR_TIME_FORMAT = \PDO::FB_ATTR_TIME_FORMAT;
public const ATTR_TIMESTAMP_FORMAT = \PDO::FB_ATTR_TIMESTAMP_FORMAT;
public function __construct(string $dsn, ?string $username = null, ?string $password = null, ?array $options = null)
{
parent::__construct($dsn, $username, $password, $options);
if ('firebird' !== $driver = $this->getAttribute(\PDO::ATTR_DRIVER_NAME)) {
throw new \PDOException(\sprintf('Pdo\Firebird::__construct() cannot be used for connecting to the "%s" driver', $driver));
}
}
public static function connect(string $dsn, ?string $username = null, ?string $password = null, ?array $options = null): self
{
try {
return new self($dsn, $username, $password, $options);
} catch (\PDOException $e) {
throw preg_match('/^Pdo\\\\Firebird::__construct\(\) cannot be used for connecting to the "([a-z]+)" driver/', $e->getMessage(), $matches) ? new \PDOException(\sprintf('Pdo\Firebird::connect() cannot be used for connecting to the "%s" driver', $matches[1])) : $e;
}
}
}
}

View File

@@ -0,0 +1,140 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Pdo;
use PDO;
if (\PHP_VERSION_ID < 80400 && \extension_loaded('pdo_mysql')) {
// Feature detection for non-mysqlnd; see also https://www.php.net/manual/en/class.pdo-mysql.php#pdo-mysql.constants.attr-max-buffer-size
if (\defined('PDO::MYSQL_ATTR_MAX_BUFFER_SIZE') && \defined('PDO::MYSQL_ATTR_READ_DEFAULT_FILE') && \defined('PDO::MYSQL_ATTR_READ_DEFAULT_GROUP')) {
class Mysql extends \PDO
{
public const ATTR_COMPRESS = \PDO::MYSQL_ATTR_COMPRESS;
public const ATTR_DIRECT_QUERY = \PDO::MYSQL_ATTR_DIRECT_QUERY;
public const ATTR_FOUND_ROWS = \PDO::MYSQL_ATTR_FOUND_ROWS;
public const ATTR_IGNORE_SPACE = \PDO::MYSQL_ATTR_IGNORE_SPACE;
public const ATTR_INIT_COMMAND = \PDO::MYSQL_ATTR_INIT_COMMAND;
public const ATTR_LOCAL_INFILE = \PDO::MYSQL_ATTR_LOCAL_INFILE;
public const ATTR_LOCAL_INFILE_DIRECTORY = \PHP_VERSION_ID >= 80100 ? \PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY : 1015;
public const ATTR_MAX_BUFFER_SIZE = \PDO::MYSQL_ATTR_MAX_BUFFER_SIZE;
public const ATTR_MULTI_STATEMENTS = \PDO::MYSQL_ATTR_MULTI_STATEMENTS;
public const ATTR_READ_DEFAULT_FILE = \PDO::MYSQL_ATTR_READ_DEFAULT_FILE;
public const ATTR_READ_DEFAULT_GROUP = \PDO::MYSQL_ATTR_READ_DEFAULT_GROUP;
public const ATTR_SERVER_PUBLIC_KEY = \PDO::MYSQL_ATTR_SERVER_PUBLIC_KEY;
public const ATTR_SSL_CA = \PDO::MYSQL_ATTR_SSL_CA;
public const ATTR_SSL_CAPATH = \PDO::MYSQL_ATTR_SSL_CAPATH;
public const ATTR_SSL_CERT = \PDO::MYSQL_ATTR_SSL_CERT;
public const ATTR_SSL_CIPHER = \PDO::MYSQL_ATTR_SSL_CIPHER;
public const ATTR_SSL_KEY = \PDO::MYSQL_ATTR_SSL_KEY;
public const ATTR_USE_BUFFERED_QUERY = \PDO::MYSQL_ATTR_USE_BUFFERED_QUERY;
public function __construct(string $dsn, ?string $username = null, ?string $password = null, ?array $options = null)
{
parent::__construct($dsn, $username, $password, $options);
if ('mysql' !== $driver = $this->getAttribute(\PDO::ATTR_DRIVER_NAME)) {
throw new \PDOException(\sprintf('Pdo\Mysql::__construct() cannot be used for connecting to the "%s" driver', $driver));
}
}
public static function connect(string $dsn, ?string $username = null, ?string $password = null, ?array $options = null): self
{
try {
return new self($dsn, $username, $password, $options);
} catch (\PDOException $e) {
throw preg_match('/^Pdo\\\\Mysql::__construct\(\) cannot be used for connecting to the "([a-z]+)" driver/', $e->getMessage(), $matches) ? new \PDOException(\sprintf('Pdo\Mysql::connect() cannot be used for connecting to the "%s" driver', $matches[1])) : $e;
}
}
}
} elseif (\defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')) {
class Mysql extends \PDO
{
public const ATTR_COMPRESS = \PDO::MYSQL_ATTR_COMPRESS;
public const ATTR_DIRECT_QUERY = \PDO::MYSQL_ATTR_DIRECT_QUERY;
public const ATTR_FOUND_ROWS = \PDO::MYSQL_ATTR_FOUND_ROWS;
public const ATTR_IGNORE_SPACE = \PDO::MYSQL_ATTR_IGNORE_SPACE;
public const ATTR_INIT_COMMAND = \PDO::MYSQL_ATTR_INIT_COMMAND;
public const ATTR_LOCAL_INFILE = \PDO::MYSQL_ATTR_LOCAL_INFILE;
public const ATTR_LOCAL_INFILE_DIRECTORY = \PHP_VERSION_ID >= 80100 ? \PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY : 1015;
// public const ATTR_MAX_BUFFER_SIZE = PDO::MYSQL_ATTR_MAX_BUFFER_SIZE; // disabled for mysqlnd
public const ATTR_MULTI_STATEMENTS = \PDO::MYSQL_ATTR_MULTI_STATEMENTS;
// public const ATTR_READ_DEFAULT_FILE = PDO::MYSQL_ATTR_READ_DEFAULT_FILE; // disabled for mysqlnd
// public const ATTR_READ_DEFAULT_GROUP = PDO::MYSQL_ATTR_READ_DEFAULT_GROUP; // disabled for mysqlnd
public const ATTR_SERVER_PUBLIC_KEY = \PDO::MYSQL_ATTR_SERVER_PUBLIC_KEY;
public const ATTR_SSL_CA = \PDO::MYSQL_ATTR_SSL_CA;
public const ATTR_SSL_CAPATH = \PDO::MYSQL_ATTR_SSL_CAPATH;
public const ATTR_SSL_CERT = \PDO::MYSQL_ATTR_SSL_CERT;
public const ATTR_SSL_CIPHER = \PDO::MYSQL_ATTR_SSL_CIPHER;
public const ATTR_SSL_KEY = \PDO::MYSQL_ATTR_SSL_KEY;
public const ATTR_SSL_VERIFY_SERVER_CERT = \PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT;
public const ATTR_USE_BUFFERED_QUERY = \PDO::MYSQL_ATTR_USE_BUFFERED_QUERY;
public function __construct(string $dsn, ?string $username = null, ?string $password = null, ?array $options = null)
{
parent::__construct($dsn, $username, $password, $options);
if ('mysql' !== $driver = $this->getAttribute(\PDO::ATTR_DRIVER_NAME)) {
throw new \PDOException(\sprintf('Pdo\Mysql::__construct() cannot be used for connecting to the "%s" driver', $driver));
}
}
public static function connect(string $dsn, ?string $username = null, ?string $password = null, ?array $options = null): self
{
try {
return new self($dsn, $username, $password, $options);
} catch (\PDOException $e) {
throw preg_match('/^Pdo\\\\Mysql::__construct\(\) cannot be used for connecting to the "([a-z]+)" driver/', $e->getMessage(), $matches) ? new \PDOException(\sprintf('Pdo\Mysql::connect() cannot be used for connecting to the "%s" driver', $matches[1])) : $e;
}
}
}
} else {
class Mysql extends \PDO
{
public const ATTR_COMPRESS = \PDO::MYSQL_ATTR_COMPRESS;
public const ATTR_DIRECT_QUERY = \PDO::MYSQL_ATTR_DIRECT_QUERY;
public const ATTR_FOUND_ROWS = \PDO::MYSQL_ATTR_FOUND_ROWS;
public const ATTR_IGNORE_SPACE = \PDO::MYSQL_ATTR_IGNORE_SPACE;
public const ATTR_INIT_COMMAND = \PDO::MYSQL_ATTR_INIT_COMMAND;
public const ATTR_LOCAL_INFILE = \PDO::MYSQL_ATTR_LOCAL_INFILE;
public const ATTR_LOCAL_INFILE_DIRECTORY = \PHP_VERSION_ID >= 80100 ? \PDO::MYSQL_ATTR_LOCAL_INFILE_DIRECTORY : 1015;
// public const ATTR_MAX_BUFFER_SIZE = PDO::MYSQL_ATTR_MAX_BUFFER_SIZE; // disabled for mysqlnd
public const ATTR_MULTI_STATEMENTS = \PDO::MYSQL_ATTR_MULTI_STATEMENTS;
// public const ATTR_READ_DEFAULT_FILE = PDO::MYSQL_ATTR_READ_DEFAULT_FILE; // disabled for mysqlnd
// public const ATTR_READ_DEFAULT_GROUP = PDO::MYSQL_ATTR_READ_DEFAULT_GROUP; // disabled for mysqlnd
public const ATTR_SERVER_PUBLIC_KEY = \PDO::MYSQL_ATTR_SERVER_PUBLIC_KEY;
public const ATTR_SSL_CA = \PDO::MYSQL_ATTR_SSL_CA;
public const ATTR_SSL_CAPATH = \PDO::MYSQL_ATTR_SSL_CAPATH;
public const ATTR_SSL_CERT = \PDO::MYSQL_ATTR_SSL_CERT;
public const ATTR_SSL_CIPHER = \PDO::MYSQL_ATTR_SSL_CIPHER;
public const ATTR_SSL_KEY = \PDO::MYSQL_ATTR_SSL_KEY;
public const ATTR_USE_BUFFERED_QUERY = \PDO::MYSQL_ATTR_USE_BUFFERED_QUERY;
public function __construct(string $dsn, ?string $username = null, ?string $password = null, ?array $options = null)
{
parent::__construct($dsn, $username, $password, $options);
if ('mysql' !== $driver = $this->getAttribute(\PDO::ATTR_DRIVER_NAME)) {
throw new \PDOException(\sprintf('Pdo\Mysql::__construct() cannot be used for connecting to the "%s" driver', $driver));
}
}
public static function connect(string $dsn, ?string $username = null, ?string $password = null, ?array $options = null): self
{
try {
return new self($dsn, $username, $password, $options);
} catch (\PDOException $e) {
throw preg_match('/^Pdo\\\\Mysql::__construct\(\) cannot be used for connecting to the "([a-z]+)" driver/', $e->getMessage(), $matches) ? new \PDOException(\sprintf('Pdo\Mysql::connect() cannot be used for connecting to the "%s" driver', $matches[1])) : $e;
}
}
}
}
}

View File

@@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Pdo;
if (\PHP_VERSION_ID < 80400 && \extension_loaded('pdo_odbc')) {
class Odbc extends \PDO
{
public const ATTR_USE_CURSOR_LIBRARY = \PDO::ODBC_ATTR_USE_CURSOR_LIBRARY;
public const ATTR_ASSUME_UTF8 = \PDO::ODBC_ATTR_ASSUME_UTF8;
public const SQL_USE_IF_NEEDED = \PDO::ODBC_SQL_USE_IF_NEEDED;
public const SQL_USE_DRIVER = \PDO::ODBC_SQL_USE_DRIVER;
public const SQL_USE_ODBC = \PDO::ODBC_SQL_USE_ODBC;
public function __construct(string $dsn, ?string $username = null, ?string $password = null, ?array $options = null)
{
parent::__construct($dsn, $username, $password, $options);
if ('odbc' !== $driver = $this->getAttribute(\PDO::ATTR_DRIVER_NAME)) {
throw new \PDOException(\sprintf('Pdo\Odbc::__construct() cannot be used for connecting to the "%s" driver', $driver));
}
}
public static function connect(string $dsn, ?string $username = null, ?string $password = null, ?array $options = null): self
{
try {
return new self($dsn, $username, $password, $options);
} catch (\PDOException $e) {
throw preg_match('/^Pdo\\\\Odbc::__construct\(\) cannot be used for connecting to the "([a-z]+)" driver/', $e->getMessage(), $matches) ? new \PDOException(\sprintf('Pdo\Odbc::connect() cannot be used for connecting to the "%s" driver', $matches[1])) : $e;
}
}
}
}

View File

@@ -0,0 +1,94 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Pdo;
if (\PHP_VERSION_ID < 80400 && \extension_loaded('pdo_pgsql')) {
class Pgsql extends \PDO
{
public const ATTR_DISABLE_PREPARES = \PDO::PGSQL_ATTR_DISABLE_PREPARES;
public function __construct(string $dsn, ?string $username = null, ?string $password = null, ?array $options = null)
{
parent::__construct($dsn, $username, $password, $options);
if ('pgsql' !== $driver = $this->getAttribute(\PDO::ATTR_DRIVER_NAME)) {
throw new \PDOException(\sprintf('Pdo\Pgsql::__construct() cannot be used for connecting to the "%s" driver', $driver));
}
}
public static function connect(string $dsn, ?string $username = null, ?string $password = null, ?array $options = null): self
{
try {
return new self($dsn, $username, $password, $options);
} catch (\PDOException $e) {
throw preg_match('/^Pdo\\\\Pgsql::__construct\(\) cannot be used for connecting to the "([a-z]+)" driver/', $e->getMessage(), $matches) ? new \PDOException(\sprintf('Pdo\Pgsql::connect() cannot be used for connecting to the "%s" driver', $matches[1])) : $e;
}
}
public function copyFromArray(string $tableName, array $rows, string $separator = "\t", string $nullAs = '\\\\N', ?string $fields = null): bool
{
return $this->pgsqlCopyFromArray($tableName, $rows, $separator, $nullAs, $fields);
}
public function copyFromFile(string $tableName, string $filename, string $separator = "\t", string $nullAs = '\\\\N', ?string $fields = null): bool
{
return $this->pgsqlCopyFromFile($tableName, $filename, $separator, $nullAs, $fields);
}
/**
* @return array|false
*/
public function copyToArray(string $tableName, string $separator = "\t", string $nullAs = '\\\\N', ?string $fields = null)
{
return $this->pgsqlCopyToArray($tableName, $separator, $nullAs, $fields);
}
public function copyToFile(string $tableName, string $filename, string $separator = "\t", string $nullAs = '\\\\N', ?string $fields = null): bool
{
return $this->pgsqlCopyToFile($tableName, $filename, $separator, $nullAs, $fields);
}
/**
* @return array|false
*/
public function getNotify(int $fetchMode = \PDO::FETCH_DEFAULT, int $timeoutMilliseconds = 0)
{
return $this->pgsqlGetNotify($fetchMode, $timeoutMilliseconds);
}
public function getPid(): int
{
return $this->pgsqlGetPid();
}
/**
* @return string|false
*/
public function lobCreate()
{
return $this->pgsqlLOBCreate();
}
/**
* @return resource|false
*/
public function lobOpen(string $oid, string $mode = 'rb')
{
return $this->pgsqlLOBOpen($oid, $mode);
}
public function lobUnlink(string $oid): bool
{
return $this->pgsqlLOBUnlink($oid);
}
}
}

View File

@@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Pdo;
if (\PHP_VERSION_ID < 80400 && \extension_loaded('pdo_sqlite')) {
class Sqlite extends \PDO
{
public const ATTR_EXTENDED_RESULT_CODES = \PHP_VERSION_ID >= 70400 ? \PDO::SQLITE_ATTR_EXTENDED_RESULT_CODES : 1002;
public const ATTR_OPEN_FLAGS = \PHP_VERSION_ID >= 70300 ? \PDO::SQLITE_ATTR_OPEN_FLAGS : 1000;
public const ATTR_READONLY_STATEMENT = \PHP_VERSION_ID >= 70400 ? \PDO::SQLITE_ATTR_READONLY_STATEMENT : 1001;
public const DETERMINISTIC = \PDO::SQLITE_DETERMINISTIC;
public const OPEN_READONLY = \PHP_VERSION_ID >= 70300 ? \PDO::SQLITE_OPEN_READONLY : 1;
public const OPEN_READWRITE = \PHP_VERSION_ID >= 70300 ? \PDO::SQLITE_OPEN_READWRITE : 2;
public const OPEN_CREATE = \PHP_VERSION_ID >= 70300 ? \PDO::SQLITE_OPEN_CREATE : 4;
public function __construct(string $dsn, ?string $username = null, ?string $password = null, ?array $options = null)
{
parent::__construct($dsn, $username, $password, $options);
if ('sqlite' !== $driver = $this->getAttribute(\PDO::ATTR_DRIVER_NAME)) {
throw new \PDOException(\sprintf('Pdo\Sqlite::__construct() cannot be used for connecting to the "%s" driver', $driver));
}
}
public static function connect(string $dsn, ?string $username = null, ?string $password = null, ?array $options = null): self
{
try {
return new self($dsn, $username, $password, $options);
} catch (\PDOException $e) {
throw preg_match('/^Pdo\\\\Sqlite::__construct\(\) cannot be used for connecting to the "([a-z]+)" driver/', $e->getMessage(), $matches) ? new \PDOException(\sprintf('Pdo\Sqlite::connect() cannot be used for connecting to the "%s" driver', $matches[1])) : $e;
}
}
public function createAggregate(string $name, callable $step, callable $finalize, int $numArgs = -1): bool
{
return $this->sqliteCreateAggregate($name, $step, $finalize, $numArgs);
}
public function createCollation(string $name, callable $callback): bool
{
return $this->sqliteCreateCollation($name, $callback);
}
public function createFunction(string $function_name, callable $callback, int $num_args = -1, int $flags = 0): bool
{
return $this->sqliteCreateFunction($function_name, $callback, $num_args, $flags);
}
}
}

View File

@@ -10,9 +10,7 @@
*/
if (\PHP_VERSION_ID < 80400) {
/**
* @author Daniel Scherzer <daniel.e.scherzer@gmail.com>
*/
// @author Daniel Scherzer <daniel.e.scherzer@gmail.com>
final class ReflectionConstant
{
/**
@@ -24,6 +22,7 @@ if (\PHP_VERSION_ID < 80400) {
private $value;
private $deprecated;
private $persistent;
private static $persistentConstants = [];

View File

@@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80100) {
final class RoundingMode
{
const HalfAwayFromZero = 0;
const HalfTowardsZero = 1;
const HalfEven = 2;
const HalfOdd = 3;
const TowardsZero = 4;
const AwayFromZero = 5;
const NegativeInfinity = 6;
const PositiveInfinity = 7;
private function __construct()
{
}
public static function cases(): array
{
return [
self::HalfAwayFromZero,
self::HalfTowardsZero,
self::HalfEven,
self::HalfOdd,
self::TowardsZero,
self::AwayFromZero,
self::NegativeInfinity,
self::PositiveInfinity,
];
}
}
} elseif (\PHP_VERSION_ID < 80400) {
require dirname(__DIR__).'/RoundingMode.php';
}

View File

@@ -15,13 +15,17 @@ if (\PHP_VERSION_ID >= 80400) {
return;
}
if (defined('CURL_VERSION_HTTP3') || PHP_VERSION_ID < 80200 && function_exists('curl_version') && curl_version()['version'] >= 0x074200) { // libcurl >= 7.66.0
if (!defined('CURL_HTTP_VERSION_3')) {
define('CURL_HTTP_VERSION_3', 30);
}
if (\extension_loaded('curl')) {
// CURL_VERSION_HTTP3 is defined by PHP 8.2+ when libcurl >= 7.66.0
if (defined('CURL_VERSION_HTTP3') || \PHP_VERSION_ID < 80200 && curl_version()['version_number'] >= 0x074200) {
if (!defined('CURL_HTTP_VERSION_3')) {
define('CURL_HTTP_VERSION_3', 30);
}
if (!defined('CURL_HTTP_VERSION_3ONLY') && defined('CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256')) { // libcurl >= 7.80.0 (7.88 would be better but is slow to check)
define('CURL_HTTP_VERSION_3ONLY', 31);
// CURL_HTTP_VERSION_3ONLY requires libcurl >= 7.88.0 and is not gated by any PHP-defined constant before 8.4
if (!defined('CURL_HTTP_VERSION_3ONLY') && curl_version()['version_number'] >= 0x075800) {
define('CURL_HTTP_VERSION_3ONLY', 31);
}
}
}
@@ -45,32 +49,45 @@ if (!function_exists('fpow')) {
function fpow(float $num, float $exponent): float { return p\Php84::fpow($num, $exponent); }
}
if (\PHP_VERSION_ID < 80000) {
require __DIR__.'/bootstrap72.php';
}
if (extension_loaded('mbstring')) {
if (!function_exists('mb_ucfirst')) {
function mb_ucfirst(string $string, ?string $encoding = null): string { return p\Php84::mb_ucfirst($string, $encoding); }
function mb_ucfirst(?string $string, ?string $encoding = null): string { return p\Php84::mb_ucfirst((string) $string, $encoding); }
}
if (!function_exists('mb_lcfirst')) {
function mb_lcfirst(string $string, ?string $encoding = null): string { return p\Php84::mb_lcfirst($string, $encoding); }
function mb_lcfirst(?string $string, ?string $encoding = null): string { return p\Php84::mb_lcfirst((string) $string, $encoding); }
}
if (!function_exists('mb_trim')) {
function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Php84::mb_trim($string, $characters, $encoding); }
function mb_trim(?string $string, ?string $characters = null, ?string $encoding = null): string { return p\Php84::mb_trim((string) $string, $characters, $encoding); }
}
if (!function_exists('mb_ltrim')) {
function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Php84::mb_ltrim($string, $characters, $encoding); }
function mb_ltrim(?string $string, ?string $characters = null, ?string $encoding = null): string { return p\Php84::mb_ltrim((string) $string, $characters, $encoding); }
}
if (!function_exists('mb_rtrim')) {
function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Php84::mb_rtrim($string, $characters, $encoding); }
function mb_rtrim(?string $string, ?string $characters = null, ?string $encoding = null): string { return p\Php84::mb_rtrim((string) $string, $characters, $encoding); }
}
}
if (extension_loaded('bcmath')) {
if (!function_exists('bcceil')) {
function bcceil(string $num): string { return p\Php84::bcceil($num); }
}
if (!function_exists('bcdivmod')) {
function bcdivmod(string $num1, string $num2, ?int $scale = null): ?array { return p\Php84::bcdivmod($num1, $num2, $scale); }
}
if (!function_exists('bcfloor')) {
function bcfloor(string $num): string { return p\Php84::bcfloor($num); }
}
if (!function_exists('bcround')) {
function bcround(string $num, int $precision = 0, $mode = RoundingMode::HalfAwayFromZero): string { return p\Php84::bcround($num, $precision, $mode); }
}
}
if (\PHP_VERSION_ID >= 80200) {

View File

@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Polyfill\Php84 as p;
if (extension_loaded('mbstring')) {
if (!function_exists('mb_ucfirst')) {
/** @return string|false */
function mb_ucfirst(?string $string, ?string $encoding = null) { return p\Php84::mb_ucfirst((string) $string, $encoding); }
}
if (!function_exists('mb_lcfirst')) {
/** @return string|false */
function mb_lcfirst(?string $string, ?string $encoding = null) { return p\Php84::mb_lcfirst((string) $string, $encoding); }
}
if (!function_exists('mb_trim')) {
/** @return string|false */
function mb_trim(?string $string, ?string $characters = null, ?string $encoding = null) { return p\Php84::mb_trim((string) $string, $characters, $encoding); }
}
if (!function_exists('mb_ltrim')) {
/** @return string|false */
function mb_ltrim(?string $string, ?string $characters = null, ?string $encoding = null) { return p\Php84::mb_ltrim((string) $string, $characters, $encoding); }
}
if (!function_exists('mb_rtrim')) {
/** @return string|false */
function mb_rtrim(?string $string, ?string $characters = null, ?string $encoding = null) { return p\Php84::mb_rtrim((string) $string, $characters, $encoding); }
}
}

View File

@@ -13,6 +13,7 @@ namespace Symfony\Polyfill\Php85;
/**
* @author Pierre Ambroise <pierre27.ambroise@gmail.com>
* @author Alexander Schranz <alexander@sulu.io>
*
* @internal
*/
@@ -45,6 +46,94 @@ final class Php85
public static function array_last(array $array)
{
return $array ? current(array_slice($array, -1)) : null;
return $array ? current(\array_slice($array, -1)) : null;
}
private const RTL_SCRIPTS = [
'Adlm' => true, 'Arab' => true, 'Armi' => true, 'Hebr' => true,
'Mand' => true, 'Mani' => true, 'Mend' => true, 'Nkoo' => true,
'Orkh' => true, 'Phnx' => true, 'Rohg' => true, 'Samr' => true,
'Syrc' => true, 'Thaa' => true, 'Yezi' => true,
];
private const LANG_TO_SCRIPT = [
'ar' => 'Arab',
'ckb' => 'Arab',
'dv' => 'Thaa',
'fa' => 'Arab',
'he' => 'Hebr',
'ku' => 'Arab',
'nqo' => 'Nkoo',
'ps' => 'Arab',
'sd' => 'Arab',
'ug' => 'Arab',
'ur' => 'Arab',
'yi' => 'Hebr',
];
public static function locale_is_right_to_left(string $locale): bool
{
if ('' === $locale) {
return false;
}
$parts = preg_split('/[_-]/', $locale);
$language = strtolower($parts[0]);
foreach ($parts as $part) {
if (4 === \strlen($part) && ctype_alpha($part)) {
return isset(self::RTL_SCRIPTS[ucfirst(strtolower($part))]);
}
}
return isset(self::LANG_TO_SCRIPT[$language]) && isset(self::RTL_SCRIPTS[self::LANG_TO_SCRIPT[$language]]);
}
public static function grapheme_levenshtein(string $s1, string $s2, int $insertion_cost = 1, int $replacement_cost = 1, int $deletion_cost = 1)
{
if (!preg_match('//u', $s1) || !preg_match('//u', $s2)) {
return false;
}
if (0 > $insertion_cost || 0 > $replacement_cost || 0 > $deletion_cost) {
throw new \ValueError('grapheme_levenshtein(): Argument #3 ($insertion_cost), #4 ($replacement_cost), and #5 ($deletion_cost) must be greater than or equal to 0');
}
$regex = ((float) \PCRE_VERSION >= 10.44)
? '\X'
: '(?:\r\n|[\x{1F1E6}-\x{1F1FF}][\x{1F1E6}-\x{1F1FF}]?|(?:[ -~\x{200C}\x{200D}]|[ᆨ-ᇹ]+|[ᄀ-]*(?:[가개갸걔거게겨계고과괘괴교구궈궤귀규그긔기까깨꺄꺠꺼께껴꼐꼬꽈꽤꾀꾜꾸꿔꿰뀌뀨끄끠끼나내냐냬너네녀녜노놔놰뇌뇨누눠눼뉘뉴느늬니다대댜댸더데뎌뎨도돠돼되됴두둬뒈뒤듀드듸디따때땨떄떠떼뗘뗴또똬뙈뙤뚀뚜뚸뛔뛰뜌뜨띄띠라래랴럐러레려례로롸뢔뢰료루뤄뤠뤼류르릐리마매먀먜머메며몌모뫄뫠뫼묘무뭐뭬뮈뮤므믜미바배뱌뱨버베벼볘보봐봬뵈뵤부붜붸뷔뷰브븨비빠빼뺘뺴뻐뻬뼈뼤뽀뽜뽸뾔뾰뿌뿨쀄쀠쀼쁘쁴삐사새샤섀서세셔셰소솨쇄쇠쇼수숴쉐쉬슈스싀시싸쌔쌰썌써쎄쎠쎼쏘쏴쐐쐬쑈쑤쒀쒜쒸쓔쓰씌씨아애야얘어에여예오와왜외요우워웨위유으의이자재쟈쟤저제져졔조좌좨죄죠주줘줴쥐쥬즈즤지짜째쨔쨰쩌쩨쪄쪠쪼쫘쫴쬐쬬쭈쭤쮀쮜쮸쯔쯰찌차채챠챼처체쳐쳬초촤쵀최쵸추춰췌취츄츠츼치카캐캬컈커케켜켸코콰쾌쾨쿄쿠쿼퀘퀴큐크킈키타태탸턔터테텨톄토톼퇘퇴툐투퉈퉤튀튜트틔티파패퍄퍠퍼페펴폐포퐈퐤푀표푸풔풰퓌퓨프픠피하해햐햬허헤혀혜호화홰회효후훠훼휘휴흐희히]?[-ᆢ]+|[가-힣])[ᆨ-ᇹ]*|[ᄀ-]+|[^\p{Cc}\p{Cf}\p{Zl}\p{Zp}])[\p{Mn}\p{Mc}\p{Me}\x{09BE}\x{09D7}\x{0B3E}\x{0B57}\x{0BBE}\x{0BD7}\x{0CC2}\x{0CD5}\x{0CD6}\x{0D3E}\x{0D57}\x{0DCF}\x{0DDF}\x{200C}\x{1D165}\x{1D16E}-\x{1D172}\x{1F3FB}-\x{1F3FF}\x{FE0E}-\x{FE0F}\x{E0020}-\x{E007F}]*(?:\x{200D}(?:[\x{1F1E6}-\x{1F1FF}]|[ -~\x{200C}\x{200D}]|[ᆨ-ᇹ]+|[ᄀ-]*(?:[가-힣]?[-ᆢ]+|[가-힣])[ᆨ-ᇹ]*|[ᄀ-]+|[^\p{Cc}\p{Cf}\p{Zl}\p{Zp}])[\p{Mn}\p{Mc}\p{Me}\x{09BE}\x{09D7}\x{0B3E}\x{0B57}\x{0BBE}\x{0BD7}\x{0CC2}\x{0CD5}\x{0CD6}\x{0D3E}\x{0D57}\x{0DCF}\x{0DDF}\x{200C}\x{1D165}\x{1D16E}-\x{1D172}\x{1F3FB}-\x{1F3FF}\x{FE0E}-\x{FE0F}\x{E0020}-\x{E007F}]*)*|[\p{Cc}\p{Cf}\p{Zl}\p{Zp}])';
preg_match_all('/'.$regex.'/u', $s1, $s1);
preg_match_all('/'.$regex.'/u', $s2, $s2);
$s1 = $s1[0];
$s2 = $s2[0];
$l1 = \count($s1);
$l2 = \count($s2);
if (0 === $l1) {
return $l2 * $insertion_cost;
}
if (0 === $l2) {
return $l1 * $deletion_cost;
}
$dp = array_fill(0, $l1 + 1, array_fill(0, $l2 + 1, 0));
for ($i = 1; $i <= $l1; ++$i) {
$dp[$i][0] = $dp[$i - 1][0] + $deletion_cost;
}
for ($j = 1; $j <= $l2; ++$j) {
$dp[0][$j] = $dp[0][$j - 1] + $insertion_cost;
}
for ($i = 1; $i <= $l1; ++$i) {
for ($j = 1; $j <= $l2; ++$j) {
$cost = ($s1[$i - 1] === $s2[$j - 1]) ? 0 : $replacement_cost;
$dp[$i][$j] = min($dp[$i - 1][$j] + $deletion_cost, $dp[$i][$j - 1] + $insertion_cost, $dp[$i - 1][$j - 1] + $cost);
}
}
return $dp[$l1][$l2];
}
}

View File

@@ -6,6 +6,10 @@ This component provides features added to PHP 8.5 core:
- [`get_error_handler` and `get_exception_handler`](https://wiki.php.net/rfc/get-error-exception-handler)
- [`NoDiscard`](https://wiki.php.net/rfc/marking_return_value_as_important)
- [`array_first` and `array_last`](https://wiki.php.net/rfc/array_first_last)
- [`DelayedTargetValidation`](https://wiki.php.net/rfc/delayedtargetvalidation_attribute)
- [`Filter\FilterException class`](https://wiki.php.net/rfc/filter_throw_on_failure)
- [`Filter\FilterFailedException class`](https://wiki.php.net/rfc/filter_throw_on_failure)
- [`locale_is_right_to_left`](https://php.watch/versions/8.5/locale_is_right_to_left-Locale-isRightToleft)
More information can be found in the
[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).

View File

@@ -0,0 +1,18 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (\PHP_VERSION_ID < 80500) {
// @author Daniel Scherzer <daniel.e.scherzer@gmail.com>
#[Attribute(Attribute::TARGET_ALL)]
final class DelayedTargetValidation
{
}
}

View File

@@ -0,0 +1,18 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Filter;
if (\PHP_VERSION_ID < 80500) {
class FilterException extends \Exception
{
}
}

View File

@@ -0,0 +1,18 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Filter;
if (\PHP_VERSION_ID < 80500) {
class FilterFailedException extends FilterException
{
}
}

View File

@@ -30,3 +30,17 @@ if (!function_exists('array_first')) {
if (!function_exists('array_last')) {
function array_last(array $array) { return p\Php85::array_last($array); }
}
if (extension_loaded('intl') && !function_exists('locale_is_right_to_left')) {
function locale_is_right_to_left(string $locale): bool { return p\Php85::locale_is_right_to_left($locale); }
}
if (\PHP_VERSION_ID >= 80000) {
require __DIR__.'/bootstrap80.php';
return;
}
if (extension_loaded('intl') && !function_exists('grapheme_levenshtein')) {
function grapheme_levenshtein(string $string1, string $string2, int $insertion_cost = 1, int $replacement_cost = 1, int $deletion_cost = 1, string $locale = '') { return p\Php85::grapheme_levenshtein($string1, $string2, $insertion_cost, $replacement_cost, $deletion_cost); }
}

View File

@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Symfony\Polyfill\Php85 as p;
if (\PHP_VERSION_ID >= 80500) {
return;
}
if (extension_loaded('intl') && !function_exists('grapheme_levenshtein')) {
function grapheme_levenshtein(string $string1, string $string2, int $insertion_cost = 1, int $replacement_cost = 1, int $deletion_cost = 1, string $locale = ''): int|false { return p\Php85::grapheme_levenshtein($string1, $string2, $insertion_cost, $replacement_cost, $deletion_cost); }
}

View File

@@ -14,6 +14,7 @@ namespace Symfony\Contracts\Translation\Test;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorTrait;
@@ -26,7 +27,7 @@ use Symfony\Contracts\Translation\TranslatorTrait;
*
* As mentioned by chx http://drupal.org/node/1273968 we can cover all by testing number from 0 to 199
*
* The goal to cover all languages is to far fetched so this test case is smaller.
* The goal to cover all languages is too far fetched so this test case is smaller.
*
* @author Clemens Tolboom clemens@build2be.nl
*/
@@ -124,10 +125,12 @@ class TranslatorTest extends TestCase
public static function getTransTests()
{
return [
['Symfony is great!', 'Symfony is great!', []],
['Symfony is awesome!', 'Symfony is %what%!', ['%what%' => 'awesome']],
];
yield ['Symfony is great!', 'Symfony is great!', []];
yield ['Symfony is awesome!', 'Symfony is %what%!', ['%what%' => 'awesome']];
if (class_exists(TranslatableMessage::class)) {
yield ['He said "Symfony is awesome!".', 'He said "%what%".', ['%what%' => new TranslatableMessage('Symfony is %what%!', ['%what%' => 'awesome'])]];
}
}
public static function getTransChoiceTests()
@@ -346,7 +349,7 @@ class TranslatorTest extends TestCase
* This both depends on a complete list trying to add above as understanding
* the plural rules of the current failing languages.
*
* @return array with nplural together with langcodes
* @return array With nplural together with langcodes
*/
public static function failingLangcodes(): array
{

View File

@@ -41,6 +41,12 @@ trait TranslatorTrait
return '';
}
foreach ($parameters as $k => $v) {
if ($v instanceof TranslatableInterface) {
$parameters[$k] = $v->trans($this, $locale);
}
}
if (!isset($parameters['%count%']) || !is_numeric($parameters['%count%'])) {
return strtr($id, $parameters);
}
@@ -56,22 +62,22 @@ trait TranslatorTrait
}
$intervalRegexp = <<<'EOF'
/^(?P<interval>
({\s*
(\-?\d+(\.\d+)?[\s*,\s*\-?\d+(\.\d+)?]*)
\s*})
/^(?P<interval>
({\s*
(\-?\d+(\.\d+)?[\s*,\s*\-?\d+(\.\d+)?]*)
\s*})
|
|
(?P<left_delimiter>[\[\]])
\s*
(?P<left>-Inf|\-?\d+(\.\d+)?)
\s*,\s*
(?P<right>\+?Inf|\-?\d+(\.\d+)?)
\s*
(?P<right_delimiter>[\[\]])
)\s*(?P<message>.*?)$/xs
EOF;
(?P<left_delimiter>[\[\]])
\s*
(?P<left>-Inf|\-?\d+(\.\d+)?)
\s*,\s*
(?P<right>\+?Inf|\-?\d+(\.\d+)?)
\s*
(?P<right_delimiter>[\[\]])
)\s*(?P<message>.*?)$/xs
EOF;
$standardRules = [];
foreach ($parts as $part) {

View File

@@ -27,7 +27,7 @@
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-main": "3.6-dev"
"dev-main": "3.7-dev"
},
"thanks": {
"name": "symfony/contracts",

View File

@@ -1,6 +1,13 @@
CHANGELOG
=========
7.4
---
* Make the extractor alias optional
* Deprecate `TranslatableMessage::__toString`
* Add `Symfony\Component\Translation\StaticMessage`
7.3
---

View File

@@ -54,10 +54,10 @@ class TranslationLintCommand extends Command
new InputOption('locale', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the locales to lint.', $this->enabledLocales),
])
->setHelp(<<<'EOF'
The <info>%command.name%</> command lint translations.
The <info>%command.name%</> command lint translations.
<info>php %command.full_name%</>
EOF
<info>php %command.full_name%</>
EOF
);
}
@@ -70,7 +70,7 @@ EOF
{
$locales = $input->getOption('locale');
/** @var array<string, array<string, array<string, \Throwable>> $errors */
/** @var array<string, array<string, array<string, \Throwable>>> $errors */
$errors = [];
$domainsByLocales = [];

View File

@@ -90,21 +90,22 @@ final class TranslationPullCommand extends Command
new InputOption('as-tree', null, InputOption::VALUE_REQUIRED, 'Write messages as a tree-like structure. Needs --format=yaml. The given value defines the level where to switch to inline YAML'),
])
->setHelp(<<<'EOF'
The <info>%command.name%</> command pulls translations from the given provider. Only
new translations are pulled, existing ones are not overwritten.
The <info>%command.name%</> command pulls translations from the given provider. Only
new translations are pulled, existing ones are not overwritten.
You can overwrite existing translations (and remove the missing ones on local side) by using the <comment>--force</> flag:
You can overwrite existing translations (and remove the missing ones on local side) by using the <info>--force</> flag:
<info>php %command.full_name% --force provider</>
<info>php %command.full_name% --force provider</>
Full example:
Full example:
<info>php %command.full_name% provider --force --domains=messages --domains=validators --locales=en</>
<info>php %command.full_name% provider --force --domains=messages --domains=validators --locales=en</>
This command pulls all translations associated with the <comment>messages</> and <comment>validators</> domains for the <comment>en</> locale.
Local translations for the specified domains and locale are deleted if they're not present on the provider and overwritten if it's the case.
Local translations for others domains and locales are ignored.
EOF
This command pulls all translations associated with the <info>messages</> and <info>validators</> domains for the <info>en</> locale.
Local translations for the specified domains and locale are deleted if they're not present on the provider and overwritten if it's the case.
Local translations for others domains and locales are ignored.
EOF
)
;
}

View File

@@ -81,25 +81,26 @@ final class TranslationPushCommand extends Command
new InputOption('locales', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Specify the locales to push.', $this->enabledLocales),
])
->setHelp(<<<'EOF'
The <info>%command.name%</> command pushes translations to the given provider. Only new
translations are pushed, existing ones are not overwritten.
The <info>%command.name%</> command pushes translations to the given provider. Only new
translations are pushed, existing ones are not overwritten.
You can overwrite existing translations by using the <comment>--force</> flag:
You can overwrite existing translations by using the <info>--force</> flag:
<info>php %command.full_name% --force provider</>
<info>php %command.full_name% --force provider</>
You can delete provider translations which are not present locally by using the <comment>--delete-missing</> flag:
You can delete provider translations which are not present locally by using the <info>--delete-missing</> flag:
<info>php %command.full_name% --delete-missing provider</>
<info>php %command.full_name% --delete-missing provider</>
Full example:
Full example:
<info>php %command.full_name% provider --force --delete-missing --domains=messages --domains=validators --locales=en</>
<info>php %command.full_name% provider --force --delete-missing --domains=messages --domains=validators --locales=en</>
This command pushes all translations associated with the <comment>messages</> and <comment>validators</> domains for the <comment>en</> locale.
Provider translations for the specified domains and locale are deleted if they're not present locally and overwritten if it's the case.
Provider translations for others domains and locales are ignored.
EOF
This command pushes all translations associated with the <info>messages</> and <info>validators</> domains for the <info>en</> locale.
Provider translations for the specified domains and locale are deleted if they're not present locally and overwritten if it's the case.
Provider translations for others domains and locales are ignored.
EOF
)
;
}
@@ -168,7 +169,7 @@ EOF
$domains = [];
foreach ($translatorBag->getCatalogues() as $catalogue) {
$domains += $catalogue->getDomains();
$domains = array_merge($domains, $catalogue->getDomains());
}
return array_unique($domains);

View File

@@ -58,26 +58,26 @@ class XliffLintCommand extends Command
->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN')
->addOption('format', null, InputOption::VALUE_REQUIRED, \sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions())))
->setHelp(<<<EOF
The <info>%command.name%</info> command lints an XLIFF file and outputs to STDOUT
the first encountered syntax error.
The <info>%command.name%</info> command lints an XLIFF file and outputs to STDOUT
the first encountered syntax error.
You can validates XLIFF contents passed from STDIN:
You can validates XLIFF contents passed from STDIN:
<info>cat filename | php %command.full_name% -</info>
<info>cat filename | php %command.full_name% -</info>
You can also validate the syntax of a file:
You can also validate the syntax of a file:
<info>php %command.full_name% filename</info>
<info>php %command.full_name% filename</info>
Or of a whole directory:
Or of a whole directory:
<info>php %command.full_name% dirname</info>
<info>php %command.full_name% dirname</info>
The <info>--format</info> option specifies the format of the command output:
The <info>--format</info> option specifies the format of the command output:
<info>php %command.full_name% dirname --format=json</info>
<info>php %command.full_name% dirname --format=json</info>
EOF
EOF
)
;
}
@@ -178,7 +178,7 @@ EOF
} elseif (!$info['valid']) {
++$erroredFiles;
$io->text('<error> ERROR </error>'.($info['file'] ? \sprintf(' in %s', $info['file']) : ''));
$io->listing(array_map(function ($error) use ($info, $githubReporter) {
$io->listing(array_map(static function ($error) use ($info, $githubReporter) {
// general document errors have a '-1' line number
$line = -1 === $error['line'] ? null : $error['line'];
@@ -202,7 +202,7 @@ EOF
{
$errors = 0;
array_walk($filesInfo, function (&$v) use (&$errors) {
array_walk($filesInfo, static function (&$v) use (&$errors) {
$v['file'] = (string) $v['file'];
if (!$v['valid']) {
++$errors;
@@ -226,7 +226,7 @@ EOF
}
foreach ($this->getDirectoryIterator($fileOrDirectory) as $file) {
if (!\in_array($file->getExtension(), ['xlf', 'xliff'])) {
if (!\in_array($file->getExtension(), ['xlf', 'xliff'], true)) {
continue;
}
@@ -239,7 +239,7 @@ EOF
*/
private function getDirectoryIterator(string $directory): iterable
{
$default = fn ($directory) => new \RecursiveIteratorIterator(
$default = static fn ($directory) => new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS),
\RecursiveIteratorIterator::LEAVES_ONLY
);
@@ -253,7 +253,7 @@ EOF
private function isReadable(string $fileOrDirectory): bool
{
$default = fn ($fileOrDirectory) => is_readable($fileOrDirectory);
$default = static fn ($fileOrDirectory) => is_readable($fileOrDirectory);
if (null !== $this->isReadableProvider) {
return ($this->isReadableProvider)($fileOrDirectory, $default);

View File

@@ -12,6 +12,7 @@
namespace Symfony\Component\Translation;
use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface;
use Symfony\Contracts\Service\ResetInterface;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -20,7 +21,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*
* @final since Symfony 7.1
*/
class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface, WarmableInterface
class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface, WarmableInterface, ResetInterface
{
public const MESSAGE_DEFINED = 0;
public const MESSAGE_MISSING = 1;
@@ -33,6 +34,11 @@ class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInter
) {
}
public function reset(): void
{
$this->messages = [];
}
public function trans(?string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string
{
$trans = $this->translator->trans($id = (string) $id, $parameters, $domain, $locale);

View File

@@ -13,7 +13,6 @@ namespace Symfony\Component\Translation\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Reference;
/**
@@ -30,11 +29,7 @@ class TranslationExtractorPass implements CompilerPassInterface
$definition = $container->getDefinition('translation.extractor');
foreach ($container->findTaggedServiceIds('translation.extractor', true) as $id => $attributes) {
if (!isset($attributes[0]['alias'])) {
throw new RuntimeException(\sprintf('The alias for the tag "translation.extractor" of service "%s" must be set.', $id));
}
$definition->addMethodCall('addExtractor', [$attributes[0]['alias'], new Reference($id)]);
$definition->addMethodCall('addExtractor', [$attributes[0]['alias'] ?? $id, new Reference($id)]);
}
}
}

View File

@@ -93,10 +93,10 @@ class TranslatorPathsPass extends AbstractRecursivePass
$class = $this->definitions[$i]->getClass();
if (ServiceLocator::class === $class) {
if (!isset($this->controllers[$this->currentId])) {
if (!isset($this->controllers[$this->currentId ?? ''])) {
continue;
}
foreach ($this->controllers[$this->currentId] as $class => $_) {
foreach ($this->controllers[$this->currentId ?? ''] as $class => $_) {
$this->paths[$class] = true;
}
} else {

View File

@@ -49,7 +49,7 @@ abstract class FileDumper implements DumperInterface
$fullpath = $options['path'].'/'.$this->getRelativePath($domain, $messages->getLocale());
if (!file_exists($fullpath)) {
$directory = \dirname($fullpath);
if (!file_exists($directory) && !@mkdir($directory, 0777, true)) {
if (!file_exists($directory) && !@mkdir($directory, 0o777, true)) {
throw new RuntimeException(\sprintf('Unable to create directory "%s".', $directory));
}
}

View File

@@ -76,22 +76,22 @@ class PoFileDumper extends FileDumper
}
$intervalRegexp = <<<'EOF'
/^(?P<interval>
({\s*
(\-?\d+(\.\d+)?[\s*,\s*\-?\d+(\.\d+)?]*)
\s*})
/^(?P<interval>
({\s*
(\-?\d+(\.\d+)?[\s*,\s*\-?\d+(\.\d+)?]*)
\s*})
|
|
(?P<left_delimiter>[\[\]])
\s*
(?P<left>-Inf|\-?\d+(\.\d+)?)
\s*,\s*
(?P<right>\+?Inf|\-?\d+(\.\d+)?)
\s*
(?P<right_delimiter>[\[\]])
)\s*(?P<message>.*?)$/xs
EOF;
(?P<left_delimiter>[\[\]])
\s*
(?P<left>-Inf|\-?\d+(\.\d+)?)
\s*,\s*
(?P<right>\+?Inf|\-?\d+(\.\d+)?)
\s*
(?P<right_delimiter>[\[\]])
)\s*(?P<message>.*?)$/xs
EOF;
$standardRules = [];
foreach ($parts as $part) {

View File

@@ -51,7 +51,6 @@ final class PhpAstExtractor extends AbstractFileExtractor implements ExtractorIn
$nameResolver = new NodeVisitor\NameResolver();
$traverser->addVisitor($nameResolver);
/** @var AbstractVisitor&NodeVisitor $visitor */
foreach ($this->visitors as $visitor) {
$visitor->initialize($catalogue, $file, $this->prefix);
$traverser->addVisitor($visitor);

View File

@@ -78,7 +78,6 @@ final class ConstraintVisitor extends AbstractVisitor implements NodeVisitor
$messages = [];
$options = $arg->value;
/** @var Node\Expr\ArrayItem $item */
foreach ($options->items as $item) {
if (!$item->key instanceof Node\Scalar\String_) {
continue;

View File

@@ -37,7 +37,7 @@ class CsvFileLoader extends FileLoader
throw new NotFoundResourceException(\sprintf('Error opening file "%s".', $resource), 0, $e);
}
$file->setFlags(\SplFileObject::READ_CSV | \SplFileObject::SKIP_EMPTY);
$file->setFlags(\SplFileObject::READ_CSV | \SplFileObject::SKIP_EMPTY | \SplFileObject::DROP_NEW_LINE);
$file->setCsvControl($this->delimiter, $this->enclosure, $this->escape);
foreach ($file as $data) {

View File

@@ -55,7 +55,7 @@ final class PseudoLocalizationTranslator implements TranslatorInterface, Transla
* * parse_html:
* type: boolean
* default: false
* description: parse the translated string as HTML - looking for HTML tags has a performance impact but allows to preserve them from alterations - it also allows to compute the visible translated string length which is useful to correctly expand ot when it contains HTML
* description: parse the translated string as HTML - looking for HTML tags has a performance impact but allows to preserve them from alterations - it also allows to compute the visible translated string length which is useful to correctly expand or when it contains HTML
* warning: unclosed tags are unsupported, they will be fixed (closed) by the parser - eg, "foo <div>bar" => "foo <div>bar</div>"
*
* * localizable_html_attributes:
@@ -166,7 +166,6 @@ final class PseudoLocalizationTranslator implements TranslatorInterface, Transla
$parts[] = [false, false, '<'.$childNode->tagName];
/** @var \DOMAttr $attribute */
foreach ($childNode->attributes as $attribute) {
$parts[] = [false, false, ' '.$attribute->nodeName.'="'];
@@ -184,7 +183,7 @@ final class PseudoLocalizationTranslator implements TranslatorInterface, Transla
$parts[] = [false, false, '>'];
$parts = array_merge($parts, $this->parseNode($childNode, $parts));
$parts = array_merge($parts, $this->parseNode($childNode));
$parts[] = [false, false, '</'.$childNode->tagName.'>'];
}
@@ -383,3 +382,5 @@ final class PseudoLocalizationTranslator implements TranslatorInterface, Transla
return false === ($encoding = mb_detect_encoding($s, null, true)) ? \strlen($s) : mb_strlen($s, $encoding);
}
}
// @php-cs-fixer-ignore random_api_migration As logic is coupled with mt_srand() in tests

View File

@@ -26,10 +26,6 @@ echo $translator->trans('Hello World!'); // outputs « Bonjour ! »
Sponsor
-------
The Translation component for Symfony 7.1 is [backed][1] by:
* [Crowdin][2], a cloud-based localization management software helping teams to go global and stay agile.
Help Symfony by [sponsoring][3] its development!
Resources
@@ -41,6 +37,4 @@ Resources
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
[1]: https://symfony.com/backers
[2]: https://crowdin.com
[3]: https://symfony.com/sponsor

View File

@@ -15,27 +15,27 @@ if ('cli' !== \PHP_SAPI) {
$usageInstructions = <<<END
Usage instructions
-------------------------------------------------------------------------------
Usage instructions
-------------------------------------------------------------------------------
$ cd symfony-code-root-directory/
$ cd symfony-code-root-directory/
# show the translation status of all locales
$ php translation-status.php
# show the translation status of all locales
$ php translation-status.php
# only show the translation status of incomplete or erroneous locales
$ php translation-status.php --incomplete
# only show the translation status of incomplete or erroneous locales
$ php translation-status.php --incomplete
# show the translation status of all locales, all their missing translations and mismatches between trans-unit id and source
$ php translation-status.php -v
# show the translation status of all locales, all their missing translations and mismatches between trans-unit id and source
$ php translation-status.php -v
# show the status of a single locale
$ php translation-status.php fr
# show the status of a single locale
$ php translation-status.php fr
# show the status of a single locale, missing translations and mismatches between trans-unit id and source
$ php translation-status.php fr -v
# show the status of a single locale, missing translations and mismatches between trans-unit id and source
$ php translation-status.php fr -v
END;
END;
$config = [
// if TRUE, the full list of missing translations is displayed
@@ -87,8 +87,8 @@ foreach ($config['original_files'] as $originalFilePath) {
$translationFilePaths = findTranslationFiles($originalFilePath, $config['locale_to_analyze']);
$translationStatus = calculateTranslationStatus($originalFilePath, $translationFilePaths);
$totalMissingTranslations += array_sum(array_map(fn ($translation) => count($translation['missingKeys']), array_values($translationStatus)));
$totalTranslationMismatches += array_sum(array_map(fn ($translation) => count($translation['mismatches']), array_values($translationStatus)));
$totalMissingTranslations += array_sum(array_map(static fn ($translation) => count($translation['missingKeys']), array_values($translationStatus)));
$totalTranslationMismatches += array_sum(array_map(static fn ($translation) => count($translation['mismatches']), array_values($translationStatus)));
printTranslationStatus($originalFilePath, $translationStatus, $config['verbose_output'], $config['include_completed_languages']);
}

View File

@@ -23,6 +23,7 @@
"en_DG": "en_001",
"en_DK": "en_150",
"en_DM": "en_001",
"en_EE": "en_150",
"en_ER": "en_001",
"en_ES": "en_150",
"en_FI": "en_150",
@@ -32,6 +33,7 @@
"en_FR": "en_150",
"en_GB": "en_001",
"en_GD": "en_001",
"en_GE": "en_150",
"en_GG": "en_001",
"en_GH": "en_001",
"en_GI": "en_001",
@@ -56,6 +58,8 @@
"en_LC": "en_001",
"en_LR": "en_001",
"en_LS": "en_001",
"en_LT": "en_150",
"en_LV": "en_150",
"en_MG": "en_001",
"en_MO": "en_001",
"en_MS": "en_001",
@@ -98,6 +102,7 @@
"en_TT": "en_001",
"en_TV": "en_001",
"en_TZ": "en_001",
"en_UA": "en_150",
"en_UG": "en_001",
"en_VC": "en_001",
"en_VG": "en_001",
@@ -130,6 +135,7 @@
"es_VE": "es_419",
"ff_Adlm": "root",
"hi_Latn": "en_IN",
"kk_Arab": "root",
"ks_Deva": "root",
"nb": "no",
"nn": "no",

View File

@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation;
use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
final class StaticMessage implements TranslatableInterface
{
public function __construct(
private string $message,
) {
}
public function getMessage(): string
{
return $this->message;
}
public function trans(TranslatorInterface $translator, ?string $locale = null): string
{
return $this->getMessage();
}
}

View File

@@ -15,10 +15,13 @@ use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\Translation\Dumper\XliffFileDumper;
use Symfony\Component\Translation\Loader\ArrayLoader;
use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\Provider\ProviderInterface;
use Symfony\Component\Translation\TranslatorBag;
use Symfony\Component\Translation\TranslatorBagInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
@@ -59,12 +62,12 @@ abstract class ProviderTestCase extends TestCase
protected function getLoader(): LoaderInterface
{
return $this->loader ??= $this->createMock(LoaderInterface::class);
return $this->loader ??= new ArrayLoader();
}
protected function getLogger(): LoggerInterface
{
return $this->logger ??= $this->createMock(LoggerInterface::class);
return $this->logger ??= new NullLogger();
}
protected function getDefaultLocale(): string
@@ -74,11 +77,11 @@ abstract class ProviderTestCase extends TestCase
protected function getXliffFileDumper(): XliffFileDumper
{
return $this->xliffFileDumper ??= $this->createMock(XliffFileDumper::class);
return $this->xliffFileDumper ??= new XliffFileDumper();
}
protected function getTranslatorBag(): TranslatorBagInterface
{
return $this->translatorBag ??= $this->createMock(TranslatorBagInterface::class);
return $this->translatorBag ??= new TranslatorBag();
}
}

View File

@@ -26,8 +26,13 @@ class TranslatableMessage implements TranslatableInterface
) {
}
/**
* @deprecated since Symfony 7.4
*/
public function __toString(): string
{
trigger_deprecation('symfony/translation', '7.4', 'Method "%s()" is deprecated.', __METHOD__);
return $this->getMessage();
}

View File

@@ -307,17 +307,16 @@ class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleA
$fallbackContent = $this->getFallbackContent($this->catalogues[$locale]);
$content = \sprintf(<<<EOF
<?php
<?php
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\MessageCatalogue;
\$catalogue = new MessageCatalogue('%s', %s);
\$catalogue = new MessageCatalogue('%s', %s);
%s
return \$catalogue;
%s
return \$catalogue;
EOF
,
EOF,
$locale,
var_export($this->getAllMessages($this->catalogues[$locale]), true),
$fallbackContent
@@ -338,11 +337,10 @@ EOF
$currentSuffix = ucfirst(preg_replace($replacementPattern, '_', $current));
$fallbackContent .= \sprintf(<<<'EOF'
$catalogue%s = new MessageCatalogue('%s', %s);
$catalogue%s->addFallbackCatalogue($catalogue%s);
$catalogue%s = new MessageCatalogue('%s', %s);
$catalogue%s->addFallbackCatalogue($catalogue%s);
EOF
,
EOF,
$fallbackSuffix,
$fallback,
var_export($this->getAllMessages($fallbackCatalogue), true),

View File

@@ -31,7 +31,6 @@ class XliffUtils
*/
public static function getVersionNumber(\DOMDocument $dom): string
{
/** @var \DOMNode $xliff */
foreach ($dom->getElementsByTagName('xliff') as $xliff) {
$version = $xliff->attributes->getNamedItem('version');
if ($version) {
@@ -83,31 +82,6 @@ class XliffUtils
return [];
}
private static function shouldEnableEntityLoader(): bool
{
static $dom, $schema;
if (null === $dom) {
$dom = new \DOMDocument();
$dom->loadXML('<?xml version="1.0"?><test/>');
$tmpfile = tempnam(sys_get_temp_dir(), 'symfony');
register_shutdown_function(static function () use ($tmpfile) {
@unlink($tmpfile);
});
$schema = '<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:include schemaLocation="file:///'.str_replace('\\', '/', $tmpfile).'" />
</xsd:schema>';
file_put_contents($tmpfile, '<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:element name="test" type="testType" />
<xsd:complexType name="testType"/>
</xsd:schema>');
}
return !@$dom->schemaValidateSource($schema);
}
public static function getErrorsAsString(array $xmlErrors): string
{
$errorsAsString = '';
@@ -126,6 +100,44 @@ class XliffUtils
return $errorsAsString;
}
private static function shouldEnableEntityLoader(): bool
{
static $dom, $schema;
if (null === $dom) {
$dom = new \DOMDocument();
$dom->loadXML('<?xml version="1.0"?><test/>');
$tmpfile = tempnam(sys_get_temp_dir(), 'symfony');
register_shutdown_function(static function () use ($tmpfile) {
@unlink($tmpfile);
});
$schema = '<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:include schemaLocation="'.self::getFileUrl($tmpfile).'" />
</xsd:schema>';
file_put_contents($tmpfile, '<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:element name="test" type="testType" />
<xsd:complexType name="testType"/>
</xsd:schema>');
}
return !@$dom->schemaValidateSource($schema);
}
private static function getFileUrl(string $path): string
{
if ('\\' === \DIRECTORY_SEPARATOR) {
$parts = explode('/', str_replace('\\', '/', $path));
$drive = array_shift($parts).'/';
} else {
$parts = explode('/', $path);
$drive = '';
}
return 'file:///'.$drive.implode('/', array_map('rawurlencode', $parts));
}
private static function getSchema(string $xliffVersion): string
{
if ('1.2' === $xliffVersion) {
@@ -146,22 +158,21 @@ class XliffUtils
*/
private static function fixXmlLocation(string $schemaSource, string $xmlUri): string
{
$newPath = str_replace('\\', '/', __DIR__).'/../Resources/schemas/xml.xsd';
$parts = explode('/', $newPath);
$locationstart = 'file:///';
if (0 === stripos($newPath, 'phar://')) {
$tmpfile = tempnam(sys_get_temp_dir(), 'symfony');
if ($tmpfile) {
copy($newPath, $tmpfile);
$parts = explode('/', str_replace('\\', '/', $tmpfile));
} else {
array_shift($parts);
$locationstart = 'phar:///';
}
}
$path = __DIR__.'/../Resources/schemas/xml.xsd';
$drive = '\\' === \DIRECTORY_SEPARATOR ? array_shift($parts).'/' : '';
$newPath = $locationstart.$drive.implode('/', array_map('rawurlencode', $parts));
if (0 === stripos($path, 'phar://')) {
if ($tmpfile = tempnam(sys_get_temp_dir(), 'symfony')) {
copy($path, $tmpfile);
$newPath = self::getFileUrl($tmpfile);
} else {
$parts = explode('/', '\\' === \DIRECTORY_SEPARATOR ? str_replace('\\', '/', $path) : $path);
array_shift($parts);
$drive = '\\' === \DIRECTORY_SEPARATOR ? array_shift($parts).'/' : '';
$newPath = 'phar:///'.$drive.implode('/', array_map('rawurlencode', $parts));
}
} else {
$newPath = self::getFileUrl($path);
}
return str_replace($xmlUri, $newPath, $schemaSource);
}

View File

@@ -61,7 +61,7 @@ class TranslationWriter implements TranslationWriterInterface
// get the right dumper
$dumper = $this->dumpers[$format];
if (isset($options['path']) && !is_dir($options['path']) && !@mkdir($options['path'], 0777, true) && !is_dir($options['path'])) {
if (isset($options['path']) && !is_dir($options['path']) && !@mkdir($options['path'], 0o777, true) && !is_dir($options['path'])) {
throw new RuntimeException(\sprintf('Translation Writer was not able to create directory "%s".', $options['path']));
}

View File

@@ -18,22 +18,22 @@
"require": {
"php": ">=8.2",
"symfony/polyfill-mbstring": "~1.0",
"symfony/translation-contracts": "^2.5|^3.0",
"symfony/translation-contracts": "^2.5.3|^3.3",
"symfony/deprecation-contracts": "^2.5|^3"
},
"require-dev": {
"nikic/php-parser": "^5.0",
"symfony/config": "^6.4|^7.0",
"symfony/console": "^6.4|^7.0",
"symfony/dependency-injection": "^6.4|^7.0",
"symfony/config": "^6.4|^7.0|^8.0",
"symfony/console": "^6.4|^7.0|^8.0",
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
"symfony/http-client-contracts": "^2.5|^3.0",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/intl": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0|^8.0",
"symfony/intl": "^6.4|^7.0|^8.0",
"symfony/polyfill-intl-icu": "^1.21",
"symfony/routing": "^6.4|^7.0",
"symfony/routing": "^6.4|^7.0|^8.0",
"symfony/service-contracts": "^2.5|^3",
"symfony/yaml": "^6.4|^7.0",
"symfony/finder": "^6.4|^7.0",
"symfony/yaml": "^6.4|^7.0|^8.0",
"symfony/finder": "^6.4|^7.0|^8.0",
"psr/log": "^1|^2|^3"
},
"conflict": {