初始化项目
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
vendor/
|
||||
composer.lock
|
||||
phpunit.xml
|
||||
@@ -0,0 +1,5 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
The changelog is maintained for all Symfony contracts at the following URL:
|
||||
https://github.com/symfony/contracts/blob/main/CHANGELOG.md
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2020-present Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,26 @@
|
||||
Symfony Deprecation Contracts
|
||||
=============================
|
||||
|
||||
A generic function and convention to trigger deprecation notices.
|
||||
|
||||
This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices.
|
||||
|
||||
By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component,
|
||||
the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments.
|
||||
|
||||
The function requires at least 3 arguments:
|
||||
- the name of the Composer package that is triggering the deprecation
|
||||
- the version of the package that introduced the deprecation
|
||||
- the message of the deprecation
|
||||
- more arguments can be provided: they will be inserted in the message using `printf()` formatting
|
||||
|
||||
Example:
|
||||
```php
|
||||
trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin');
|
||||
```
|
||||
|
||||
This will generate the following message:
|
||||
`Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.`
|
||||
|
||||
While not necessarily recommended, the deprecation notices can be completely ignored by declaring an empty
|
||||
`function trigger_deprecation() {}` in your application.
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
"type": "library",
|
||||
"description": "A generic function and convention to trigger deprecation notices",
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"function.php"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "2.5-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/contracts",
|
||||
"url": "https://github.com/symfony/contracts"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?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 (!function_exists('trigger_deprecation')) {
|
||||
/**
|
||||
* Triggers a silenced deprecation notice.
|
||||
*
|
||||
* @param string $package The name of the Composer package that is triggering the deprecation
|
||||
* @param string $version The version of the package that introduced the deprecation
|
||||
* @param string $message The message of the deprecation
|
||||
* @param mixed ...$args Values to insert in the message using printf() formatting
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
function trigger_deprecation(string $package, string $version, string $message, ...$args): void
|
||||
{
|
||||
@trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2015-present Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,996 @@
|
||||
<?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\Polyfill\Mbstring;
|
||||
|
||||
/**
|
||||
* Partial mbstring implementation in PHP, iconv based, UTF-8 centric.
|
||||
*
|
||||
* Implemented:
|
||||
* - mb_chr - Returns a specific character from its Unicode code point
|
||||
* - mb_convert_encoding - Convert character encoding
|
||||
* - mb_convert_variables - Convert character code in variable(s)
|
||||
* - mb_decode_mimeheader - Decode string in MIME header field
|
||||
* - mb_encode_mimeheader - Encode string for MIME header XXX NATIVE IMPLEMENTATION IS REALLY BUGGED
|
||||
* - mb_decode_numericentity - Decode HTML numeric string reference to character
|
||||
* - mb_encode_numericentity - Encode character to HTML numeric string reference
|
||||
* - mb_convert_case - Perform case folding on a string
|
||||
* - mb_detect_encoding - Detect character encoding
|
||||
* - mb_get_info - Get internal settings of mbstring
|
||||
* - mb_http_input - Detect HTTP input character encoding
|
||||
* - mb_http_output - Set/Get HTTP output character encoding
|
||||
* - mb_internal_encoding - Set/Get internal character encoding
|
||||
* - mb_list_encodings - Returns an array of all supported encodings
|
||||
* - mb_ord - Returns the Unicode code point of a character
|
||||
* - mb_output_handler - Callback function converts character encoding in output buffer
|
||||
* - mb_scrub - Replaces ill-formed byte sequences with substitute characters
|
||||
* - mb_strlen - Get string length
|
||||
* - mb_strpos - Find position of first occurrence of string in a string
|
||||
* - mb_strrpos - Find position of last occurrence of a string in a string
|
||||
* - mb_str_split - Convert a string to an array
|
||||
* - mb_strtolower - Make a string lowercase
|
||||
* - mb_strtoupper - Make a string uppercase
|
||||
* - mb_substitute_character - Set/Get substitution character
|
||||
* - mb_substr - Get part of string
|
||||
* - mb_stripos - Finds position of first occurrence of a string within another, case insensitive
|
||||
* - mb_stristr - Finds first occurrence of a string within another, case insensitive
|
||||
* - mb_strrchr - Finds the last occurrence of a character in a string within another
|
||||
* - mb_strrichr - Finds the last occurrence of a character in a string within another, case insensitive
|
||||
* - mb_strripos - Finds position of last occurrence of a string within another, case insensitive
|
||||
* - mb_strstr - Finds first occurrence of a string within another
|
||||
* - mb_strwidth - Return width of string
|
||||
* - mb_substr_count - Count the number of substring occurrences
|
||||
* - mb_ucfirst - Make a string's first character uppercase
|
||||
* - mb_lcfirst - Make a string's first character lowercase
|
||||
*
|
||||
* Not implemented:
|
||||
* - mb_convert_kana - Convert "kana" one from another ("zen-kaku", "han-kaku" and more)
|
||||
* - mb_ereg_* - Regular expression with multibyte support
|
||||
* - mb_parse_str - Parse GET/POST/COOKIE data and set global variable
|
||||
* - mb_preferred_mime_name - Get MIME charset string
|
||||
* - mb_regex_encoding - Returns current encoding for multibyte regex as string
|
||||
* - mb_regex_set_options - Set/Get the default options for mbregex functions
|
||||
* - mb_send_mail - Send encoded mail
|
||||
* - mb_split - Split multibyte string using regular expression
|
||||
* - mb_strcut - Get part of string
|
||||
* - mb_strimwidth - Get truncated string with specified width
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class Mbstring
|
||||
{
|
||||
public const MB_CASE_FOLD = \PHP_INT_MAX;
|
||||
|
||||
private const SIMPLE_CASE_FOLD = [
|
||||
['µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"],
|
||||
['μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'],
|
||||
];
|
||||
|
||||
private static $encodingList = ['ASCII', 'UTF-8'];
|
||||
private static $language = 'neutral';
|
||||
private static $internalEncoding = 'UTF-8';
|
||||
|
||||
public static function mb_convert_encoding($s, $toEncoding, $fromEncoding = null)
|
||||
{
|
||||
if (\is_array($s)) {
|
||||
if (PHP_VERSION_ID < 70200) {
|
||||
trigger_error('mb_convert_encoding() expects parameter 1 to be string, array given', \E_USER_WARNING);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$r = [];
|
||||
foreach ($s as $str) {
|
||||
$r[] = self::mb_convert_encoding($str, $toEncoding, $fromEncoding);
|
||||
}
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
if (\is_array($fromEncoding) || (null !== $fromEncoding && false !== strpos($fromEncoding, ','))) {
|
||||
$fromEncoding = self::mb_detect_encoding($s, $fromEncoding);
|
||||
} else {
|
||||
$fromEncoding = self::getEncoding($fromEncoding);
|
||||
}
|
||||
|
||||
$toEncoding = self::getEncoding($toEncoding);
|
||||
|
||||
if ('BASE64' === $fromEncoding) {
|
||||
$s = base64_decode($s);
|
||||
$fromEncoding = $toEncoding;
|
||||
}
|
||||
|
||||
if ('BASE64' === $toEncoding) {
|
||||
return base64_encode($s);
|
||||
}
|
||||
|
||||
if ('HTML-ENTITIES' === $toEncoding || 'HTML' === $toEncoding) {
|
||||
if ('HTML-ENTITIES' === $fromEncoding || 'HTML' === $fromEncoding) {
|
||||
$fromEncoding = 'Windows-1252';
|
||||
}
|
||||
if ('UTF-8' !== $fromEncoding) {
|
||||
$s = iconv($fromEncoding, 'UTF-8//IGNORE', $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');
|
||||
$fromEncoding = 'UTF-8';
|
||||
}
|
||||
|
||||
return iconv($fromEncoding, $toEncoding.'//IGNORE', $s);
|
||||
}
|
||||
|
||||
public static function mb_convert_variables($toEncoding, $fromEncoding, &...$vars)
|
||||
{
|
||||
$ok = true;
|
||||
array_walk_recursive($vars, function (&$v) use (&$ok, $toEncoding, $fromEncoding) {
|
||||
if (false === $v = self::mb_convert_encoding($v, $toEncoding, $fromEncoding)) {
|
||||
$ok = false;
|
||||
}
|
||||
});
|
||||
|
||||
return $ok ? $fromEncoding : false;
|
||||
}
|
||||
|
||||
public static function mb_decode_mimeheader($s)
|
||||
{
|
||||
return iconv_mime_decode($s, 2, self::$internalEncoding);
|
||||
}
|
||||
|
||||
public static function mb_encode_mimeheader($s, $charset = null, $transferEncoding = null, $linefeed = null, $indent = null)
|
||||
{
|
||||
trigger_error('mb_encode_mimeheader() is bugged. Please use iconv_mime_encode() instead', \E_USER_WARNING);
|
||||
}
|
||||
|
||||
public static function mb_decode_numericentity($s, $convmap, $encoding = null)
|
||||
{
|
||||
if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) {
|
||||
trigger_error('mb_decode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (null !== $encoding && !\is_scalar($encoding)) {
|
||||
trigger_error('mb_decode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING);
|
||||
|
||||
return ''; // Instead of null (cf. mb_encode_numericentity).
|
||||
}
|
||||
|
||||
$s = (string) $s;
|
||||
if ('' === $s) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$encoding = self::getEncoding($encoding);
|
||||
|
||||
if ('UTF-8' === $encoding) {
|
||||
$encoding = null;
|
||||
if (!preg_match('//u', $s)) {
|
||||
$s = @iconv('UTF-8', 'UTF-8//IGNORE', $s);
|
||||
}
|
||||
} else {
|
||||
$s = iconv($encoding, 'UTF-8//IGNORE', $s);
|
||||
}
|
||||
|
||||
$cnt = floor(\count($convmap) / 4) * 4;
|
||||
|
||||
for ($i = 0; $i < $cnt; $i += 4) {
|
||||
// collector_decode_htmlnumericentity ignores $convmap[$i + 3]
|
||||
$convmap[$i] += $convmap[$i + 2];
|
||||
$convmap[$i + 1] += $convmap[$i + 2];
|
||||
}
|
||||
|
||||
$s = preg_replace_callback('/&#(?:0*([0-9]+)|x0*([0-9a-fA-F]+))(?!&);?/', 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]) {
|
||||
return self::mb_chr($c - $convmap[$i + 2]);
|
||||
}
|
||||
}
|
||||
|
||||
return $m[0];
|
||||
}, $s);
|
||||
|
||||
if (null === $encoding) {
|
||||
return $s;
|
||||
}
|
||||
|
||||
return iconv('UTF-8', $encoding.'//IGNORE', $s);
|
||||
}
|
||||
|
||||
public static function mb_encode_numericentity($s, $convmap, $encoding = null, $is_hex = false)
|
||||
{
|
||||
if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) {
|
||||
trigger_error('mb_encode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (null !== $encoding && !\is_scalar($encoding)) {
|
||||
trigger_error('mb_encode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING);
|
||||
|
||||
return null; // Instead of '' (cf. mb_decode_numericentity).
|
||||
}
|
||||
|
||||
if (null !== $is_hex && !\is_scalar($is_hex)) {
|
||||
trigger_error('mb_encode_numericentity() expects parameter 4 to be boolean, '.\gettype($s).' given', \E_USER_WARNING);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$s = (string) $s;
|
||||
if ('' === $s) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$encoding = self::getEncoding($encoding);
|
||||
|
||||
if ('UTF-8' === $encoding) {
|
||||
$encoding = null;
|
||||
if (!preg_match('//u', $s)) {
|
||||
$s = @iconv('UTF-8', 'UTF-8//IGNORE', $s);
|
||||
}
|
||||
} else {
|
||||
$s = iconv($encoding, 'UTF-8//IGNORE', $s);
|
||||
}
|
||||
|
||||
static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4];
|
||||
|
||||
$cnt = floor(\count($convmap) / 4) * 4;
|
||||
$i = 0;
|
||||
$len = \strlen($s);
|
||||
$result = '';
|
||||
|
||||
while ($i < $len) {
|
||||
$ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"];
|
||||
$uchr = substr($s, $i, $ulen);
|
||||
$i += $ulen;
|
||||
$c = self::mb_ord($uchr);
|
||||
|
||||
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.';';
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
$result .= $uchr;
|
||||
}
|
||||
|
||||
if (null === $encoding) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return iconv('UTF-8', $encoding.'//IGNORE', $result);
|
||||
}
|
||||
|
||||
public static function mb_convert_case($s, $mode, $encoding = null)
|
||||
{
|
||||
$s = (string) $s;
|
||||
if ('' === $s) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$encoding = self::getEncoding($encoding);
|
||||
|
||||
if ('UTF-8' === $encoding) {
|
||||
$encoding = null;
|
||||
if (!preg_match('//u', $s)) {
|
||||
$s = @iconv('UTF-8', 'UTF-8//IGNORE', $s);
|
||||
}
|
||||
} else {
|
||||
$s = iconv($encoding, 'UTF-8//IGNORE', $s);
|
||||
}
|
||||
|
||||
if (\MB_CASE_TITLE == $mode) {
|
||||
static $titleRegexp = null;
|
||||
if (null === $titleRegexp) {
|
||||
$titleRegexp = self::getData('titleCaseRegexp');
|
||||
}
|
||||
$s = preg_replace_callback($titleRegexp, [__CLASS__, 'title_case'], $s);
|
||||
} else {
|
||||
if (\MB_CASE_UPPER == $mode) {
|
||||
static $upper = null;
|
||||
if (null === $upper) {
|
||||
$upper = self::getData('upperCase');
|
||||
}
|
||||
$map = $upper;
|
||||
} else {
|
||||
if (self::MB_CASE_FOLD === $mode) {
|
||||
static $caseFolding = null;
|
||||
if (null === $caseFolding) {
|
||||
$caseFolding = self::getData('caseFolding');
|
||||
}
|
||||
$s = strtr($s, $caseFolding);
|
||||
}
|
||||
|
||||
static $lower = null;
|
||||
if (null === $lower) {
|
||||
$lower = self::getData('lowerCase');
|
||||
}
|
||||
$map = $lower;
|
||||
}
|
||||
|
||||
static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4];
|
||||
|
||||
$i = 0;
|
||||
$len = \strlen($s);
|
||||
|
||||
while ($i < $len) {
|
||||
$ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"];
|
||||
$uchr = substr($s, $i, $ulen);
|
||||
$i += $ulen;
|
||||
|
||||
if (isset($map[$uchr])) {
|
||||
$uchr = $map[$uchr];
|
||||
$nlen = \strlen($uchr);
|
||||
|
||||
if ($nlen == $ulen) {
|
||||
$nlen = $i;
|
||||
do {
|
||||
$s[--$nlen] = $uchr[--$ulen];
|
||||
} while ($ulen);
|
||||
} else {
|
||||
$s = substr_replace($s, $uchr, $i - $ulen, $ulen);
|
||||
$len += $nlen - $ulen;
|
||||
$i += $nlen - $ulen;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $encoding) {
|
||||
return $s;
|
||||
}
|
||||
|
||||
return iconv('UTF-8', $encoding.'//IGNORE', $s);
|
||||
}
|
||||
|
||||
public static function mb_internal_encoding($encoding = null)
|
||||
{
|
||||
if (null === $encoding) {
|
||||
return self::$internalEncoding;
|
||||
}
|
||||
|
||||
$normalizedEncoding = self::getEncoding($encoding);
|
||||
|
||||
if ('UTF-8' === $normalizedEncoding || false !== @iconv($normalizedEncoding, $normalizedEncoding, ' ')) {
|
||||
self::$internalEncoding = $normalizedEncoding;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (80000 > \PHP_VERSION_ID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new \ValueError(sprintf('Argument #1 ($encoding) must be a valid encoding, "%s" given', $encoding));
|
||||
}
|
||||
|
||||
public static function mb_language($lang = null)
|
||||
{
|
||||
if (null === $lang) {
|
||||
return self::$language;
|
||||
}
|
||||
|
||||
switch ($normalizedLang = strtolower($lang)) {
|
||||
case 'uni':
|
||||
case 'neutral':
|
||||
self::$language = $normalizedLang;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (80000 > \PHP_VERSION_ID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new \ValueError(sprintf('Argument #1 ($language) must be a valid language, "%s" given', $lang));
|
||||
}
|
||||
|
||||
public static function mb_list_encodings()
|
||||
{
|
||||
return ['UTF-8'];
|
||||
}
|
||||
|
||||
public static function mb_encoding_aliases($encoding)
|
||||
{
|
||||
switch (strtoupper($encoding)) {
|
||||
case 'UTF8':
|
||||
case 'UTF-8':
|
||||
return ['utf8'];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function mb_check_encoding($var = null, $encoding = null)
|
||||
{
|
||||
if (\PHP_VERSION_ID < 70200 && \is_array($var)) {
|
||||
trigger_error('mb_check_encoding() expects parameter 1 to be string, array given', \E_USER_WARNING);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (null === $encoding) {
|
||||
if (null === $var) {
|
||||
return false;
|
||||
}
|
||||
$encoding = self::$internalEncoding;
|
||||
}
|
||||
|
||||
if (!\is_array($var)) {
|
||||
return self::mb_detect_encoding($var, [$encoding]) || false !== @iconv($encoding, $encoding, $var);
|
||||
}
|
||||
|
||||
foreach ($var as $key => $value) {
|
||||
if (!self::mb_check_encoding($key, $encoding)) {
|
||||
return false;
|
||||
}
|
||||
if (!self::mb_check_encoding($value, $encoding)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function mb_detect_encoding($str, $encodingList = null, $strict = false)
|
||||
{
|
||||
if (null === $encodingList) {
|
||||
$encodingList = self::$encodingList;
|
||||
} else {
|
||||
if (!\is_array($encodingList)) {
|
||||
$encodingList = array_map('trim', explode(',', $encodingList));
|
||||
}
|
||||
$encodingList = array_map('strtoupper', $encodingList);
|
||||
}
|
||||
|
||||
foreach ($encodingList as $enc) {
|
||||
switch ($enc) {
|
||||
case 'ASCII':
|
||||
if (!preg_match('/[\x80-\xFF]/', $str)) {
|
||||
return $enc;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'UTF8':
|
||||
case 'UTF-8':
|
||||
if (preg_match('//u', $str)) {
|
||||
return 'UTF-8';
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (0 === strncmp($enc, 'ISO-8859-', 9)) {
|
||||
return $enc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function mb_detect_order($encodingList = null)
|
||||
{
|
||||
if (null === $encodingList) {
|
||||
return self::$encodingList;
|
||||
}
|
||||
|
||||
if (!\is_array($encodingList)) {
|
||||
$encodingList = array_map('trim', explode(',', $encodingList));
|
||||
}
|
||||
$encodingList = array_map('strtoupper', $encodingList);
|
||||
|
||||
foreach ($encodingList as $enc) {
|
||||
switch ($enc) {
|
||||
default:
|
||||
if (strncmp($enc, 'ISO-8859-', 9)) {
|
||||
return false;
|
||||
}
|
||||
// no break
|
||||
case 'ASCII':
|
||||
case 'UTF8':
|
||||
case 'UTF-8':
|
||||
}
|
||||
}
|
||||
|
||||
self::$encodingList = $encodingList;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function mb_strlen($s, $encoding = null)
|
||||
{
|
||||
$encoding = self::getEncoding($encoding);
|
||||
if ('CP850' === $encoding || 'ASCII' === $encoding) {
|
||||
return \strlen($s);
|
||||
}
|
||||
|
||||
return @iconv_strlen($s, $encoding);
|
||||
}
|
||||
|
||||
public static function mb_strpos($haystack, $needle, $offset = 0, $encoding = null)
|
||||
{
|
||||
$encoding = self::getEncoding($encoding);
|
||||
if ('CP850' === $encoding || 'ASCII' === $encoding) {
|
||||
return strpos($haystack, $needle, $offset);
|
||||
}
|
||||
|
||||
$needle = (string) $needle;
|
||||
if ('' === $needle) {
|
||||
if (80000 > \PHP_VERSION_ID) {
|
||||
trigger_error(__METHOD__.': Empty delimiter', \E_USER_WARNING);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
return iconv_strpos($haystack, $needle, $offset, $encoding);
|
||||
}
|
||||
|
||||
public static function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null)
|
||||
{
|
||||
$encoding = self::getEncoding($encoding);
|
||||
if ('CP850' === $encoding || 'ASCII' === $encoding) {
|
||||
return strrpos($haystack, $needle, $offset);
|
||||
}
|
||||
|
||||
if ($offset != (int) $offset) {
|
||||
$offset = 0;
|
||||
} elseif ($offset = (int) $offset) {
|
||||
if ($offset < 0) {
|
||||
if (0 > $offset += self::mb_strlen($needle)) {
|
||||
$haystack = self::mb_substr($haystack, 0, $offset, $encoding);
|
||||
}
|
||||
$offset = 0;
|
||||
} else {
|
||||
$haystack = self::mb_substr($haystack, $offset, 2147483647, $encoding);
|
||||
}
|
||||
}
|
||||
|
||||
$pos = '' !== $needle || 80000 > \PHP_VERSION_ID
|
||||
? iconv_strrpos($haystack, $needle, $encoding)
|
||||
: self::mb_strlen($haystack, $encoding);
|
||||
|
||||
return false !== $pos ? $offset + $pos : false;
|
||||
}
|
||||
|
||||
public static function mb_str_split($string, $split_length = 1, $encoding = null)
|
||||
{
|
||||
if (null !== $string && !\is_scalar($string) && !(\is_object($string) && method_exists($string, '__toString'))) {
|
||||
trigger_error('mb_str_split() expects parameter 1 to be string, '.\gettype($string).' given', \E_USER_WARNING);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (1 > $split_length = (int) $split_length) {
|
||||
if (80000 > \PHP_VERSION_ID) {
|
||||
trigger_error('The length of each segment must be greater than zero', \E_USER_WARNING);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new \ValueError('Argument #2 ($length) must be greater than 0');
|
||||
}
|
||||
|
||||
if (null === $encoding) {
|
||||
$encoding = mb_internal_encoding();
|
||||
}
|
||||
|
||||
if ('UTF-8' === $encoding = self::getEncoding($encoding)) {
|
||||
$rx = '/(';
|
||||
while (65535 < $split_length) {
|
||||
$rx .= '.{65535}';
|
||||
$split_length -= 65535;
|
||||
}
|
||||
$rx .= '.{'.$split_length.'})/us';
|
||||
|
||||
return preg_split($rx, $string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY);
|
||||
}
|
||||
|
||||
$result = [];
|
||||
$length = mb_strlen($string, $encoding);
|
||||
|
||||
for ($i = 0; $i < $length; $i += $split_length) {
|
||||
$result[] = mb_substr($string, $i, $split_length, $encoding);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function mb_strtolower($s, $encoding = null)
|
||||
{
|
||||
return self::mb_convert_case($s, \MB_CASE_LOWER, $encoding);
|
||||
}
|
||||
|
||||
public static function mb_strtoupper($s, $encoding = null)
|
||||
{
|
||||
return self::mb_convert_case($s, \MB_CASE_UPPER, $encoding);
|
||||
}
|
||||
|
||||
public static function mb_substitute_character($c = null)
|
||||
{
|
||||
if (null === $c) {
|
||||
return 'none';
|
||||
}
|
||||
if (0 === strcasecmp($c, 'none')) {
|
||||
return true;
|
||||
}
|
||||
if (80000 > \PHP_VERSION_ID) {
|
||||
return false;
|
||||
}
|
||||
if (\is_int($c) || 'long' === $c || 'entity' === $c) {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new \ValueError('Argument #1 ($substitute_character) must be "none", "long", "entity" or a valid codepoint');
|
||||
}
|
||||
|
||||
public static function mb_substr($s, $start, $length = null, $encoding = null)
|
||||
{
|
||||
$encoding = self::getEncoding($encoding);
|
||||
if ('CP850' === $encoding || 'ASCII' === $encoding) {
|
||||
return (string) substr($s, $start, null === $length ? 2147483647 : $length);
|
||||
}
|
||||
|
||||
if ($start < 0) {
|
||||
$start = iconv_strlen($s, $encoding) + $start;
|
||||
if ($start < 0) {
|
||||
$start = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $length) {
|
||||
$length = 2147483647;
|
||||
} elseif ($length < 0) {
|
||||
$length = iconv_strlen($s, $encoding) + $length - $start;
|
||||
if ($length < 0) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return (string) iconv_substr($s, $start, $length, $encoding);
|
||||
}
|
||||
|
||||
public static function mb_stripos($haystack, $needle, $offset = 0, $encoding = null)
|
||||
{
|
||||
[$haystack, $needle] = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], [
|
||||
self::mb_convert_case($haystack, \MB_CASE_LOWER, $encoding),
|
||||
self::mb_convert_case($needle, \MB_CASE_LOWER, $encoding),
|
||||
]);
|
||||
|
||||
return self::mb_strpos($haystack, $needle, $offset, $encoding);
|
||||
}
|
||||
|
||||
public static function mb_stristr($haystack, $needle, $part = false, $encoding = null)
|
||||
{
|
||||
$pos = self::mb_stripos($haystack, $needle, 0, $encoding);
|
||||
|
||||
return self::getSubpart($pos, $part, $haystack, $encoding);
|
||||
}
|
||||
|
||||
public static function mb_strrchr($haystack, $needle, $part = false, $encoding = null)
|
||||
{
|
||||
$encoding = self::getEncoding($encoding);
|
||||
if ('CP850' === $encoding || 'ASCII' === $encoding) {
|
||||
$pos = strrpos($haystack, $needle);
|
||||
} else {
|
||||
$needle = self::mb_substr($needle, 0, 1, $encoding);
|
||||
$pos = iconv_strrpos($haystack, $needle, $encoding);
|
||||
}
|
||||
|
||||
return self::getSubpart($pos, $part, $haystack, $encoding);
|
||||
}
|
||||
|
||||
public static function mb_strrichr($haystack, $needle, $part = false, $encoding = null)
|
||||
{
|
||||
$needle = self::mb_substr($needle, 0, 1, $encoding);
|
||||
$pos = self::mb_strripos($haystack, $needle, $encoding);
|
||||
|
||||
return self::getSubpart($pos, $part, $haystack, $encoding);
|
||||
}
|
||||
|
||||
public static function mb_strripos($haystack, $needle, $offset = 0, $encoding = null)
|
||||
{
|
||||
$haystack = self::mb_convert_case($haystack, \MB_CASE_LOWER, $encoding);
|
||||
$needle = self::mb_convert_case($needle, \MB_CASE_LOWER, $encoding);
|
||||
|
||||
$haystack = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], $haystack);
|
||||
$needle = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], $needle);
|
||||
|
||||
return self::mb_strrpos($haystack, $needle, $offset, $encoding);
|
||||
}
|
||||
|
||||
public static function mb_strstr($haystack, $needle, $part = false, $encoding = null)
|
||||
{
|
||||
$pos = strpos($haystack, $needle);
|
||||
if (false === $pos) {
|
||||
return false;
|
||||
}
|
||||
if ($part) {
|
||||
return substr($haystack, 0, $pos);
|
||||
}
|
||||
|
||||
return substr($haystack, $pos);
|
||||
}
|
||||
|
||||
public static function mb_get_info($type = 'all')
|
||||
{
|
||||
$info = [
|
||||
'internal_encoding' => self::$internalEncoding,
|
||||
'http_output' => 'pass',
|
||||
'http_output_conv_mimetypes' => '^(text/|application/xhtml\+xml)',
|
||||
'func_overload' => 0,
|
||||
'func_overload_list' => 'no overload',
|
||||
'mail_charset' => 'UTF-8',
|
||||
'mail_header_encoding' => 'BASE64',
|
||||
'mail_body_encoding' => 'BASE64',
|
||||
'illegal_chars' => 0,
|
||||
'encoding_translation' => 'Off',
|
||||
'language' => self::$language,
|
||||
'detect_order' => self::$encodingList,
|
||||
'substitute_character' => 'none',
|
||||
'strict_detection' => 'Off',
|
||||
];
|
||||
|
||||
if ('all' === $type) {
|
||||
return $info;
|
||||
}
|
||||
if (isset($info[$type])) {
|
||||
return $info[$type];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function mb_http_input($type = '')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function mb_http_output($encoding = null)
|
||||
{
|
||||
return null !== $encoding ? 'pass' === $encoding : 'pass';
|
||||
}
|
||||
|
||||
public static function mb_strwidth($s, $encoding = null)
|
||||
{
|
||||
$encoding = self::getEncoding($encoding);
|
||||
|
||||
if ('UTF-8' !== $encoding) {
|
||||
$s = iconv($encoding, 'UTF-8//IGNORE', $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);
|
||||
|
||||
return ($wide << 1) + iconv_strlen($s, 'UTF-8');
|
||||
}
|
||||
|
||||
public static function mb_substr_count($haystack, $needle, $encoding = null)
|
||||
{
|
||||
return substr_count($haystack, $needle);
|
||||
}
|
||||
|
||||
public static function mb_output_handler($contents, $status)
|
||||
{
|
||||
return $contents;
|
||||
}
|
||||
|
||||
public static function mb_chr($code, $encoding = null)
|
||||
{
|
||||
if (0x80 > $code %= 0x200000) {
|
||||
$s = \chr($code);
|
||||
} elseif (0x800 > $code) {
|
||||
$s = \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F);
|
||||
} elseif (0x10000 > $code) {
|
||||
$s = \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F);
|
||||
} else {
|
||||
$s = \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F);
|
||||
}
|
||||
|
||||
if ('UTF-8' !== $encoding = self::getEncoding($encoding)) {
|
||||
$s = mb_convert_encoding($s, $encoding, 'UTF-8');
|
||||
}
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
public static function mb_ord($s, $encoding = null)
|
||||
{
|
||||
if ('UTF-8' !== $encoding = self::getEncoding($encoding)) {
|
||||
$s = mb_convert_encoding($s, 'UTF-8', $encoding);
|
||||
}
|
||||
|
||||
if (1 === \strlen($s)) {
|
||||
return \ord($s);
|
||||
}
|
||||
|
||||
$code = ($s = unpack('C*', substr($s, 0, 4))) ? $s[1] : 0;
|
||||
if (0xF0 <= $code) {
|
||||
return (($code - 0xF0) << 18) + (($s[2] - 0x80) << 12) + (($s[3] - 0x80) << 6) + $s[4] - 0x80;
|
||||
}
|
||||
if (0xE0 <= $code) {
|
||||
return (($code - 0xE0) << 12) + (($s[2] - 0x80) << 6) + $s[3] - 0x80;
|
||||
}
|
||||
if (0xC0 <= $code) {
|
||||
return (($code - 0xC0) << 6) + $s[2] - 0x80;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
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');
|
||||
}
|
||||
|
||||
if (self::mb_strlen($pad_string, $encoding) <= 0) {
|
||||
throw new \ValueError('mb_str_pad(): Argument #3 ($pad_string) must be a non-empty string');
|
||||
}
|
||||
|
||||
$paddingRequired = $length - self::mb_strlen($string, $encoding);
|
||||
|
||||
if ($paddingRequired < 1) {
|
||||
return $string;
|
||||
}
|
||||
|
||||
switch ($pad_type) {
|
||||
case \STR_PAD_LEFT:
|
||||
return self::mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding).$string;
|
||||
case \STR_PAD_RIGHT:
|
||||
return $string.self::mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding);
|
||||
default:
|
||||
$leftPaddingLength = floor($paddingRequired / 2);
|
||||
$rightPaddingLength = $paddingRequired - $leftPaddingLength;
|
||||
|
||||
return self::mb_substr(str_repeat($pad_string, $leftPaddingLength), 0, $leftPaddingLength, $encoding).$string.self::mb_substr(str_repeat($pad_string, $rightPaddingLength), 0, $rightPaddingLength, $encoding);
|
||||
}
|
||||
}
|
||||
|
||||
public static function mb_ucfirst(string $string, ?string $encoding = null): string
|
||||
{
|
||||
if (null === $encoding) {
|
||||
$encoding = self::mb_internal_encoding();
|
||||
} else {
|
||||
self::assertEncoding($encoding, 'mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given');
|
||||
}
|
||||
|
||||
$firstChar = mb_substr($string, 0, 1, $encoding);
|
||||
$firstChar = mb_convert_case($firstChar, \MB_CASE_TITLE, $encoding);
|
||||
|
||||
return $firstChar.mb_substr($string, 1, null, $encoding);
|
||||
}
|
||||
|
||||
public static function mb_lcfirst(string $string, ?string $encoding = null): string
|
||||
{
|
||||
if (null === $encoding) {
|
||||
$encoding = self::mb_internal_encoding();
|
||||
} else {
|
||||
self::assertEncoding($encoding, 'mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given');
|
||||
}
|
||||
|
||||
$firstChar = mb_substr($string, 0, 1, $encoding);
|
||||
$firstChar = mb_convert_case($firstChar, \MB_CASE_LOWER, $encoding);
|
||||
|
||||
return $firstChar.mb_substr($string, 1, null, $encoding);
|
||||
}
|
||||
|
||||
private static function getSubpart($pos, $part, $haystack, $encoding)
|
||||
{
|
||||
if (false === $pos) {
|
||||
return false;
|
||||
}
|
||||
if ($part) {
|
||||
return self::mb_substr($haystack, 0, $pos, $encoding);
|
||||
}
|
||||
|
||||
return self::mb_substr($haystack, $pos, null, $encoding);
|
||||
}
|
||||
|
||||
private static function html_encoding_callback(array $m)
|
||||
{
|
||||
$i = 1;
|
||||
$entities = '';
|
||||
$m = unpack('C*', htmlentities($m[0], \ENT_COMPAT, 'UTF-8'));
|
||||
|
||||
while (isset($m[$i])) {
|
||||
if (0x80 > $m[$i]) {
|
||||
$entities .= \chr($m[$i++]);
|
||||
continue;
|
||||
}
|
||||
if (0xF0 <= $m[$i]) {
|
||||
$c = (($m[$i++] - 0xF0) << 18) + (($m[$i++] - 0x80) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80;
|
||||
} elseif (0xE0 <= $m[$i]) {
|
||||
$c = (($m[$i++] - 0xE0) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80;
|
||||
} else {
|
||||
$c = (($m[$i++] - 0xC0) << 6) + $m[$i++] - 0x80;
|
||||
}
|
||||
|
||||
$entities .= '&#'.$c.';';
|
||||
}
|
||||
|
||||
return $entities;
|
||||
}
|
||||
|
||||
private static function title_case(array $s)
|
||||
{
|
||||
return self::mb_convert_case($s[1], \MB_CASE_UPPER, 'UTF-8').self::mb_convert_case($s[2], \MB_CASE_LOWER, 'UTF-8');
|
||||
}
|
||||
|
||||
private static function getData($file)
|
||||
{
|
||||
if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) {
|
||||
return require $file;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static function getEncoding($encoding)
|
||||
{
|
||||
if (null === $encoding) {
|
||||
return self::$internalEncoding;
|
||||
}
|
||||
|
||||
if ('UTF-8' === $encoding) {
|
||||
return 'UTF-8';
|
||||
}
|
||||
|
||||
$encoding = strtoupper($encoding);
|
||||
|
||||
if ('8BIT' === $encoding || 'BINARY' === $encoding) {
|
||||
return 'CP850';
|
||||
}
|
||||
|
||||
if ('UTF8' === $encoding) {
|
||||
return 'UTF-8';
|
||||
}
|
||||
|
||||
return $encoding;
|
||||
}
|
||||
|
||||
private static function assertEncoding(string $encoding, string $errorFormat): void
|
||||
{
|
||||
try {
|
||||
$validEncoding = @self::mb_check_encoding('', $encoding);
|
||||
} catch (\ValueError $e) {
|
||||
throw new \ValueError(\sprintf($errorFormat, $encoding));
|
||||
}
|
||||
|
||||
// BC for PHP 7.3 and lower
|
||||
if (!$validEncoding) {
|
||||
throw new \ValueError(\sprintf($errorFormat, $encoding));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
Symfony Polyfill / Mbstring
|
||||
===========================
|
||||
|
||||
This component provides a partial, native PHP implementation for the
|
||||
[Mbstring](https://php.net/mbstring) extension.
|
||||
|
||||
More information can be found in the
|
||||
[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
This library is released under the [MIT license](LICENSE).
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'İ' => 'i̇',
|
||||
'µ' => 'μ',
|
||||
'ſ' => 's',
|
||||
'ͅ' => 'ι',
|
||||
'ς' => 'σ',
|
||||
'ϐ' => 'β',
|
||||
'ϑ' => 'θ',
|
||||
'ϕ' => 'φ',
|
||||
'ϖ' => 'π',
|
||||
'ϰ' => 'κ',
|
||||
'ϱ' => 'ρ',
|
||||
'ϵ' => 'ε',
|
||||
'ẛ' => 'ṡ',
|
||||
'ι' => 'ι',
|
||||
'ß' => 'ss',
|
||||
'ʼn' => 'ʼn',
|
||||
'ǰ' => 'ǰ',
|
||||
'ΐ' => 'ΐ',
|
||||
'ΰ' => 'ΰ',
|
||||
'և' => 'եւ',
|
||||
'ẖ' => 'ẖ',
|
||||
'ẗ' => 'ẗ',
|
||||
'ẘ' => 'ẘ',
|
||||
'ẙ' => 'ẙ',
|
||||
'ẚ' => 'aʾ',
|
||||
'ẞ' => 'ss',
|
||||
'ὐ' => 'ὐ',
|
||||
'ὒ' => 'ὒ',
|
||||
'ὔ' => 'ὔ',
|
||||
'ὖ' => 'ὖ',
|
||||
'ᾀ' => 'ἀι',
|
||||
'ᾁ' => 'ἁι',
|
||||
'ᾂ' => 'ἂι',
|
||||
'ᾃ' => 'ἃι',
|
||||
'ᾄ' => 'ἄι',
|
||||
'ᾅ' => 'ἅι',
|
||||
'ᾆ' => 'ἆι',
|
||||
'ᾇ' => 'ἇι',
|
||||
'ᾈ' => 'ἀι',
|
||||
'ᾉ' => 'ἁι',
|
||||
'ᾊ' => 'ἂι',
|
||||
'ᾋ' => 'ἃι',
|
||||
'ᾌ' => 'ἄι',
|
||||
'ᾍ' => 'ἅι',
|
||||
'ᾎ' => 'ἆι',
|
||||
'ᾏ' => 'ἇι',
|
||||
'ᾐ' => 'ἠι',
|
||||
'ᾑ' => 'ἡι',
|
||||
'ᾒ' => 'ἢι',
|
||||
'ᾓ' => 'ἣι',
|
||||
'ᾔ' => 'ἤι',
|
||||
'ᾕ' => 'ἥι',
|
||||
'ᾖ' => 'ἦι',
|
||||
'ᾗ' => 'ἧι',
|
||||
'ᾘ' => 'ἠι',
|
||||
'ᾙ' => 'ἡι',
|
||||
'ᾚ' => 'ἢι',
|
||||
'ᾛ' => 'ἣι',
|
||||
'ᾜ' => 'ἤι',
|
||||
'ᾝ' => 'ἥι',
|
||||
'ᾞ' => 'ἦι',
|
||||
'ᾟ' => 'ἧι',
|
||||
'ᾠ' => 'ὠι',
|
||||
'ᾡ' => 'ὡι',
|
||||
'ᾢ' => 'ὢι',
|
||||
'ᾣ' => 'ὣι',
|
||||
'ᾤ' => 'ὤι',
|
||||
'ᾥ' => 'ὥι',
|
||||
'ᾦ' => 'ὦι',
|
||||
'ᾧ' => 'ὧι',
|
||||
'ᾨ' => 'ὠι',
|
||||
'ᾩ' => 'ὡι',
|
||||
'ᾪ' => 'ὢι',
|
||||
'ᾫ' => 'ὣι',
|
||||
'ᾬ' => 'ὤι',
|
||||
'ᾭ' => 'ὥι',
|
||||
'ᾮ' => 'ὦι',
|
||||
'ᾯ' => 'ὧι',
|
||||
'ᾲ' => 'ὰι',
|
||||
'ᾳ' => 'αι',
|
||||
'ᾴ' => 'άι',
|
||||
'ᾶ' => 'ᾶ',
|
||||
'ᾷ' => 'ᾶι',
|
||||
'ᾼ' => 'αι',
|
||||
'ῂ' => 'ὴι',
|
||||
'ῃ' => 'ηι',
|
||||
'ῄ' => 'ήι',
|
||||
'ῆ' => 'ῆ',
|
||||
'ῇ' => 'ῆι',
|
||||
'ῌ' => 'ηι',
|
||||
'ῒ' => 'ῒ',
|
||||
'ῖ' => 'ῖ',
|
||||
'ῗ' => 'ῗ',
|
||||
'ῢ' => 'ῢ',
|
||||
'ῤ' => 'ῤ',
|
||||
'ῦ' => 'ῦ',
|
||||
'ῧ' => 'ῧ',
|
||||
'ῲ' => 'ὼι',
|
||||
'ῳ' => 'ωι',
|
||||
'ῴ' => 'ώι',
|
||||
'ῶ' => 'ῶ',
|
||||
'ῷ' => 'ῶι',
|
||||
'ῼ' => 'ωι',
|
||||
'ff' => 'ff',
|
||||
'fi' => 'fi',
|
||||
'fl' => 'fl',
|
||||
'ffi' => 'ffi',
|
||||
'ffl' => 'ffl',
|
||||
'ſt' => 'st',
|
||||
'st' => 'st',
|
||||
'ﬓ' => 'մն',
|
||||
'ﬔ' => 'մե',
|
||||
'ﬕ' => 'մի',
|
||||
'ﬖ' => 'վն',
|
||||
'ﬗ' => 'մխ',
|
||||
];
|
||||
+1397
File diff suppressed because it is too large
Load Diff
+5
File diff suppressed because one or more lines are too long
+1489
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,159 @@
|
||||
<?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 (\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 (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);
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?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(array|string|null $string, ?string $to_encoding, array|string|null $from_encoding = null): array|string|false { return p\Mbstring::mb_convert_encoding($string ?? '', (string) $to_encoding, $from_encoding); }
|
||||
}
|
||||
if (!function_exists('mb_decode_mimeheader')) {
|
||||
function mb_decode_mimeheader(?string $string): string { return p\Mbstring::mb_decode_mimeheader((string) $string); }
|
||||
}
|
||||
if (!function_exists('mb_encode_mimeheader')) {
|
||||
function mb_encode_mimeheader(?string $string, ?string $charset = null, ?string $transfer_encoding = null, ?string $newline = "\r\n", ?int $indent = 0): string { return p\Mbstring::mb_encode_mimeheader((string) $string, $charset, $transfer_encoding, (string) $newline, (int) $indent); }
|
||||
}
|
||||
if (!function_exists('mb_decode_numericentity')) {
|
||||
function mb_decode_numericentity(?string $string, array $map, ?string $encoding = null): string { return p\Mbstring::mb_decode_numericentity((string) $string, $map, $encoding); }
|
||||
}
|
||||
if (!function_exists('mb_encode_numericentity')) {
|
||||
function mb_encode_numericentity(?string $string, array $map, ?string $encoding = null, ?bool $hex = false): string { return p\Mbstring::mb_encode_numericentity((string) $string, $map, $encoding, (bool) $hex); }
|
||||
}
|
||||
if (!function_exists('mb_convert_case')) {
|
||||
function mb_convert_case(?string $string, ?int $mode, ?string $encoding = null): string { return p\Mbstring::mb_convert_case((string) $string, (int) $mode, $encoding); }
|
||||
}
|
||||
if (!function_exists('mb_internal_encoding')) {
|
||||
function mb_internal_encoding(?string $encoding = null): string|bool { return p\Mbstring::mb_internal_encoding($encoding); }
|
||||
}
|
||||
if (!function_exists('mb_language')) {
|
||||
function mb_language(?string $language = null): string|bool { return p\Mbstring::mb_language($language); }
|
||||
}
|
||||
if (!function_exists('mb_list_encodings')) {
|
||||
function mb_list_encodings(): array { return p\Mbstring::mb_list_encodings(); }
|
||||
}
|
||||
if (!function_exists('mb_encoding_aliases')) {
|
||||
function mb_encoding_aliases(?string $encoding): array { return p\Mbstring::mb_encoding_aliases((string) $encoding); }
|
||||
}
|
||||
if (!function_exists('mb_check_encoding')) {
|
||||
function mb_check_encoding(array|string|null $value = null, ?string $encoding = null): bool { return p\Mbstring::mb_check_encoding($value, $encoding); }
|
||||
}
|
||||
if (!function_exists('mb_detect_encoding')) {
|
||||
function mb_detect_encoding(?string $string, array|string|null $encodings = null, ?bool $strict = false): string|false { return p\Mbstring::mb_detect_encoding((string) $string, $encodings, (bool) $strict); }
|
||||
}
|
||||
if (!function_exists('mb_detect_order')) {
|
||||
function mb_detect_order(array|string|null $encoding = null): array|bool { return p\Mbstring::mb_detect_order($encoding); }
|
||||
}
|
||||
if (!function_exists('mb_parse_str')) {
|
||||
function mb_parse_str(?string $string, &$result = []): bool { parse_str((string) $string, $result); return (bool) $result; }
|
||||
}
|
||||
if (!function_exists('mb_strlen')) {
|
||||
function mb_strlen(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strlen((string) $string, $encoding); }
|
||||
}
|
||||
if (!function_exists('mb_strpos')) {
|
||||
function mb_strpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strpos((string) $haystack, (string) $needle, (int) $offset, $encoding); }
|
||||
}
|
||||
if (!function_exists('mb_strtolower')) {
|
||||
function mb_strtolower(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtolower((string) $string, $encoding); }
|
||||
}
|
||||
if (!function_exists('mb_strtoupper')) {
|
||||
function mb_strtoupper(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtoupper((string) $string, $encoding); }
|
||||
}
|
||||
if (!function_exists('mb_substitute_character')) {
|
||||
function mb_substitute_character(string|int|null $substitute_character = null): string|int|bool { return p\Mbstring::mb_substitute_character($substitute_character); }
|
||||
}
|
||||
if (!function_exists('mb_substr')) {
|
||||
function mb_substr(?string $string, ?int $start, ?int $length = null, ?string $encoding = null): string { return p\Mbstring::mb_substr((string) $string, (int) $start, $length, $encoding); }
|
||||
}
|
||||
if (!function_exists('mb_stripos')) {
|
||||
function mb_stripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_stripos((string) $haystack, (string) $needle, (int) $offset, $encoding); }
|
||||
}
|
||||
if (!function_exists('mb_stristr')) {
|
||||
function mb_stristr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_stristr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); }
|
||||
}
|
||||
if (!function_exists('mb_strrchr')) {
|
||||
function mb_strrchr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrchr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); }
|
||||
}
|
||||
if (!function_exists('mb_strrichr')) {
|
||||
function mb_strrichr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrichr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); }
|
||||
}
|
||||
if (!function_exists('mb_strripos')) {
|
||||
function mb_strripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strripos((string) $haystack, (string) $needle, (int) $offset, $encoding); }
|
||||
}
|
||||
if (!function_exists('mb_strrpos')) {
|
||||
function mb_strrpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strrpos((string) $haystack, (string) $needle, (int) $offset, $encoding); }
|
||||
}
|
||||
if (!function_exists('mb_strstr')) {
|
||||
function mb_strstr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strstr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); }
|
||||
}
|
||||
if (!function_exists('mb_get_info')) {
|
||||
function mb_get_info(?string $type = 'all'): array|string|int|false { return p\Mbstring::mb_get_info((string) $type); }
|
||||
}
|
||||
if (!function_exists('mb_http_output')) {
|
||||
function mb_http_output(?string $encoding = null): string|bool { return p\Mbstring::mb_http_output($encoding); }
|
||||
}
|
||||
if (!function_exists('mb_strwidth')) {
|
||||
function mb_strwidth(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strwidth((string) $string, $encoding); }
|
||||
}
|
||||
if (!function_exists('mb_substr_count')) {
|
||||
function mb_substr_count(?string $haystack, ?string $needle, ?string $encoding = null): int { return p\Mbstring::mb_substr_count((string) $haystack, (string) $needle, $encoding); }
|
||||
}
|
||||
if (!function_exists('mb_output_handler')) {
|
||||
function mb_output_handler(?string $string, ?int $status): string { return p\Mbstring::mb_output_handler((string) $string, (int) $status); }
|
||||
}
|
||||
if (!function_exists('mb_http_input')) {
|
||||
function mb_http_input(?string $type = null): array|string|false { return p\Mbstring::mb_http_input($type); }
|
||||
}
|
||||
|
||||
if (!function_exists('mb_convert_variables')) {
|
||||
function mb_convert_variables(?string $to_encoding, array|string|null $from_encoding, mixed &$var, mixed &...$vars): string|false { return p\Mbstring::mb_convert_variables((string) $to_encoding, $from_encoding ?? '', $var, ...$vars); }
|
||||
}
|
||||
|
||||
if (!function_exists('mb_ord')) {
|
||||
function mb_ord(?string $string, ?string $encoding = null): int|false { return p\Mbstring::mb_ord((string) $string, $encoding); }
|
||||
}
|
||||
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); }
|
||||
}
|
||||
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); }
|
||||
}
|
||||
|
||||
if (!function_exists('mb_ucfirst')) {
|
||||
function mb_ucfirst($string, ?string $encoding = null): string { return p\Mbstring::mb_ucfirst($string, $encoding); }
|
||||
}
|
||||
|
||||
if (!function_exists('mb_lcfirst')) {
|
||||
function mb_lcfirst($string, ?string $encoding = null): string { return p\Mbstring::mb_lcfirst($string, $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);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "symfony/polyfill-mbstring",
|
||||
"type": "library",
|
||||
"description": "Symfony polyfill for the Mbstring extension",
|
||||
"keywords": ["polyfill", "shim", "compatibility", "portable", "mbstring"],
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"provide": {
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Polyfill\\Mbstring\\": "" },
|
||||
"files": [ "bootstrap.php" ]
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "For best performance"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2020-present Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
<?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\Polyfill\Php80;
|
||||
|
||||
/**
|
||||
* @author Ion Bazan <ion.bazan@gmail.com>
|
||||
* @author Nico Oelgart <nicoswd@gmail.com>
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class Php80
|
||||
{
|
||||
public static function fdiv(float $dividend, float $divisor): float
|
||||
{
|
||||
return @($dividend / $divisor);
|
||||
}
|
||||
|
||||
public static function get_debug_type($value): string
|
||||
{
|
||||
switch (true) {
|
||||
case null === $value: return 'null';
|
||||
case \is_bool($value): return 'bool';
|
||||
case \is_string($value): return 'string';
|
||||
case \is_array($value): return 'array';
|
||||
case \is_int($value): return 'int';
|
||||
case \is_float($value): return 'float';
|
||||
case \is_object($value): break;
|
||||
case $value instanceof \__PHP_Incomplete_Class: return '__PHP_Incomplete_Class';
|
||||
default:
|
||||
if (null === $type = @get_resource_type($value)) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
if ('Unknown' === $type) {
|
||||
$type = 'closed';
|
||||
}
|
||||
|
||||
return "resource ($type)";
|
||||
}
|
||||
|
||||
$class = \get_class($value);
|
||||
|
||||
if (false === strpos($class, '@')) {
|
||||
return $class;
|
||||
}
|
||||
|
||||
return (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous';
|
||||
}
|
||||
|
||||
public static function get_resource_id($res): int
|
||||
{
|
||||
if (!\is_resource($res) && null === @get_resource_type($res)) {
|
||||
throw new \TypeError(sprintf('Argument 1 passed to get_resource_id() must be of the type resource, %s given', get_debug_type($res)));
|
||||
}
|
||||
|
||||
return (int) $res;
|
||||
}
|
||||
|
||||
public static function preg_last_error_msg(): string
|
||||
{
|
||||
switch (preg_last_error()) {
|
||||
case \PREG_INTERNAL_ERROR:
|
||||
return 'Internal error';
|
||||
case \PREG_BAD_UTF8_ERROR:
|
||||
return 'Malformed UTF-8 characters, possibly incorrectly encoded';
|
||||
case \PREG_BAD_UTF8_OFFSET_ERROR:
|
||||
return 'The offset did not correspond to the beginning of a valid UTF-8 code point';
|
||||
case \PREG_BACKTRACK_LIMIT_ERROR:
|
||||
return 'Backtrack limit exhausted';
|
||||
case \PREG_RECURSION_LIMIT_ERROR:
|
||||
return 'Recursion limit exhausted';
|
||||
case \PREG_JIT_STACKLIMIT_ERROR:
|
||||
return 'JIT stack limit exhausted';
|
||||
case \PREG_NO_ERROR:
|
||||
return 'No error';
|
||||
default:
|
||||
return 'Unknown error';
|
||||
}
|
||||
}
|
||||
|
||||
public static function str_contains(string $haystack, string $needle): bool
|
||||
{
|
||||
return '' === $needle || false !== strpos($haystack, $needle);
|
||||
}
|
||||
|
||||
public static function str_starts_with(string $haystack, string $needle): bool
|
||||
{
|
||||
return 0 === strncmp($haystack, $needle, \strlen($needle));
|
||||
}
|
||||
|
||||
public static function str_ends_with(string $haystack, string $needle): bool
|
||||
{
|
||||
if ('' === $needle || $needle === $haystack) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ('' === $haystack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$needleLength = \strlen($needle);
|
||||
|
||||
return $needleLength <= \strlen($haystack) && 0 === substr_compare($haystack, $needle, -$needleLength);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?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\Polyfill\Php80;
|
||||
|
||||
/**
|
||||
* @author Fedonyuk Anton <info@ensostudio.ru>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class PhpToken implements \Stringable
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $text;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $line;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $pos;
|
||||
|
||||
public function __construct(int $id, string $text, int $line = -1, int $position = -1)
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->text = $text;
|
||||
$this->line = $line;
|
||||
$this->pos = $position;
|
||||
}
|
||||
|
||||
public function getTokenName(): ?string
|
||||
{
|
||||
if ('UNKNOWN' === $name = token_name($this->id)) {
|
||||
$name = \strlen($this->text) > 1 || \ord($this->text) < 32 ? null : $this->text;
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|string|array $kind
|
||||
*/
|
||||
public function is($kind): bool
|
||||
{
|
||||
foreach ((array) $kind as $value) {
|
||||
if (\in_array($value, [$this->id, $this->text], true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isIgnorable(): bool
|
||||
{
|
||||
return \in_array($this->id, [\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT, \T_OPEN_TAG], true);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return (string) $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static[]
|
||||
*/
|
||||
public static function tokenize(string $code, int $flags = 0): array
|
||||
{
|
||||
$line = 1;
|
||||
$position = 0;
|
||||
$tokens = token_get_all($code, $flags);
|
||||
foreach ($tokens as $index => $token) {
|
||||
if (\is_string($token)) {
|
||||
$id = \ord($token);
|
||||
$text = $token;
|
||||
} else {
|
||||
[$id, $text, $line] = $token;
|
||||
}
|
||||
$tokens[$index] = new static($id, $text, $line, $position);
|
||||
$position += \strlen($text);
|
||||
}
|
||||
|
||||
return $tokens;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
Symfony Polyfill / Php80
|
||||
========================
|
||||
|
||||
This component provides features added to PHP 8.0 core:
|
||||
|
||||
- [`Stringable`](https://php.net/stringable) interface
|
||||
- [`fdiv`](https://php.net/fdiv)
|
||||
- [`ValueError`](https://php.net/valueerror) class
|
||||
- [`UnhandledMatchError`](https://php.net/unhandledmatcherror) class
|
||||
- `FILTER_VALIDATE_BOOL` constant
|
||||
- [`get_debug_type`](https://php.net/get_debug_type)
|
||||
- [`PhpToken`](https://php.net/phptoken) class
|
||||
- [`preg_last_error_msg`](https://php.net/preg_last_error_msg)
|
||||
- [`str_contains`](https://php.net/str_contains)
|
||||
- [`str_starts_with`](https://php.net/str_starts_with)
|
||||
- [`str_ends_with`](https://php.net/str_ends_with)
|
||||
- [`get_resource_id`](https://php.net/get_resource_id)
|
||||
|
||||
More information can be found in the
|
||||
[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
This library is released under the [MIT license](LICENSE).
|
||||
@@ -0,0 +1,31 @@
|
||||
<?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.
|
||||
*/
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
final class Attribute
|
||||
{
|
||||
public const TARGET_CLASS = 1;
|
||||
public const TARGET_FUNCTION = 2;
|
||||
public const TARGET_METHOD = 4;
|
||||
public const TARGET_PROPERTY = 8;
|
||||
public const TARGET_CLASS_CONSTANT = 16;
|
||||
public const TARGET_PARAMETER = 32;
|
||||
public const TARGET_ALL = 63;
|
||||
public const IS_REPEATABLE = 64;
|
||||
|
||||
/** @var int */
|
||||
public $flags;
|
||||
|
||||
public function __construct(int $flags = self::TARGET_ALL)
|
||||
{
|
||||
$this->flags = $flags;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?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 < 80000 && extension_loaded('tokenizer')) {
|
||||
class PhpToken extends Symfony\Polyfill\Php80\PhpToken
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
if (\PHP_VERSION_ID < 80000) {
|
||||
interface Stringable
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString();
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?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 < 80000) {
|
||||
class UnhandledMatchError extends Error
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?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 < 80000) {
|
||||
class ValueError extends Error
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?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\Php80 as p;
|
||||
|
||||
if (\PHP_VERSION_ID >= 80000) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!defined('FILTER_VALIDATE_BOOL') && defined('FILTER_VALIDATE_BOOLEAN')) {
|
||||
define('FILTER_VALIDATE_BOOL', \FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
if (!function_exists('fdiv')) {
|
||||
function fdiv(float $num1, float $num2): float { return p\Php80::fdiv($num1, $num2); }
|
||||
}
|
||||
if (!function_exists('preg_last_error_msg')) {
|
||||
function preg_last_error_msg(): string { return p\Php80::preg_last_error_msg(); }
|
||||
}
|
||||
if (!function_exists('str_contains')) {
|
||||
function str_contains(?string $haystack, ?string $needle): bool { return p\Php80::str_contains($haystack ?? '', $needle ?? ''); }
|
||||
}
|
||||
if (!function_exists('str_starts_with')) {
|
||||
function str_starts_with(?string $haystack, ?string $needle): bool { return p\Php80::str_starts_with($haystack ?? '', $needle ?? ''); }
|
||||
}
|
||||
if (!function_exists('str_ends_with')) {
|
||||
function str_ends_with(?string $haystack, ?string $needle): bool { return p\Php80::str_ends_with($haystack ?? '', $needle ?? ''); }
|
||||
}
|
||||
if (!function_exists('get_debug_type')) {
|
||||
function get_debug_type($value): string { return p\Php80::get_debug_type($value); }
|
||||
}
|
||||
if (!function_exists('get_resource_id')) {
|
||||
function get_resource_id($resource): int { return p\Php80::get_resource_id($resource); }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "symfony/polyfill-php80",
|
||||
"type": "library",
|
||||
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
|
||||
"keywords": ["polyfill", "shim", "compatibility", "portable"],
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ion Bazan",
|
||||
"email": "ion.bazan@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Polyfill\\Php80\\": "" },
|
||||
"files": [ "bootstrap.php" ],
|
||||
"classmap": [ "Resources/stubs" ]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
}
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
5.2.0
|
||||
-----
|
||||
|
||||
* added `Process::setOptions()` to set `Process` specific options
|
||||
* added option `create_new_console` to allow a subprocess to continue
|
||||
to run after the main script exited, both on Linux and on Windows
|
||||
|
||||
5.1.0
|
||||
-----
|
||||
|
||||
* added `Process::getStartTime()` to retrieve the start time of the process as float
|
||||
|
||||
5.0.0
|
||||
-----
|
||||
|
||||
* removed `Process::inheritEnvironmentVariables()`
|
||||
* removed `PhpProcess::setPhpBinary()`
|
||||
* `Process` must be instantiated with a command array, use `Process::fromShellCommandline()` when the command should be parsed by the shell
|
||||
* removed `Process::setCommandLine()`
|
||||
|
||||
4.4.0
|
||||
-----
|
||||
|
||||
* deprecated `Process::inheritEnvironmentVariables()`: env variables are always inherited.
|
||||
* added `Process::getLastOutputTime()` method
|
||||
|
||||
4.2.0
|
||||
-----
|
||||
|
||||
* added the `Process::fromShellCommandline()` to run commands in a shell wrapper
|
||||
* deprecated passing a command as string when creating a `Process` instance
|
||||
* deprecated the `Process::setCommandline()` and the `PhpProcess::setPhpBinary()` methods
|
||||
* added the `Process::waitUntil()` method to wait for the process only for a
|
||||
specific output, then continue the normal execution of your application
|
||||
|
||||
4.1.0
|
||||
-----
|
||||
|
||||
* added the `Process::isTtySupported()` method that allows to check for TTY support
|
||||
* made `PhpExecutableFinder` look for the `PHP_BINARY` env var when searching the php binary
|
||||
* added the `ProcessSignaledException` class to properly catch signaled process errors
|
||||
|
||||
4.0.0
|
||||
-----
|
||||
|
||||
* environment variables will always be inherited
|
||||
* added a second `array $env = []` argument to the `start()`, `run()`,
|
||||
`mustRun()`, and `restart()` methods of the `Process` class
|
||||
* added a second `array $env = []` argument to the `start()` method of the
|
||||
`PhpProcess` class
|
||||
* the `ProcessUtils::escapeArgument()` method has been removed
|
||||
* the `areEnvironmentVariablesInherited()`, `getOptions()`, and `setOptions()`
|
||||
methods of the `Process` class have been removed
|
||||
* support for passing `proc_open()` options has been removed
|
||||
* removed the `ProcessBuilder` class, use the `Process` class instead
|
||||
* removed the `getEnhanceWindowsCompatibility()` and `setEnhanceWindowsCompatibility()` methods of the `Process` class
|
||||
* passing a not existing working directory to the constructor of the `Symfony\Component\Process\Process` class is not
|
||||
supported anymore
|
||||
|
||||
3.4.0
|
||||
-----
|
||||
|
||||
* deprecated the ProcessBuilder class
|
||||
* deprecated calling `Process::start()` without setting a valid working directory beforehand (via `setWorkingDirectory()` or constructor)
|
||||
|
||||
3.3.0
|
||||
-----
|
||||
|
||||
* added command line arrays in the `Process` class
|
||||
* added `$env` argument to `Process::start()`, `run()`, `mustRun()` and `restart()` methods
|
||||
* deprecated the `ProcessUtils::escapeArgument()` method
|
||||
* deprecated not inheriting environment variables
|
||||
* deprecated configuring `proc_open()` options
|
||||
* deprecated configuring enhanced Windows compatibility
|
||||
* deprecated configuring enhanced sigchild compatibility
|
||||
|
||||
2.5.0
|
||||
-----
|
||||
|
||||
* added support for PTY mode
|
||||
* added the convenience method "mustRun"
|
||||
* deprecation: Process::setStdin() is deprecated in favor of Process::setInput()
|
||||
* deprecation: Process::getStdin() is deprecated in favor of Process::getInput()
|
||||
* deprecation: Process::setInput() and ProcessBuilder::setInput() do not accept non-scalar types
|
||||
|
||||
2.4.0
|
||||
-----
|
||||
|
||||
* added the ability to define an idle timeout
|
||||
|
||||
2.3.0
|
||||
-----
|
||||
|
||||
* added ProcessUtils::escapeArgument() to fix the bug in escapeshellarg() function on Windows
|
||||
* added Process::signal()
|
||||
* added Process::getPid()
|
||||
* added support for a TTY mode
|
||||
|
||||
2.2.0
|
||||
-----
|
||||
|
||||
* added ProcessBuilder::setArguments() to reset the arguments on a builder
|
||||
* added a way to retrieve the standard and error output incrementally
|
||||
* added Process:restart()
|
||||
|
||||
2.1.0
|
||||
-----
|
||||
|
||||
* added support for non-blocking processes (start(), wait(), isRunning(), stop())
|
||||
* enhanced Windows compatibility
|
||||
* added Process::getExitCodeText() that returns a string representation for
|
||||
the exit code returned by the process
|
||||
* added ProcessBuilder
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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\Process\Exception;
|
||||
|
||||
/**
|
||||
* Marker Interface for the Process Component.
|
||||
*
|
||||
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
|
||||
*/
|
||||
interface ExceptionInterface extends \Throwable
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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\Process\Exception;
|
||||
|
||||
/**
|
||||
* InvalidArgumentException for the Process Component.
|
||||
*
|
||||
* @author Romain Neutron <imprec@gmail.com>
|
||||
*/
|
||||
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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\Process\Exception;
|
||||
|
||||
/**
|
||||
* LogicException for the Process Component.
|
||||
*
|
||||
* @author Romain Neutron <imprec@gmail.com>
|
||||
*/
|
||||
class LogicException extends \LogicException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?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\Process\Exception;
|
||||
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
/**
|
||||
* Exception for failed processes.
|
||||
*
|
||||
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
|
||||
*/
|
||||
class ProcessFailedException extends RuntimeException
|
||||
{
|
||||
private $process;
|
||||
|
||||
public function __construct(Process $process)
|
||||
{
|
||||
if ($process->isSuccessful()) {
|
||||
throw new InvalidArgumentException('Expected a failed process, but the given process was successful.');
|
||||
}
|
||||
|
||||
$error = sprintf('The command "%s" failed.'."\n\nExit Code: %s(%s)\n\nWorking directory: %s",
|
||||
$process->getCommandLine(),
|
||||
$process->getExitCode(),
|
||||
$process->getExitCodeText(),
|
||||
$process->getWorkingDirectory()
|
||||
);
|
||||
|
||||
if (!$process->isOutputDisabled()) {
|
||||
$error .= sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s",
|
||||
$process->getOutput(),
|
||||
$process->getErrorOutput()
|
||||
);
|
||||
}
|
||||
|
||||
parent::__construct($error);
|
||||
|
||||
$this->process = $process;
|
||||
}
|
||||
|
||||
public function getProcess()
|
||||
{
|
||||
return $this->process;
|
||||
}
|
||||
}
|
||||
@@ -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 Symfony\Component\Process\Exception;
|
||||
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
/**
|
||||
* Exception that is thrown when a process has been signaled.
|
||||
*
|
||||
* @author Sullivan Senechal <soullivaneuh@gmail.com>
|
||||
*/
|
||||
final class ProcessSignaledException extends RuntimeException
|
||||
{
|
||||
private $process;
|
||||
|
||||
public function __construct(Process $process)
|
||||
{
|
||||
$this->process = $process;
|
||||
|
||||
parent::__construct(sprintf('The process has been signaled with signal "%s".', $process->getTermSignal()));
|
||||
}
|
||||
|
||||
public function getProcess(): Process
|
||||
{
|
||||
return $this->process;
|
||||
}
|
||||
|
||||
public function getSignal(): int
|
||||
{
|
||||
return $this->getProcess()->getTermSignal();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?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\Process\Exception;
|
||||
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
/**
|
||||
* Exception that is thrown when a process times out.
|
||||
*
|
||||
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
|
||||
*/
|
||||
class ProcessTimedOutException extends RuntimeException
|
||||
{
|
||||
public const TYPE_GENERAL = 1;
|
||||
public const TYPE_IDLE = 2;
|
||||
|
||||
private $process;
|
||||
private $timeoutType;
|
||||
|
||||
public function __construct(Process $process, int $timeoutType)
|
||||
{
|
||||
$this->process = $process;
|
||||
$this->timeoutType = $timeoutType;
|
||||
|
||||
parent::__construct(sprintf(
|
||||
'The process "%s" exceeded the timeout of %s seconds.',
|
||||
$process->getCommandLine(),
|
||||
$this->getExceededTimeout()
|
||||
));
|
||||
}
|
||||
|
||||
public function getProcess()
|
||||
{
|
||||
return $this->process;
|
||||
}
|
||||
|
||||
public function isGeneralTimeout()
|
||||
{
|
||||
return self::TYPE_GENERAL === $this->timeoutType;
|
||||
}
|
||||
|
||||
public function isIdleTimeout()
|
||||
{
|
||||
return self::TYPE_IDLE === $this->timeoutType;
|
||||
}
|
||||
|
||||
public function getExceededTimeout()
|
||||
{
|
||||
switch ($this->timeoutType) {
|
||||
case self::TYPE_GENERAL:
|
||||
return $this->process->getTimeout();
|
||||
|
||||
case self::TYPE_IDLE:
|
||||
return $this->process->getIdleTimeout();
|
||||
|
||||
default:
|
||||
throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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\Process\Exception;
|
||||
|
||||
/**
|
||||
* RuntimeException for the Process Component.
|
||||
*
|
||||
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
|
||||
*/
|
||||
class RuntimeException extends \RuntimeException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?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\Process;
|
||||
|
||||
/**
|
||||
* Generic executable finder.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
|
||||
*/
|
||||
class ExecutableFinder
|
||||
{
|
||||
private $suffixes = ['.exe', '.bat', '.cmd', '.com'];
|
||||
|
||||
/**
|
||||
* Replaces default suffixes of executable.
|
||||
*/
|
||||
public function setSuffixes(array $suffixes)
|
||||
{
|
||||
$this->suffixes = $suffixes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds new possible suffix to check for executable.
|
||||
*/
|
||||
public function addSuffix(string $suffix)
|
||||
{
|
||||
$this->suffixes[] = $suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an executable by name.
|
||||
*
|
||||
* @param string $name The executable name (without the extension)
|
||||
* @param string|null $default The default to return if no executable is found
|
||||
* @param array $extraDirs Additional dirs to check into
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function find(string $name, ?string $default = null, array $extraDirs = [])
|
||||
{
|
||||
if (\ini_get('open_basedir')) {
|
||||
$searchPath = array_merge(explode(\PATH_SEPARATOR, \ini_get('open_basedir')), $extraDirs);
|
||||
$dirs = [];
|
||||
foreach ($searchPath as $path) {
|
||||
// Silencing against https://bugs.php.net/69240
|
||||
if (@is_dir($path)) {
|
||||
$dirs[] = $path;
|
||||
} else {
|
||||
if (basename($path) == $name && @is_executable($path)) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$dirs = array_merge(
|
||||
explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')),
|
||||
$extraDirs
|
||||
);
|
||||
}
|
||||
|
||||
$suffixes = [''];
|
||||
if ('\\' === \DIRECTORY_SEPARATOR) {
|
||||
$pathExt = getenv('PATHEXT');
|
||||
$suffixes = array_merge($pathExt ? explode(\PATH_SEPARATOR, $pathExt) : $this->suffixes, $suffixes);
|
||||
}
|
||||
foreach ($suffixes as $suffix) {
|
||||
foreach ($dirs as $dir) {
|
||||
if (@is_file($file = $dir.\DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === \DIRECTORY_SEPARATOR || @is_executable($file))) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?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\Process;
|
||||
|
||||
use Symfony\Component\Process\Exception\RuntimeException;
|
||||
|
||||
/**
|
||||
* Provides a way to continuously write to the input of a Process until the InputStream is closed.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*
|
||||
* @implements \IteratorAggregate<int, string>
|
||||
*/
|
||||
class InputStream implements \IteratorAggregate
|
||||
{
|
||||
/** @var callable|null */
|
||||
private $onEmpty = null;
|
||||
private $input = [];
|
||||
private $open = true;
|
||||
|
||||
/**
|
||||
* Sets a callback that is called when the write buffer becomes empty.
|
||||
*/
|
||||
public function onEmpty(?callable $onEmpty = null)
|
||||
{
|
||||
$this->onEmpty = $onEmpty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an input to the write buffer.
|
||||
*
|
||||
* @param resource|string|int|float|bool|\Traversable|null $input The input to append as scalar,
|
||||
* stream resource or \Traversable
|
||||
*/
|
||||
public function write($input)
|
||||
{
|
||||
if (null === $input) {
|
||||
return;
|
||||
}
|
||||
if ($this->isClosed()) {
|
||||
throw new RuntimeException(sprintf('"%s" is closed.', static::class));
|
||||
}
|
||||
$this->input[] = ProcessUtils::validateInput(__METHOD__, $input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the write buffer.
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
$this->open = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether the write buffer is closed or not.
|
||||
*/
|
||||
public function isClosed()
|
||||
{
|
||||
return !$this->open;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Traversable<int, string>
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function getIterator()
|
||||
{
|
||||
$this->open = true;
|
||||
|
||||
while ($this->open || $this->input) {
|
||||
if (!$this->input) {
|
||||
yield '';
|
||||
continue;
|
||||
}
|
||||
$current = array_shift($this->input);
|
||||
|
||||
if ($current instanceof \Iterator) {
|
||||
yield from $current;
|
||||
} else {
|
||||
yield $current;
|
||||
}
|
||||
if (!$this->input && $this->open && null !== $onEmpty = $this->onEmpty) {
|
||||
$this->write($onEmpty($this));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2004-present Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,103 @@
|
||||
<?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\Process;
|
||||
|
||||
/**
|
||||
* An executable finder specifically designed for the PHP executable.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
|
||||
*/
|
||||
class PhpExecutableFinder
|
||||
{
|
||||
private $executableFinder;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->executableFinder = new ExecutableFinder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds The PHP executable.
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function find(bool $includeArgs = true)
|
||||
{
|
||||
if ($php = getenv('PHP_BINARY')) {
|
||||
if (!is_executable($php)) {
|
||||
$command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --';
|
||||
if ($php = strtok(exec($command.' '.escapeshellarg($php)), \PHP_EOL)) {
|
||||
if (!is_executable($php)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (@is_dir($php)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $php;
|
||||
}
|
||||
|
||||
$args = $this->findArguments();
|
||||
$args = $includeArgs && $args ? ' '.implode(' ', $args) : '';
|
||||
|
||||
// PHP_BINARY return the current sapi executable
|
||||
if (\PHP_BINARY && \in_array(\PHP_SAPI, ['cli', 'cli-server', 'phpdbg'], true)) {
|
||||
return \PHP_BINARY.$args;
|
||||
}
|
||||
|
||||
if ($php = getenv('PHP_PATH')) {
|
||||
if (!@is_executable($php) || @is_dir($php)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $php;
|
||||
}
|
||||
|
||||
if ($php = getenv('PHP_PEAR_PHP_BIN')) {
|
||||
if (@is_executable($php) && !@is_dir($php)) {
|
||||
return $php;
|
||||
}
|
||||
}
|
||||
|
||||
if (@is_executable($php = \PHP_BINDIR.('\\' === \DIRECTORY_SEPARATOR ? '\\php.exe' : '/php')) && !@is_dir($php)) {
|
||||
return $php;
|
||||
}
|
||||
|
||||
$dirs = [\PHP_BINDIR];
|
||||
if ('\\' === \DIRECTORY_SEPARATOR) {
|
||||
$dirs[] = 'C:\xampp\php\\';
|
||||
}
|
||||
|
||||
return $this->executableFinder->find('php', false, $dirs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the PHP executable arguments.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function findArguments()
|
||||
{
|
||||
$arguments = [];
|
||||
if ('phpdbg' === \PHP_SAPI) {
|
||||
$arguments[] = '-qrr';
|
||||
}
|
||||
|
||||
return $arguments;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?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\Process;
|
||||
|
||||
use Symfony\Component\Process\Exception\LogicException;
|
||||
use Symfony\Component\Process\Exception\RuntimeException;
|
||||
|
||||
/**
|
||||
* PhpProcess runs a PHP script in an independent process.
|
||||
*
|
||||
* $p = new PhpProcess('<?php echo "foo"; ?>');
|
||||
* $p->run();
|
||||
* print $p->getOutput()."\n";
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class PhpProcess extends Process
|
||||
{
|
||||
/**
|
||||
* @param string $script The PHP script to run (as a string)
|
||||
* @param string|null $cwd The working directory or null to use the working dir of the current PHP process
|
||||
* @param array|null $env The environment variables or null to use the same environment as the current PHP process
|
||||
* @param int $timeout The timeout in seconds
|
||||
* @param array|null $php Path to the PHP binary to use with any additional arguments
|
||||
*/
|
||||
public function __construct(string $script, ?string $cwd = null, ?array $env = null, int $timeout = 60, ?array $php = null)
|
||||
{
|
||||
if (null === $php) {
|
||||
$executableFinder = new PhpExecutableFinder();
|
||||
$php = $executableFinder->find(false);
|
||||
$php = false === $php ? null : array_merge([$php], $executableFinder->findArguments());
|
||||
}
|
||||
if ('phpdbg' === \PHP_SAPI) {
|
||||
$file = tempnam(sys_get_temp_dir(), 'dbg');
|
||||
file_put_contents($file, $script);
|
||||
register_shutdown_function('unlink', $file);
|
||||
$php[] = $file;
|
||||
$script = null;
|
||||
}
|
||||
|
||||
parent::__construct($php, $cwd, $env, $script, $timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, $input = null, ?float $timeout = 60)
|
||||
{
|
||||
throw new LogicException(sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function start(?callable $callback = null, array $env = [])
|
||||
{
|
||||
if (null === $this->getCommandLine()) {
|
||||
throw new RuntimeException('Unable to find the PHP executable.');
|
||||
}
|
||||
|
||||
parent::start($callback, $env);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
<?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\Process\Pipes;
|
||||
|
||||
use Symfony\Component\Process\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @author Romain Neutron <imprec@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract class AbstractPipes implements PipesInterface
|
||||
{
|
||||
public $pipes = [];
|
||||
|
||||
private $inputBuffer = '';
|
||||
private $input;
|
||||
private $blocked = true;
|
||||
private $lastError;
|
||||
|
||||
/**
|
||||
* @param resource|string|int|float|bool|\Iterator|null $input
|
||||
*/
|
||||
public function __construct($input)
|
||||
{
|
||||
if (\is_resource($input) || $input instanceof \Iterator) {
|
||||
$this->input = $input;
|
||||
} elseif (\is_string($input)) {
|
||||
$this->inputBuffer = $input;
|
||||
} else {
|
||||
$this->inputBuffer = (string) $input;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
foreach ($this->pipes as $pipe) {
|
||||
if (\is_resource($pipe)) {
|
||||
fclose($pipe);
|
||||
}
|
||||
}
|
||||
$this->pipes = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a system call has been interrupted.
|
||||
*/
|
||||
protected function hasSystemCallBeenInterrupted(): bool
|
||||
{
|
||||
$lastError = $this->lastError;
|
||||
$this->lastError = null;
|
||||
|
||||
// stream_select returns false when the `select` system call is interrupted by an incoming signal
|
||||
return null !== $lastError && false !== stripos($lastError, 'interrupted system call');
|
||||
}
|
||||
|
||||
/**
|
||||
* Unblocks streams.
|
||||
*/
|
||||
protected function unblock()
|
||||
{
|
||||
if (!$this->blocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->pipes as $pipe) {
|
||||
stream_set_blocking($pipe, 0);
|
||||
}
|
||||
if (\is_resource($this->input)) {
|
||||
stream_set_blocking($this->input, 0);
|
||||
}
|
||||
|
||||
$this->blocked = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes input to stdin.
|
||||
*
|
||||
* @throws InvalidArgumentException When an input iterator yields a non supported value
|
||||
*/
|
||||
protected function write(): ?array
|
||||
{
|
||||
if (!isset($this->pipes[0])) {
|
||||
return null;
|
||||
}
|
||||
$input = $this->input;
|
||||
|
||||
if ($input instanceof \Iterator) {
|
||||
if (!$input->valid()) {
|
||||
$input = null;
|
||||
} elseif (\is_resource($input = $input->current())) {
|
||||
stream_set_blocking($input, 0);
|
||||
} elseif (!isset($this->inputBuffer[0])) {
|
||||
if (!\is_string($input)) {
|
||||
if (!\is_scalar($input)) {
|
||||
throw new InvalidArgumentException(sprintf('"%s" yielded a value of type "%s", but only scalars and stream resources are supported.', get_debug_type($this->input), get_debug_type($input)));
|
||||
}
|
||||
$input = (string) $input;
|
||||
}
|
||||
$this->inputBuffer = $input;
|
||||
$this->input->next();
|
||||
$input = null;
|
||||
} else {
|
||||
$input = null;
|
||||
}
|
||||
}
|
||||
|
||||
$r = $e = [];
|
||||
$w = [$this->pipes[0]];
|
||||
|
||||
// let's have a look if something changed in streams
|
||||
if (false === @stream_select($r, $w, $e, 0, 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($w as $stdin) {
|
||||
if (isset($this->inputBuffer[0])) {
|
||||
$written = fwrite($stdin, $this->inputBuffer);
|
||||
$this->inputBuffer = substr($this->inputBuffer, $written);
|
||||
if (isset($this->inputBuffer[0])) {
|
||||
return [$this->pipes[0]];
|
||||
}
|
||||
}
|
||||
|
||||
if ($input) {
|
||||
while (true) {
|
||||
$data = fread($input, self::CHUNK_SIZE);
|
||||
if (!isset($data[0])) {
|
||||
break;
|
||||
}
|
||||
$written = fwrite($stdin, $data);
|
||||
$data = substr($data, $written);
|
||||
if (isset($data[0])) {
|
||||
$this->inputBuffer = $data;
|
||||
|
||||
return [$this->pipes[0]];
|
||||
}
|
||||
}
|
||||
if (feof($input)) {
|
||||
if ($this->input instanceof \Iterator) {
|
||||
$this->input->next();
|
||||
} else {
|
||||
$this->input = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// no input to read on resource, buffer is empty
|
||||
if (!isset($this->inputBuffer[0]) && !($this->input instanceof \Iterator ? $this->input->valid() : $this->input)) {
|
||||
$this->input = null;
|
||||
fclose($this->pipes[0]);
|
||||
unset($this->pipes[0]);
|
||||
} elseif (!$w) {
|
||||
return [$this->pipes[0]];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function handleError(int $type, string $msg)
|
||||
{
|
||||
$this->lastError = $msg;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?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\Process\Pipes;
|
||||
|
||||
/**
|
||||
* PipesInterface manages descriptors and pipes for the use of proc_open.
|
||||
*
|
||||
* @author Romain Neutron <imprec@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
interface PipesInterface
|
||||
{
|
||||
public const CHUNK_SIZE = 16384;
|
||||
|
||||
/**
|
||||
* Returns an array of descriptors for the use of proc_open.
|
||||
*/
|
||||
public function getDescriptors(): array;
|
||||
|
||||
/**
|
||||
* Returns an array of filenames indexed by their related stream in case these pipes use temporary files.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getFiles(): array;
|
||||
|
||||
/**
|
||||
* Reads data in file handles and pipes.
|
||||
*
|
||||
* @param bool $blocking Whether to use blocking calls or not
|
||||
* @param bool $close Whether to close pipes if they've reached EOF
|
||||
*
|
||||
* @return string[] An array of read data indexed by their fd
|
||||
*/
|
||||
public function readAndWrite(bool $blocking, bool $close = false): array;
|
||||
|
||||
/**
|
||||
* Returns if the current state has open file handles or pipes.
|
||||
*/
|
||||
public function areOpen(): bool;
|
||||
|
||||
/**
|
||||
* Returns if pipes are able to read output.
|
||||
*/
|
||||
public function haveReadSupport(): bool;
|
||||
|
||||
/**
|
||||
* Closes file handles and pipes.
|
||||
*/
|
||||
public function close();
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<?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\Process\Pipes;
|
||||
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
/**
|
||||
* UnixPipes implementation uses unix pipes as handles.
|
||||
*
|
||||
* @author Romain Neutron <imprec@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class UnixPipes extends AbstractPipes
|
||||
{
|
||||
private $ttyMode;
|
||||
private $ptyMode;
|
||||
private $haveReadSupport;
|
||||
|
||||
public function __construct(?bool $ttyMode, bool $ptyMode, $input, bool $haveReadSupport)
|
||||
{
|
||||
$this->ttyMode = $ttyMode;
|
||||
$this->ptyMode = $ptyMode;
|
||||
$this->haveReadSupport = $haveReadSupport;
|
||||
|
||||
parent::__construct($input);
|
||||
}
|
||||
|
||||
public function __sleep(): array
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
|
||||
}
|
||||
|
||||
public function __wakeup()
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDescriptors(): array
|
||||
{
|
||||
if (!$this->haveReadSupport) {
|
||||
$nullstream = fopen('/dev/null', 'c');
|
||||
|
||||
return [
|
||||
['pipe', 'r'],
|
||||
$nullstream,
|
||||
$nullstream,
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->ttyMode) {
|
||||
return [
|
||||
['file', '/dev/tty', 'r'],
|
||||
['file', '/dev/tty', 'w'],
|
||||
['file', '/dev/tty', 'w'],
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->ptyMode && Process::isPtySupported()) {
|
||||
return [
|
||||
['pty'],
|
||||
['pty'],
|
||||
['pty'],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
['pipe', 'r'],
|
||||
['pipe', 'w'], // stdout
|
||||
['pipe', 'w'], // stderr
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFiles(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function readAndWrite(bool $blocking, bool $close = false): array
|
||||
{
|
||||
$this->unblock();
|
||||
$w = $this->write();
|
||||
|
||||
$read = $e = [];
|
||||
$r = $this->pipes;
|
||||
unset($r[0]);
|
||||
|
||||
// let's have a look if something changed in streams
|
||||
set_error_handler([$this, 'handleError']);
|
||||
if (($r || $w) && false === stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) {
|
||||
restore_error_handler();
|
||||
// if a system call has been interrupted, forget about it, let's try again
|
||||
// otherwise, an error occurred, let's reset pipes
|
||||
if (!$this->hasSystemCallBeenInterrupted()) {
|
||||
$this->pipes = [];
|
||||
}
|
||||
|
||||
return $read;
|
||||
}
|
||||
restore_error_handler();
|
||||
|
||||
foreach ($r as $pipe) {
|
||||
// prior PHP 5.4 the array passed to stream_select is modified and
|
||||
// lose key association, we have to find back the key
|
||||
$read[$type = array_search($pipe, $this->pipes, true)] = '';
|
||||
|
||||
do {
|
||||
$data = @fread($pipe, self::CHUNK_SIZE);
|
||||
$read[$type] .= $data;
|
||||
} while (isset($data[0]) && ($close || isset($data[self::CHUNK_SIZE - 1])));
|
||||
|
||||
if (!isset($read[$type][0])) {
|
||||
unset($read[$type]);
|
||||
}
|
||||
|
||||
if ($close && feof($pipe)) {
|
||||
fclose($pipe);
|
||||
unset($this->pipes[$type]);
|
||||
}
|
||||
}
|
||||
|
||||
return $read;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function haveReadSupport(): bool
|
||||
{
|
||||
return $this->haveReadSupport;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function areOpen(): bool
|
||||
{
|
||||
return (bool) $this->pipes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
<?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\Process\Pipes;
|
||||
|
||||
use Symfony\Component\Process\Exception\RuntimeException;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
/**
|
||||
* WindowsPipes implementation uses temporary files as handles.
|
||||
*
|
||||
* @see https://bugs.php.net/51800
|
||||
* @see https://bugs.php.net/65650
|
||||
*
|
||||
* @author Romain Neutron <imprec@gmail.com>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class WindowsPipes extends AbstractPipes
|
||||
{
|
||||
private $files = [];
|
||||
private $fileHandles = [];
|
||||
private $lockHandles = [];
|
||||
private $readBytes = [
|
||||
Process::STDOUT => 0,
|
||||
Process::STDERR => 0,
|
||||
];
|
||||
private $haveReadSupport;
|
||||
|
||||
public function __construct($input, bool $haveReadSupport)
|
||||
{
|
||||
$this->haveReadSupport = $haveReadSupport;
|
||||
|
||||
if ($this->haveReadSupport) {
|
||||
// Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big.
|
||||
// Workaround for this problem is to use temporary files instead of pipes on Windows platform.
|
||||
//
|
||||
// @see https://bugs.php.net/51800
|
||||
$pipes = [
|
||||
Process::STDOUT => Process::OUT,
|
||||
Process::STDERR => Process::ERR,
|
||||
];
|
||||
$tmpDir = sys_get_temp_dir();
|
||||
$lastError = 'unknown reason';
|
||||
set_error_handler(function ($type, $msg) use (&$lastError) { $lastError = $msg; });
|
||||
for ($i = 0;; ++$i) {
|
||||
foreach ($pipes as $pipe => $name) {
|
||||
$file = sprintf('%s\\sf_proc_%02X.%s', $tmpDir, $i, $name);
|
||||
|
||||
if (!$h = fopen($file.'.lock', 'w')) {
|
||||
if (file_exists($file.'.lock')) {
|
||||
continue 2;
|
||||
}
|
||||
restore_error_handler();
|
||||
throw new RuntimeException('A temporary file could not be opened to write the process output: '.$lastError);
|
||||
}
|
||||
if (!flock($h, \LOCK_EX | \LOCK_NB)) {
|
||||
continue 2;
|
||||
}
|
||||
if (isset($this->lockHandles[$pipe])) {
|
||||
flock($this->lockHandles[$pipe], \LOCK_UN);
|
||||
fclose($this->lockHandles[$pipe]);
|
||||
}
|
||||
$this->lockHandles[$pipe] = $h;
|
||||
|
||||
if (!($h = fopen($file, 'w')) || !fclose($h) || !$h = fopen($file, 'r')) {
|
||||
flock($this->lockHandles[$pipe], \LOCK_UN);
|
||||
fclose($this->lockHandles[$pipe]);
|
||||
unset($this->lockHandles[$pipe]);
|
||||
continue 2;
|
||||
}
|
||||
$this->fileHandles[$pipe] = $h;
|
||||
$this->files[$pipe] = $file;
|
||||
}
|
||||
break;
|
||||
}
|
||||
restore_error_handler();
|
||||
}
|
||||
|
||||
parent::__construct($input);
|
||||
}
|
||||
|
||||
public function __sleep(): array
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
|
||||
}
|
||||
|
||||
public function __wakeup()
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDescriptors(): array
|
||||
{
|
||||
if (!$this->haveReadSupport) {
|
||||
$nullstream = fopen('NUL', 'c');
|
||||
|
||||
return [
|
||||
['pipe', 'r'],
|
||||
$nullstream,
|
||||
$nullstream,
|
||||
];
|
||||
}
|
||||
|
||||
// We're not using pipe on Windows platform as it hangs (https://bugs.php.net/51800)
|
||||
// We're not using file handles as it can produce corrupted output https://bugs.php.net/65650
|
||||
// So we redirect output within the commandline and pass the nul device to the process
|
||||
return [
|
||||
['pipe', 'r'],
|
||||
['file', 'NUL', 'w'],
|
||||
['file', 'NUL', 'w'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFiles(): array
|
||||
{
|
||||
return $this->files;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function readAndWrite(bool $blocking, bool $close = false): array
|
||||
{
|
||||
$this->unblock();
|
||||
$w = $this->write();
|
||||
$read = $r = $e = [];
|
||||
|
||||
if ($blocking) {
|
||||
if ($w) {
|
||||
@stream_select($r, $w, $e, 0, Process::TIMEOUT_PRECISION * 1E6);
|
||||
} elseif ($this->fileHandles) {
|
||||
usleep((int) (Process::TIMEOUT_PRECISION * 1E6));
|
||||
}
|
||||
}
|
||||
foreach ($this->fileHandles as $type => $fileHandle) {
|
||||
$data = stream_get_contents($fileHandle, -1, $this->readBytes[$type]);
|
||||
|
||||
if (isset($data[0])) {
|
||||
$this->readBytes[$type] += \strlen($data);
|
||||
$read[$type] = $data;
|
||||
}
|
||||
if ($close) {
|
||||
ftruncate($fileHandle, 0);
|
||||
fclose($fileHandle);
|
||||
flock($this->lockHandles[$type], \LOCK_UN);
|
||||
fclose($this->lockHandles[$type]);
|
||||
unset($this->fileHandles[$type], $this->lockHandles[$type]);
|
||||
}
|
||||
}
|
||||
|
||||
return $read;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function haveReadSupport(): bool
|
||||
{
|
||||
return $this->haveReadSupport;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function areOpen(): bool
|
||||
{
|
||||
return $this->pipes && $this->fileHandles;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
parent::close();
|
||||
foreach ($this->fileHandles as $type => $handle) {
|
||||
ftruncate($handle, 0);
|
||||
fclose($handle);
|
||||
flock($this->lockHandles[$type], \LOCK_UN);
|
||||
fclose($this->lockHandles[$type]);
|
||||
}
|
||||
$this->fileHandles = $this->lockHandles = [];
|
||||
}
|
||||
}
|
||||
+1666
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,69 @@
|
||||
<?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\Process;
|
||||
|
||||
use Symfony\Component\Process\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* ProcessUtils is a bunch of utility methods.
|
||||
*
|
||||
* This class contains static methods only and is not meant to be instantiated.
|
||||
*
|
||||
* @author Martin Hasoň <martin.hason@gmail.com>
|
||||
*/
|
||||
class ProcessUtils
|
||||
{
|
||||
/**
|
||||
* This class should not be instantiated.
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and normalizes a Process input.
|
||||
*
|
||||
* @param string $caller The name of method call that validates the input
|
||||
* @param mixed $input The input to validate
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws InvalidArgumentException In case the input is not valid
|
||||
*/
|
||||
public static function validateInput(string $caller, $input)
|
||||
{
|
||||
if (null !== $input) {
|
||||
if (\is_resource($input)) {
|
||||
return $input;
|
||||
}
|
||||
if (\is_string($input)) {
|
||||
return $input;
|
||||
}
|
||||
if (\is_scalar($input)) {
|
||||
return (string) $input;
|
||||
}
|
||||
if ($input instanceof Process) {
|
||||
return $input->getIterator($input::ITER_SKIP_ERR);
|
||||
}
|
||||
if ($input instanceof \Iterator) {
|
||||
return $input;
|
||||
}
|
||||
if ($input instanceof \Traversable) {
|
||||
return new \IteratorIterator($input);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf('"%s" only accepts strings, Traversable objects or stream resources.', $caller));
|
||||
}
|
||||
|
||||
return $input;
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
Process Component
|
||||
=================
|
||||
|
||||
The Process component executes commands in sub-processes.
|
||||
|
||||
Sponsor
|
||||
-------
|
||||
|
||||
The Process component for Symfony 5.4/6.0 is [backed][1] by [SensioLabs][2].
|
||||
|
||||
As the creator of Symfony, SensioLabs supports companies using Symfony, with an
|
||||
offering encompassing consultancy, expertise, services, training, and technical
|
||||
assistance to ensure the success of web application development projects.
|
||||
|
||||
Help Symfony by [sponsoring][3] its development!
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
* [Documentation](https://symfony.com/doc/current/components/process.html)
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [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
|
||||
[2]: https://sensiolabs.com
|
||||
[3]: https://symfony.com/sponsor
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"type": "library",
|
||||
"description": "Executes commands in sub-processes",
|
||||
"keywords": [],
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"symfony/polyfill-php80": "^1.16"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Component\\Process\\": "" },
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
vendor/
|
||||
composer.lock
|
||||
phpunit.xml
|
||||
@@ -0,0 +1,5 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
The changelog is maintained for all Symfony contracts at the following URL:
|
||||
https://github.com/symfony/contracts/blob/main/CHANGELOG.md
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2018-present Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,31 @@
|
||||
<?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\Contracts\Translation;
|
||||
|
||||
interface LocaleAwareInterface
|
||||
{
|
||||
/**
|
||||
* Sets the current locale.
|
||||
*
|
||||
* @param string $locale The locale
|
||||
*
|
||||
* @throws \InvalidArgumentException If the locale contains invalid characters
|
||||
*/
|
||||
public function setLocale(string $locale);
|
||||
|
||||
/**
|
||||
* Returns the current locale.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocale();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
Symfony Translation Contracts
|
||||
=============================
|
||||
|
||||
A set of abstractions extracted out of the Symfony components.
|
||||
|
||||
Can be used to build on semantics that the Symfony components proved useful - and
|
||||
that already have battle tested implementations.
|
||||
|
||||
See https://github.com/symfony/contracts/blob/main/README.md for more information.
|
||||
@@ -0,0 +1,390 @@
|
||||
<?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\Contracts\Translation\Test;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorTrait;
|
||||
|
||||
/**
|
||||
* Test should cover all languages mentioned on http://translate.sourceforge.net/wiki/l10n/pluralforms
|
||||
* and Plural forms mentioned on http://www.gnu.org/software/gettext/manual/gettext.html#Plural-forms.
|
||||
*
|
||||
* See also https://developer.mozilla.org/en/Localization_and_Plurals which mentions 15 rules having a maximum of 6 forms.
|
||||
* The mozilla code is also interesting to check for.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @author Clemens Tolboom clemens@build2be.nl
|
||||
*/
|
||||
class TranslatorTest extends TestCase
|
||||
{
|
||||
private $defaultLocale;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->defaultLocale = \Locale::getDefault();
|
||||
\Locale::setDefault('en');
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
\Locale::setDefault($this->defaultLocale);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TranslatorInterface
|
||||
*/
|
||||
public function getTranslator()
|
||||
{
|
||||
return new class() implements TranslatorInterface {
|
||||
use TranslatorTrait;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getTransTests
|
||||
*/
|
||||
public function testTrans($expected, $id, $parameters)
|
||||
{
|
||||
$translator = $this->getTranslator();
|
||||
|
||||
$this->assertEquals($expected, $translator->trans($id, $parameters));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getTransChoiceTests
|
||||
*/
|
||||
public function testTransChoiceWithExplicitLocale($expected, $id, $number)
|
||||
{
|
||||
$translator = $this->getTranslator();
|
||||
|
||||
$this->assertEquals($expected, $translator->trans($id, ['%count%' => $number]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires extension intl
|
||||
*
|
||||
* @dataProvider getTransChoiceTests
|
||||
*/
|
||||
public function testTransChoiceWithDefaultLocale($expected, $id, $number)
|
||||
{
|
||||
$translator = $this->getTranslator();
|
||||
|
||||
$this->assertEquals($expected, $translator->trans($id, ['%count%' => $number]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getTransChoiceTests
|
||||
*/
|
||||
public function testTransChoiceWithEnUsPosix($expected, $id, $number)
|
||||
{
|
||||
$translator = $this->getTranslator();
|
||||
$translator->setLocale('en_US_POSIX');
|
||||
|
||||
$this->assertEquals($expected, $translator->trans($id, ['%count%' => $number]));
|
||||
}
|
||||
|
||||
public function testGetSetLocale()
|
||||
{
|
||||
$translator = $this->getTranslator();
|
||||
|
||||
$this->assertEquals('en', $translator->getLocale());
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires extension intl
|
||||
*/
|
||||
public function testGetLocaleReturnsDefaultLocaleIfNotSet()
|
||||
{
|
||||
$translator = $this->getTranslator();
|
||||
|
||||
\Locale::setDefault('pt_BR');
|
||||
$this->assertEquals('pt_BR', $translator->getLocale());
|
||||
|
||||
\Locale::setDefault('en');
|
||||
$this->assertEquals('en', $translator->getLocale());
|
||||
}
|
||||
|
||||
public static function getTransTests()
|
||||
{
|
||||
return [
|
||||
['Symfony is great!', 'Symfony is great!', []],
|
||||
['Symfony is awesome!', 'Symfony is %what%!', ['%what%' => 'awesome']],
|
||||
];
|
||||
}
|
||||
|
||||
public static function getTransChoiceTests()
|
||||
{
|
||||
return [
|
||||
['There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0],
|
||||
['There is one apple', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 1],
|
||||
['There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10],
|
||||
['There are 0 apples', 'There is 1 apple|There are %count% apples', 0],
|
||||
['There is 1 apple', 'There is 1 apple|There are %count% apples', 1],
|
||||
['There are 10 apples', 'There is 1 apple|There are %count% apples', 10],
|
||||
// custom validation messages may be coded with a fixed value
|
||||
['There are 2 apples', 'There are 2 apples', 2],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getInterval
|
||||
*/
|
||||
public function testInterval($expected, $number, $interval)
|
||||
{
|
||||
$translator = $this->getTranslator();
|
||||
|
||||
$this->assertEquals($expected, $translator->trans($interval.' foo|[1,Inf[ bar', ['%count%' => $number]));
|
||||
}
|
||||
|
||||
public static function getInterval()
|
||||
{
|
||||
return [
|
||||
['foo', 3, '{1,2, 3 ,4}'],
|
||||
['bar', 10, '{1,2, 3 ,4}'],
|
||||
['bar', 3, '[1,2]'],
|
||||
['foo', 1, '[1,2]'],
|
||||
['foo', 2, '[1,2]'],
|
||||
['bar', 1, ']1,2['],
|
||||
['bar', 2, ']1,2['],
|
||||
['foo', log(0), '[-Inf,2['],
|
||||
['foo', -log(0), '[-2,+Inf]'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getChooseTests
|
||||
*/
|
||||
public function testChoose($expected, $id, $number, $locale = null)
|
||||
{
|
||||
$translator = $this->getTranslator();
|
||||
|
||||
$this->assertEquals($expected, $translator->trans($id, ['%count%' => $number], null, $locale));
|
||||
}
|
||||
|
||||
public function testReturnMessageIfExactlyOneStandardRuleIsGiven()
|
||||
{
|
||||
$translator = $this->getTranslator();
|
||||
|
||||
$this->assertEquals('There are two apples', $translator->trans('There are two apples', ['%count%' => 2]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getNonMatchingMessages
|
||||
*/
|
||||
public function testThrowExceptionIfMatchingMessageCannotBeFound($id, $number)
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$translator = $this->getTranslator();
|
||||
|
||||
$translator->trans($id, ['%count%' => $number]);
|
||||
}
|
||||
|
||||
public static function getNonMatchingMessages()
|
||||
{
|
||||
return [
|
||||
['{0} There are no apples|{1} There is one apple', 2],
|
||||
['{1} There is one apple|]1,Inf] There are %count% apples', 0],
|
||||
['{1} There is one apple|]2,Inf] There are %count% apples', 2],
|
||||
['{0} There are no apples|There is one apple', 2],
|
||||
];
|
||||
}
|
||||
|
||||
public static function getChooseTests()
|
||||
{
|
||||
return [
|
||||
['There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0],
|
||||
['There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0],
|
||||
['There are no apples', '{0}There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0],
|
||||
|
||||
['There is one apple', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 1],
|
||||
|
||||
['There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10],
|
||||
['There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf]There are %count% apples', 10],
|
||||
['There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10],
|
||||
|
||||
['There are 0 apples', 'There is one apple|There are %count% apples', 0],
|
||||
['There is one apple', 'There is one apple|There are %count% apples', 1],
|
||||
['There are 10 apples', 'There is one apple|There are %count% apples', 10],
|
||||
|
||||
['There are 0 apples', 'one: There is one apple|more: There are %count% apples', 0],
|
||||
['There is one apple', 'one: There is one apple|more: There are %count% apples', 1],
|
||||
['There are 10 apples', 'one: There is one apple|more: There are %count% apples', 10],
|
||||
|
||||
['There are no apples', '{0} There are no apples|one: There is one apple|more: There are %count% apples', 0],
|
||||
['There is one apple', '{0} There are no apples|one: There is one apple|more: There are %count% apples', 1],
|
||||
['There are 10 apples', '{0} There are no apples|one: There is one apple|more: There are %count% apples', 10],
|
||||
|
||||
['', '{0}|{1} There is one apple|]1,Inf] There are %count% apples', 0],
|
||||
['', '{0} There are no apples|{1}|]1,Inf] There are %count% apples', 1],
|
||||
|
||||
// Indexed only tests which are Gettext PoFile* compatible strings.
|
||||
['There are 0 apples', 'There is one apple|There are %count% apples', 0],
|
||||
['There is one apple', 'There is one apple|There are %count% apples', 1],
|
||||
['There are 2 apples', 'There is one apple|There are %count% apples', 2],
|
||||
|
||||
// Tests for float numbers
|
||||
['There is almost one apple', '{0} There are no apples|]0,1[ There is almost one apple|{1} There is one apple|[1,Inf] There is more than one apple', 0.7],
|
||||
['There is one apple', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1],
|
||||
['There is more than one apple', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1.7],
|
||||
['There are no apples', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0],
|
||||
['There are no apples', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0.0],
|
||||
['There are no apples', '{0.0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0],
|
||||
|
||||
// Test texts with new-lines
|
||||
// with double-quotes and \n in id & double-quotes and actual newlines in text
|
||||
["This is a text with a\n new-line in it. Selector = 0.", '{0}This is a text with a
|
||||
new-line in it. Selector = 0.|{1}This is a text with a
|
||||
new-line in it. Selector = 1.|[1,Inf]This is a text with a
|
||||
new-line in it. Selector > 1.', 0],
|
||||
// with double-quotes and \n in id and single-quotes and actual newlines in text
|
||||
["This is a text with a\n new-line in it. Selector = 1.", '{0}This is a text with a
|
||||
new-line in it. Selector = 0.|{1}This is a text with a
|
||||
new-line in it. Selector = 1.|[1,Inf]This is a text with a
|
||||
new-line in it. Selector > 1.', 1],
|
||||
["This is a text with a\n new-line in it. Selector > 1.", '{0}This is a text with a
|
||||
new-line in it. Selector = 0.|{1}This is a text with a
|
||||
new-line in it. Selector = 1.|[1,Inf]This is a text with a
|
||||
new-line in it. Selector > 1.', 5],
|
||||
// with double-quotes and id split across lines
|
||||
['This is a text with a
|
||||
new-line in it. Selector = 1.', '{0}This is a text with a
|
||||
new-line in it. Selector = 0.|{1}This is a text with a
|
||||
new-line in it. Selector = 1.|[1,Inf]This is a text with a
|
||||
new-line in it. Selector > 1.', 1],
|
||||
// with single-quotes and id split across lines
|
||||
['This is a text with a
|
||||
new-line in it. Selector > 1.', '{0}This is a text with a
|
||||
new-line in it. Selector = 0.|{1}This is a text with a
|
||||
new-line in it. Selector = 1.|[1,Inf]This is a text with a
|
||||
new-line in it. Selector > 1.', 5],
|
||||
// with single-quotes and \n in text
|
||||
['This is a text with a\nnew-line in it. Selector = 0.', '{0}This is a text with a\nnew-line in it. Selector = 0.|{1}This is a text with a\nnew-line in it. Selector = 1.|[1,Inf]This is a text with a\nnew-line in it. Selector > 1.', 0],
|
||||
// with double-quotes and id split across lines
|
||||
["This is a text with a\nnew-line in it. Selector = 1.", "{0}This is a text with a\nnew-line in it. Selector = 0.|{1}This is a text with a\nnew-line in it. Selector = 1.|[1,Inf]This is a text with a\nnew-line in it. Selector > 1.", 1],
|
||||
// escape pipe
|
||||
['This is a text with | in it. Selector = 0.', '{0}This is a text with || in it. Selector = 0.|{1}This is a text with || in it. Selector = 1.', 0],
|
||||
// Empty plural set (2 plural forms) from a .PO file
|
||||
['', '|', 1],
|
||||
// Empty plural set (3 plural forms) from a .PO file
|
||||
['', '||', 1],
|
||||
|
||||
// Floating values
|
||||
['1.5 liters', '%count% liter|%count% liters', 1.5],
|
||||
['1.5 litre', '%count% litre|%count% litres', 1.5, 'fr'],
|
||||
|
||||
// Negative values
|
||||
['-1 degree', '%count% degree|%count% degrees', -1],
|
||||
['-1 degré', '%count% degré|%count% degrés', -1],
|
||||
['-1.5 degrees', '%count% degree|%count% degrees', -1.5],
|
||||
['-1.5 degré', '%count% degré|%count% degrés', -1.5, 'fr'],
|
||||
['-2 degrees', '%count% degree|%count% degrees', -2],
|
||||
['-2 degrés', '%count% degré|%count% degrés', -2],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider failingLangcodes
|
||||
*/
|
||||
public function testFailedLangcodes($nplural, $langCodes)
|
||||
{
|
||||
$matrix = $this->generateTestData($langCodes);
|
||||
$this->validateMatrix($nplural, $matrix, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider successLangcodes
|
||||
*/
|
||||
public function testLangcodes($nplural, $langCodes)
|
||||
{
|
||||
$matrix = $this->generateTestData($langCodes);
|
||||
$this->validateMatrix($nplural, $matrix);
|
||||
}
|
||||
|
||||
/**
|
||||
* This array should contain all currently known langcodes.
|
||||
*
|
||||
* As it is impossible to have this ever complete we should try as hard as possible to have it almost complete.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function successLangcodes()
|
||||
{
|
||||
return [
|
||||
['1', ['ay', 'bo', 'cgg', 'dz', 'id', 'ja', 'jbo', 'ka', 'kk', 'km', 'ko', 'ky']],
|
||||
['2', ['nl', 'fr', 'en', 'de', 'de_GE', 'hy', 'hy_AM', 'en_US_POSIX']],
|
||||
['3', ['be', 'bs', 'cs', 'hr']],
|
||||
['4', ['cy', 'mt', 'sl']],
|
||||
['6', ['ar']],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* This array should be at least empty within the near future.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
public static function failingLangcodes()
|
||||
{
|
||||
return [
|
||||
['1', ['fa']],
|
||||
['2', ['jbo']],
|
||||
['3', ['cbs']],
|
||||
['4', ['gd', 'kw']],
|
||||
['5', ['ga']],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* We validate only on the plural coverage. Thus the real rules is not tested.
|
||||
*
|
||||
* @param string $nplural Plural expected
|
||||
* @param array $matrix Containing langcodes and their plural index values
|
||||
* @param bool $expectSuccess
|
||||
*/
|
||||
protected function validateMatrix($nplural, $matrix, $expectSuccess = true)
|
||||
{
|
||||
foreach ($matrix as $langCode => $data) {
|
||||
$indexes = array_flip($data);
|
||||
if ($expectSuccess) {
|
||||
$this->assertCount($nplural, $indexes, "Langcode '$langCode' has '$nplural' plural forms.");
|
||||
} else {
|
||||
$this->assertNotEquals((int) $nplural, \count($indexes), "Langcode '$langCode' has '$nplural' plural forms.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function generateTestData($langCodes)
|
||||
{
|
||||
$translator = new class() {
|
||||
use TranslatorTrait {
|
||||
getPluralizationRule as public;
|
||||
}
|
||||
};
|
||||
|
||||
$matrix = [];
|
||||
foreach ($langCodes as $langCode) {
|
||||
for ($count = 0; $count < 200; ++$count) {
|
||||
$plural = $translator->getPluralizationRule($count, $langCode);
|
||||
$matrix[$langCode][$count] = $plural;
|
||||
}
|
||||
}
|
||||
|
||||
return $matrix;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
namespace Symfony\Contracts\Translation;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
interface TranslatableInterface
|
||||
{
|
||||
public function trans(TranslatorInterface $translator, ?string $locale = null): string;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?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\Contracts\Translation;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* @method string getLocale() Returns the default locale
|
||||
*/
|
||||
interface TranslatorInterface
|
||||
{
|
||||
/**
|
||||
* Translates the given message.
|
||||
*
|
||||
* When a number is provided as a parameter named "%count%", the message is parsed for plural
|
||||
* forms and a translation is chosen according to this number using the following rules:
|
||||
*
|
||||
* Given a message with different plural translations separated by a
|
||||
* pipe (|), this method returns the correct portion of the message based
|
||||
* on the given number, locale and the pluralization rules in the message
|
||||
* itself.
|
||||
*
|
||||
* The message supports two different types of pluralization rules:
|
||||
*
|
||||
* interval: {0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples
|
||||
* indexed: There is one apple|There are %count% apples
|
||||
*
|
||||
* The indexed solution can also contain labels (e.g. one: There is one apple).
|
||||
* This is purely for making the translations more clear - it does not
|
||||
* affect the functionality.
|
||||
*
|
||||
* The two methods can also be mixed:
|
||||
* {0} There are no apples|one: There is one apple|more: There are %count% apples
|
||||
*
|
||||
* An interval can represent a finite set of numbers:
|
||||
* {1,2,3,4}
|
||||
*
|
||||
* An interval can represent numbers between two numbers:
|
||||
* [1, +Inf]
|
||||
* ]-1,2[
|
||||
*
|
||||
* The left delimiter can be [ (inclusive) or ] (exclusive).
|
||||
* The right delimiter can be [ (exclusive) or ] (inclusive).
|
||||
* Beside numbers, you can use -Inf and +Inf for the infinite.
|
||||
*
|
||||
* @see https://en.wikipedia.org/wiki/ISO_31-11
|
||||
*
|
||||
* @param string $id The message id (may also be an object that can be cast to string)
|
||||
* @param array $parameters An array of parameters for the message
|
||||
* @param string|null $domain The domain for the message or null to use the default
|
||||
* @param string|null $locale The locale or null to use the default
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws \InvalidArgumentException If the locale contains invalid characters
|
||||
*/
|
||||
public function trans(string $id, array $parameters = [], ?string $domain = null, ?string $locale = null);
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
<?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\Contracts\Translation;
|
||||
|
||||
use Symfony\Component\Translation\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* A trait to help implement TranslatorInterface and LocaleAwareInterface.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
trait TranslatorTrait
|
||||
{
|
||||
private $locale;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setLocale(string $locale)
|
||||
{
|
||||
$this->locale = $locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocale()
|
||||
{
|
||||
return $this->locale ?: (class_exists(\Locale::class) ? \Locale::getDefault() : 'en');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function trans(?string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string
|
||||
{
|
||||
if (null === $id || '' === $id) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!isset($parameters['%count%']) || !is_numeric($parameters['%count%'])) {
|
||||
return strtr($id, $parameters);
|
||||
}
|
||||
|
||||
$number = (float) $parameters['%count%'];
|
||||
$locale = $locale ?: $this->getLocale();
|
||||
|
||||
$parts = [];
|
||||
if (preg_match('/^\|++$/', $id)) {
|
||||
$parts = explode('|', $id);
|
||||
} elseif (preg_match_all('/(?:\|\||[^\|])++/', $id, $matches)) {
|
||||
$parts = $matches[0];
|
||||
}
|
||||
|
||||
$intervalRegexp = <<<'EOF'
|
||||
/^(?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;
|
||||
|
||||
$standardRules = [];
|
||||
foreach ($parts as $part) {
|
||||
$part = trim(str_replace('||', '|', $part));
|
||||
|
||||
// try to match an explicit rule, then fallback to the standard ones
|
||||
if (preg_match($intervalRegexp, $part, $matches)) {
|
||||
if ($matches[2]) {
|
||||
foreach (explode(',', $matches[3]) as $n) {
|
||||
if ($number == $n) {
|
||||
return strtr($matches['message'], $parameters);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$leftNumber = '-Inf' === $matches['left'] ? -\INF : (float) $matches['left'];
|
||||
$rightNumber = is_numeric($matches['right']) ? (float) $matches['right'] : \INF;
|
||||
|
||||
if (('[' === $matches['left_delimiter'] ? $number >= $leftNumber : $number > $leftNumber)
|
||||
&& (']' === $matches['right_delimiter'] ? $number <= $rightNumber : $number < $rightNumber)
|
||||
) {
|
||||
return strtr($matches['message'], $parameters);
|
||||
}
|
||||
}
|
||||
} elseif (preg_match('/^\w+\:\s*(.*?)$/', $part, $matches)) {
|
||||
$standardRules[] = $matches[1];
|
||||
} else {
|
||||
$standardRules[] = $part;
|
||||
}
|
||||
}
|
||||
|
||||
$position = $this->getPluralizationRule($number, $locale);
|
||||
|
||||
if (!isset($standardRules[$position])) {
|
||||
// when there's exactly one rule given, and that rule is a standard
|
||||
// rule, use this rule
|
||||
if (1 === \count($parts) && isset($standardRules[0])) {
|
||||
return strtr($standardRules[0], $parameters);
|
||||
}
|
||||
|
||||
$message = sprintf('Unable to choose a translation for "%s" with locale "%s" for value "%d". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %%count%% apples").', $id, $locale, $number);
|
||||
|
||||
if (class_exists(InvalidArgumentException::class)) {
|
||||
throw new InvalidArgumentException($message);
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException($message);
|
||||
}
|
||||
|
||||
return strtr($standardRules[$position], $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the plural position to use for the given locale and number.
|
||||
*
|
||||
* The plural rules are derived from code of the Zend Framework (2010-09-25),
|
||||
* which is subject to the new BSD license (http://framework.zend.com/license/new-bsd).
|
||||
* Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
|
||||
*/
|
||||
private function getPluralizationRule(float $number, string $locale): int
|
||||
{
|
||||
$number = abs($number);
|
||||
|
||||
switch ('pt_BR' !== $locale && 'en_US_POSIX' !== $locale && \strlen($locale) > 3 ? substr($locale, 0, strrpos($locale, '_')) : $locale) {
|
||||
case 'af':
|
||||
case 'bn':
|
||||
case 'bg':
|
||||
case 'ca':
|
||||
case 'da':
|
||||
case 'de':
|
||||
case 'el':
|
||||
case 'en':
|
||||
case 'en_US_POSIX':
|
||||
case 'eo':
|
||||
case 'es':
|
||||
case 'et':
|
||||
case 'eu':
|
||||
case 'fa':
|
||||
case 'fi':
|
||||
case 'fo':
|
||||
case 'fur':
|
||||
case 'fy':
|
||||
case 'gl':
|
||||
case 'gu':
|
||||
case 'ha':
|
||||
case 'he':
|
||||
case 'hu':
|
||||
case 'is':
|
||||
case 'it':
|
||||
case 'ku':
|
||||
case 'lb':
|
||||
case 'ml':
|
||||
case 'mn':
|
||||
case 'mr':
|
||||
case 'nah':
|
||||
case 'nb':
|
||||
case 'ne':
|
||||
case 'nl':
|
||||
case 'nn':
|
||||
case 'no':
|
||||
case 'oc':
|
||||
case 'om':
|
||||
case 'or':
|
||||
case 'pa':
|
||||
case 'pap':
|
||||
case 'ps':
|
||||
case 'pt':
|
||||
case 'so':
|
||||
case 'sq':
|
||||
case 'sv':
|
||||
case 'sw':
|
||||
case 'ta':
|
||||
case 'te':
|
||||
case 'tk':
|
||||
case 'ur':
|
||||
case 'zu':
|
||||
return (1 == $number) ? 0 : 1;
|
||||
|
||||
case 'am':
|
||||
case 'bh':
|
||||
case 'fil':
|
||||
case 'fr':
|
||||
case 'gun':
|
||||
case 'hi':
|
||||
case 'hy':
|
||||
case 'ln':
|
||||
case 'mg':
|
||||
case 'nso':
|
||||
case 'pt_BR':
|
||||
case 'ti':
|
||||
case 'wa':
|
||||
return ($number < 2) ? 0 : 1;
|
||||
|
||||
case 'be':
|
||||
case 'bs':
|
||||
case 'hr':
|
||||
case 'ru':
|
||||
case 'sh':
|
||||
case 'sr':
|
||||
case 'uk':
|
||||
return ((1 == $number % 10) && (11 != $number % 100)) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2);
|
||||
|
||||
case 'cs':
|
||||
case 'sk':
|
||||
return (1 == $number) ? 0 : ((($number >= 2) && ($number <= 4)) ? 1 : 2);
|
||||
|
||||
case 'ga':
|
||||
return (1 == $number) ? 0 : ((2 == $number) ? 1 : 2);
|
||||
|
||||
case 'lt':
|
||||
return ((1 == $number % 10) && (11 != $number % 100)) ? 0 : ((($number % 10 >= 2) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2);
|
||||
|
||||
case 'sl':
|
||||
return (1 == $number % 100) ? 0 : ((2 == $number % 100) ? 1 : (((3 == $number % 100) || (4 == $number % 100)) ? 2 : 3));
|
||||
|
||||
case 'mk':
|
||||
return (1 == $number % 10) ? 0 : 1;
|
||||
|
||||
case 'mt':
|
||||
return (1 == $number) ? 0 : (((0 == $number) || (($number % 100 > 1) && ($number % 100 < 11))) ? 1 : ((($number % 100 > 10) && ($number % 100 < 20)) ? 2 : 3));
|
||||
|
||||
case 'lv':
|
||||
return (0 == $number) ? 0 : (((1 == $number % 10) && (11 != $number % 100)) ? 1 : 2);
|
||||
|
||||
case 'pl':
|
||||
return (1 == $number) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 12) || ($number % 100 > 14))) ? 1 : 2);
|
||||
|
||||
case 'cy':
|
||||
return (1 == $number) ? 0 : ((2 == $number) ? 1 : (((8 == $number) || (11 == $number)) ? 2 : 3));
|
||||
|
||||
case 'ro':
|
||||
return (1 == $number) ? 0 : (((0 == $number) || (($number % 100 > 0) && ($number % 100 < 20))) ? 1 : 2);
|
||||
|
||||
case 'ar':
|
||||
return (0 == $number) ? 0 : ((1 == $number) ? 1 : ((2 == $number) ? 2 : ((($number % 100 >= 3) && ($number % 100 <= 10)) ? 3 : ((($number % 100 >= 11) && ($number % 100 <= 99)) ? 4 : 5))));
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "symfony/translation-contracts",
|
||||
"type": "library",
|
||||
"description": "Generic abstractions related to translation",
|
||||
"keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"],
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.2.5"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/translation-implementation": ""
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Contracts\\Translation\\": "" }
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "2.5-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/contracts",
|
||||
"url": "https://github.com/symfony/contracts"
|
||||
}
|
||||
}
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
5.4.21
|
||||
------
|
||||
|
||||
* [BC BREAK] The following data providers for `ProviderFactoryTestCase` are now static:
|
||||
`supportsProvider()`, `createProvider()`, `unsupportedSchemeProvider()`and `incompleteDsnProvider()`
|
||||
* [BC BREAK] `ProviderTestCase::toStringProvider()` is now static
|
||||
|
||||
5.4
|
||||
---
|
||||
|
||||
* Add `github` format & autodetection to render errors as annotations when
|
||||
running the XLIFF linter command in a Github Actions environment.
|
||||
* Translation providers are not experimental anymore
|
||||
|
||||
5.3
|
||||
---
|
||||
|
||||
* Add `translation:pull` and `translation:push` commands to manage translations with third-party providers
|
||||
* Add `TranslatorBagInterface::getCatalogues` method
|
||||
* Add support to load XLIFF string in `XliffFileLoader`
|
||||
|
||||
5.2.0
|
||||
-----
|
||||
|
||||
* added support for calling `trans` with ICU formatted messages
|
||||
* added `PseudoLocalizationTranslator`
|
||||
* added `TranslatableMessage` objects that represent a message that can be translated
|
||||
* added the `t()` function to easily create `TranslatableMessage` objects
|
||||
* Added support for extracting messages from `TranslatableMessage` objects
|
||||
|
||||
5.1.0
|
||||
-----
|
||||
|
||||
* added support for `name` attribute on `unit` element from xliff2 to be used as a translation key instead of always the `source` element
|
||||
|
||||
5.0.0
|
||||
-----
|
||||
|
||||
* removed support for using `null` as the locale in `Translator`
|
||||
* removed `TranslatorInterface`
|
||||
* removed `MessageSelector`
|
||||
* removed `ChoiceMessageFormatterInterface`
|
||||
* removed `PluralizationRule`
|
||||
* removed `Interval`
|
||||
* removed `transChoice()` methods, use the trans() method instead with a %count% parameter
|
||||
* removed `FileDumper::setBackup()` and `TranslationWriter::disableBackup()`
|
||||
* removed `MessageFormatter::choiceFormat()`
|
||||
* added argument `$filename` to `PhpExtractor::parseTokens()`
|
||||
* removed support for implicit STDIN usage in the `lint:xliff` command, use `lint:xliff -` (append a dash) instead to make it explicit.
|
||||
|
||||
4.4.0
|
||||
-----
|
||||
|
||||
* deprecated support for using `null` as the locale in `Translator`
|
||||
* deprecated accepting STDIN implicitly when using the `lint:xliff` command, use `lint:xliff -` (append a dash) instead to make it explicit.
|
||||
* Marked the `TranslationDataCollector` class as `@final`.
|
||||
|
||||
4.3.0
|
||||
-----
|
||||
|
||||
* Improved Xliff 1.2 loader to load the original file's metadata
|
||||
* Added `TranslatorPathsPass`
|
||||
|
||||
4.2.0
|
||||
-----
|
||||
|
||||
* Started using ICU parent locales as fallback locales.
|
||||
* allow using the ICU message format using domains with the "+intl-icu" suffix
|
||||
* deprecated `Translator::transChoice()` in favor of using `Translator::trans()` with a `%count%` parameter
|
||||
* deprecated `TranslatorInterface` in favor of `Symfony\Contracts\Translation\TranslatorInterface`
|
||||
* deprecated `MessageSelector`, `Interval` and `PluralizationRules`; use `IdentityTranslator` instead
|
||||
* Added `IntlFormatter` and `IntlFormatterInterface`
|
||||
* added support for multiple files and directories in `XliffLintCommand`
|
||||
* Marked `Translator::getFallbackLocales()` and `TranslationDataCollector::getFallbackLocales()` as internal
|
||||
|
||||
4.1.0
|
||||
-----
|
||||
|
||||
* The `FileDumper::setBackup()` method is deprecated.
|
||||
* The `TranslationWriter::disableBackup()` method is deprecated.
|
||||
* The `XliffFileDumper` will write "name" on the "unit" node when dumping XLIFF 2.0.
|
||||
|
||||
4.0.0
|
||||
-----
|
||||
|
||||
* removed the backup feature of the `FileDumper` class
|
||||
* removed `TranslationWriter::writeTranslations()` method
|
||||
* removed support for passing `MessageSelector` instances to the constructor of the `Translator` class
|
||||
|
||||
3.4.0
|
||||
-----
|
||||
|
||||
* Added `TranslationDumperPass`
|
||||
* Added `TranslationExtractorPass`
|
||||
* Added `TranslatorPass`
|
||||
* Added `TranslationReader` and `TranslationReaderInterface`
|
||||
* Added `<notes>` section to the Xliff 2.0 dumper.
|
||||
* Improved Xliff 2.0 loader to load `<notes>` section.
|
||||
* Added `TranslationWriterInterface`
|
||||
* Deprecated `TranslationWriter::writeTranslations` in favor of `TranslationWriter::write`
|
||||
* added support for adding custom message formatter and decoupling the default one.
|
||||
* Added `PhpExtractor`
|
||||
* Added `PhpStringTokenParser`
|
||||
|
||||
3.2.0
|
||||
-----
|
||||
|
||||
* Added support for escaping `|` in plural translations with double pipe.
|
||||
|
||||
3.1.0
|
||||
-----
|
||||
|
||||
* Deprecated the backup feature of the file dumper classes.
|
||||
|
||||
3.0.0
|
||||
-----
|
||||
|
||||
* removed `FileDumper::format()` method.
|
||||
* Changed the visibility of the locale property in `Translator` from protected to private.
|
||||
|
||||
2.8.0
|
||||
-----
|
||||
|
||||
* deprecated FileDumper::format(), overwrite FileDumper::formatCatalogue() instead.
|
||||
* deprecated Translator::getMessages(), rely on TranslatorBagInterface::getCatalogue() instead.
|
||||
* added `FileDumper::formatCatalogue` which allows format the catalogue without dumping it into file.
|
||||
* added option `json_encoding` to JsonFileDumper
|
||||
* added options `as_tree`, `inline` to YamlFileDumper
|
||||
* added support for XLIFF 2.0.
|
||||
* added support for XLIFF target and tool attributes.
|
||||
* added message parameters to DataCollectorTranslator.
|
||||
* [DEPRECATION] The `DiffOperation` class has been deprecated and
|
||||
will be removed in Symfony 3.0, since its operation has nothing to do with 'diff',
|
||||
so the class name is misleading. The `TargetOperation` class should be used for
|
||||
this use-case instead.
|
||||
|
||||
2.7.0
|
||||
-----
|
||||
|
||||
* added DataCollectorTranslator for collecting the translated messages.
|
||||
|
||||
2.6.0
|
||||
-----
|
||||
|
||||
* added possibility to cache catalogues
|
||||
* added TranslatorBagInterface
|
||||
* added LoggingTranslator
|
||||
* added Translator::getMessages() for retrieving the message catalogue as an array
|
||||
|
||||
2.5.0
|
||||
-----
|
||||
|
||||
* added relative file path template to the file dumpers
|
||||
* added optional backup to the file dumpers
|
||||
* changed IcuResFileDumper to extend FileDumper
|
||||
|
||||
2.3.0
|
||||
-----
|
||||
|
||||
* added classes to make operations on catalogues (like making a diff or a merge on 2 catalogues)
|
||||
* added Translator::getFallbackLocales()
|
||||
* deprecated Translator::setFallbackLocale() in favor of the new Translator::setFallbackLocales() method
|
||||
|
||||
2.2.0
|
||||
-----
|
||||
|
||||
* QtTranslationsLoader class renamed to QtFileLoader. QtTranslationsLoader is deprecated and will be removed in 2.3.
|
||||
* [BC BREAK] uniformized the exception thrown by the load() method when an error occurs. The load() method now
|
||||
throws Symfony\Component\Translation\Exception\NotFoundResourceException when a resource cannot be found
|
||||
and Symfony\Component\Translation\Exception\InvalidResourceException when a resource is invalid.
|
||||
* changed the exception class thrown by some load() methods from \RuntimeException to \InvalidArgumentException
|
||||
(IcuDatFileLoader, IcuResFileLoader and QtFileLoader)
|
||||
|
||||
2.1.0
|
||||
-----
|
||||
|
||||
* added support for more than one fallback locale
|
||||
* added support for extracting translation messages from templates (Twig and PHP)
|
||||
* added dumpers for translation catalogs
|
||||
* added support for QT, gettext, and ResourceBundles
|
||||
@@ -0,0 +1,203 @@
|
||||
<?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\Catalogue;
|
||||
|
||||
use Symfony\Component\Translation\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Translation\Exception\LogicException;
|
||||
use Symfony\Component\Translation\MessageCatalogue;
|
||||
use Symfony\Component\Translation\MessageCatalogueInterface;
|
||||
|
||||
/**
|
||||
* Base catalogues binary operation class.
|
||||
*
|
||||
* A catalogue binary operation performs operation on
|
||||
* source (the left argument) and target (the right argument) catalogues.
|
||||
*
|
||||
* @author Jean-François Simon <contact@jfsimon.fr>
|
||||
*/
|
||||
abstract class AbstractOperation implements OperationInterface
|
||||
{
|
||||
public const OBSOLETE_BATCH = 'obsolete';
|
||||
public const NEW_BATCH = 'new';
|
||||
public const ALL_BATCH = 'all';
|
||||
|
||||
protected $source;
|
||||
protected $target;
|
||||
protected $result;
|
||||
|
||||
/**
|
||||
* @var array|null The domains affected by this operation
|
||||
*/
|
||||
private $domains;
|
||||
|
||||
/**
|
||||
* This array stores 'all', 'new' and 'obsolete' messages for all valid domains.
|
||||
*
|
||||
* The data structure of this array is as follows:
|
||||
*
|
||||
* [
|
||||
* 'domain 1' => [
|
||||
* 'all' => [...],
|
||||
* 'new' => [...],
|
||||
* 'obsolete' => [...]
|
||||
* ],
|
||||
* 'domain 2' => [
|
||||
* 'all' => [...],
|
||||
* 'new' => [...],
|
||||
* 'obsolete' => [...]
|
||||
* ],
|
||||
* ...
|
||||
* ]
|
||||
*
|
||||
* @var array The array that stores 'all', 'new' and 'obsolete' messages
|
||||
*/
|
||||
protected $messages;
|
||||
|
||||
/**
|
||||
* @throws LogicException
|
||||
*/
|
||||
public function __construct(MessageCatalogueInterface $source, MessageCatalogueInterface $target)
|
||||
{
|
||||
if ($source->getLocale() !== $target->getLocale()) {
|
||||
throw new LogicException('Operated catalogues must belong to the same locale.');
|
||||
}
|
||||
|
||||
$this->source = $source;
|
||||
$this->target = $target;
|
||||
$this->result = new MessageCatalogue($source->getLocale());
|
||||
$this->messages = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDomains()
|
||||
{
|
||||
if (null === $this->domains) {
|
||||
$domains = [];
|
||||
foreach ([$this->source, $this->target] as $catalogue) {
|
||||
foreach ($catalogue->getDomains() as $domain) {
|
||||
$domains[$domain] = $domain;
|
||||
|
||||
if ($catalogue->all($domainIcu = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX)) {
|
||||
$domains[$domainIcu] = $domainIcu;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->domains = array_values($domains);
|
||||
}
|
||||
|
||||
return $this->domains;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMessages(string $domain)
|
||||
{
|
||||
if (!\in_array($domain, $this->getDomains())) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain));
|
||||
}
|
||||
|
||||
if (!isset($this->messages[$domain][self::ALL_BATCH])) {
|
||||
$this->processDomain($domain);
|
||||
}
|
||||
|
||||
return $this->messages[$domain][self::ALL_BATCH];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNewMessages(string $domain)
|
||||
{
|
||||
if (!\in_array($domain, $this->getDomains())) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain));
|
||||
}
|
||||
|
||||
if (!isset($this->messages[$domain][self::NEW_BATCH])) {
|
||||
$this->processDomain($domain);
|
||||
}
|
||||
|
||||
return $this->messages[$domain][self::NEW_BATCH];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getObsoleteMessages(string $domain)
|
||||
{
|
||||
if (!\in_array($domain, $this->getDomains())) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain));
|
||||
}
|
||||
|
||||
if (!isset($this->messages[$domain][self::OBSOLETE_BATCH])) {
|
||||
$this->processDomain($domain);
|
||||
}
|
||||
|
||||
return $this->messages[$domain][self::OBSOLETE_BATCH];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getResult()
|
||||
{
|
||||
foreach ($this->getDomains() as $domain) {
|
||||
if (!isset($this->messages[$domain])) {
|
||||
$this->processDomain($domain);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param self::*_BATCH $batch
|
||||
*/
|
||||
public function moveMessagesToIntlDomainsIfPossible(string $batch = self::ALL_BATCH): void
|
||||
{
|
||||
// If MessageFormatter class does not exists, intl domains are not supported.
|
||||
if (!class_exists(\MessageFormatter::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->getDomains() as $domain) {
|
||||
$intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
|
||||
switch ($batch) {
|
||||
case self::OBSOLETE_BATCH: $messages = $this->getObsoleteMessages($domain); break;
|
||||
case self::NEW_BATCH: $messages = $this->getNewMessages($domain); break;
|
||||
case self::ALL_BATCH: $messages = $this->getMessages($domain); break;
|
||||
default: throw new \InvalidArgumentException(sprintf('$batch argument must be one of ["%s", "%s", "%s"].', self::ALL_BATCH, self::NEW_BATCH, self::OBSOLETE_BATCH));
|
||||
}
|
||||
|
||||
if (!$messages || (!$this->source->all($intlDomain) && $this->source->all($domain))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = $this->getResult();
|
||||
$allIntlMessages = $result->all($intlDomain);
|
||||
$currentMessages = array_diff_key($messages, $result->all($domain));
|
||||
$result->replace($currentMessages, $domain);
|
||||
$result->replace($allIntlMessages + $messages, $intlDomain);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs operation on source and target catalogues for the given domain and
|
||||
* stores the results.
|
||||
*
|
||||
* @param string $domain The domain which the operation will be performed for
|
||||
*/
|
||||
abstract protected function processDomain(string $domain);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?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\Catalogue;
|
||||
|
||||
use Symfony\Component\Translation\MessageCatalogueInterface;
|
||||
|
||||
/**
|
||||
* Merge operation between two catalogues as follows:
|
||||
* all = source ∪ target = {x: x ∈ source ∨ x ∈ target}
|
||||
* new = all ∖ source = {x: x ∈ target ∧ x ∉ source}
|
||||
* obsolete = source ∖ all = {x: x ∈ source ∧ x ∉ source ∧ x ∉ target} = ∅
|
||||
* Basically, the result contains messages from both catalogues.
|
||||
*
|
||||
* @author Jean-François Simon <contact@jfsimon.fr>
|
||||
*/
|
||||
class MergeOperation extends AbstractOperation
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function processDomain(string $domain)
|
||||
{
|
||||
$this->messages[$domain] = [
|
||||
'all' => [],
|
||||
'new' => [],
|
||||
'obsolete' => [],
|
||||
];
|
||||
$intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
|
||||
|
||||
foreach ($this->source->all($domain) as $id => $message) {
|
||||
$this->messages[$domain]['all'][$id] = $message;
|
||||
$d = $this->source->defines($id, $intlDomain) ? $intlDomain : $domain;
|
||||
$this->result->add([$id => $message], $d);
|
||||
if (null !== $keyMetadata = $this->source->getMetadata($id, $d)) {
|
||||
$this->result->setMetadata($id, $keyMetadata, $d);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->target->all($domain) as $id => $message) {
|
||||
if (!$this->source->has($id, $domain)) {
|
||||
$this->messages[$domain]['all'][$id] = $message;
|
||||
$this->messages[$domain]['new'][$id] = $message;
|
||||
$d = $this->target->defines($id, $intlDomain) ? $intlDomain : $domain;
|
||||
$this->result->add([$id => $message], $d);
|
||||
if (null !== $keyMetadata = $this->target->getMetadata($id, $d)) {
|
||||
$this->result->setMetadata($id, $keyMetadata, $d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?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\Catalogue;
|
||||
|
||||
use Symfony\Component\Translation\MessageCatalogueInterface;
|
||||
|
||||
/**
|
||||
* Represents an operation on catalogue(s).
|
||||
*
|
||||
* An instance of this interface performs an operation on one or more catalogues and
|
||||
* stores intermediate and final results of the operation.
|
||||
*
|
||||
* The first catalogue in its argument(s) is called the 'source catalogue' or 'source' and
|
||||
* the following results are stored:
|
||||
*
|
||||
* Messages: also called 'all', are valid messages for the given domain after the operation is performed.
|
||||
*
|
||||
* New Messages: also called 'new' (new = all ∖ source = {x: x ∈ all ∧ x ∉ source}).
|
||||
*
|
||||
* Obsolete Messages: also called 'obsolete' (obsolete = source ∖ all = {x: x ∈ source ∧ x ∉ all}).
|
||||
*
|
||||
* Result: also called 'result', is the resulting catalogue for the given domain that holds the same messages as 'all'.
|
||||
*
|
||||
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
|
||||
*/
|
||||
interface OperationInterface
|
||||
{
|
||||
/**
|
||||
* Returns domains affected by operation.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getDomains();
|
||||
|
||||
/**
|
||||
* Returns all valid messages ('all') after operation.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getMessages(string $domain);
|
||||
|
||||
/**
|
||||
* Returns new messages ('new') after operation.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getNewMessages(string $domain);
|
||||
|
||||
/**
|
||||
* Returns obsolete messages ('obsolete') after operation.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getObsoleteMessages(string $domain);
|
||||
|
||||
/**
|
||||
* Returns resulting catalogue ('result').
|
||||
*
|
||||
* @return MessageCatalogueInterface
|
||||
*/
|
||||
public function getResult();
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?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\Catalogue;
|
||||
|
||||
use Symfony\Component\Translation\MessageCatalogueInterface;
|
||||
|
||||
/**
|
||||
* Target operation between two catalogues:
|
||||
* intersection = source ∩ target = {x: x ∈ source ∧ x ∈ target}
|
||||
* all = intersection ∪ (target ∖ intersection) = target
|
||||
* new = all ∖ source = {x: x ∈ target ∧ x ∉ source}
|
||||
* obsolete = source ∖ all = source ∖ target = {x: x ∈ source ∧ x ∉ target}
|
||||
* Basically, the result contains messages from the target catalogue.
|
||||
*
|
||||
* @author Michael Lee <michael.lee@zerustech.com>
|
||||
*/
|
||||
class TargetOperation extends AbstractOperation
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function processDomain(string $domain)
|
||||
{
|
||||
$this->messages[$domain] = [
|
||||
'all' => [],
|
||||
'new' => [],
|
||||
'obsolete' => [],
|
||||
];
|
||||
$intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
|
||||
|
||||
// For 'all' messages, the code can't be simplified as ``$this->messages[$domain]['all'] = $target->all($domain);``,
|
||||
// because doing so will drop messages like {x: x ∈ source ∧ x ∉ target.all ∧ x ∈ target.fallback}
|
||||
//
|
||||
// For 'new' messages, the code can't be simplified as ``array_diff_assoc($this->target->all($domain), $this->source->all($domain));``
|
||||
// because doing so will not exclude messages like {x: x ∈ target ∧ x ∉ source.all ∧ x ∈ source.fallback}
|
||||
//
|
||||
// For 'obsolete' messages, the code can't be simplified as ``array_diff_assoc($this->source->all($domain), $this->target->all($domain))``
|
||||
// because doing so will not exclude messages like {x: x ∈ source ∧ x ∉ target.all ∧ x ∈ target.fallback}
|
||||
|
||||
foreach ($this->source->all($domain) as $id => $message) {
|
||||
if ($this->target->has($id, $domain)) {
|
||||
$this->messages[$domain]['all'][$id] = $message;
|
||||
$d = $this->source->defines($id, $intlDomain) ? $intlDomain : $domain;
|
||||
$this->result->add([$id => $message], $d);
|
||||
if (null !== $keyMetadata = $this->source->getMetadata($id, $d)) {
|
||||
$this->result->setMetadata($id, $keyMetadata, $d);
|
||||
}
|
||||
} else {
|
||||
$this->messages[$domain]['obsolete'][$id] = $message;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->target->all($domain) as $id => $message) {
|
||||
if (!$this->source->has($id, $domain)) {
|
||||
$this->messages[$domain]['all'][$id] = $message;
|
||||
$this->messages[$domain]['new'][$id] = $message;
|
||||
$d = $this->target->defines($id, $intlDomain) ? $intlDomain : $domain;
|
||||
$this->result->add([$id => $message], $d);
|
||||
if (null !== $keyMetadata = $this->target->getMetadata($id, $d)) {
|
||||
$this->result->setMetadata($id, $keyMetadata, $d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
<?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\Command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Translation\Catalogue\TargetOperation;
|
||||
use Symfony\Component\Translation\MessageCatalogue;
|
||||
use Symfony\Component\Translation\Provider\TranslationProviderCollection;
|
||||
use Symfony\Component\Translation\Reader\TranslationReaderInterface;
|
||||
use Symfony\Component\Translation\Writer\TranslationWriterInterface;
|
||||
|
||||
/**
|
||||
* @author Mathieu Santostefano <msantostefano@protonmail.com>
|
||||
*/
|
||||
final class TranslationPullCommand extends Command
|
||||
{
|
||||
use TranslationTrait;
|
||||
|
||||
protected static $defaultName = 'translation:pull';
|
||||
protected static $defaultDescription = 'Pull translations from a given provider.';
|
||||
|
||||
private $providerCollection;
|
||||
private $writer;
|
||||
private $reader;
|
||||
private $defaultLocale;
|
||||
private $transPaths;
|
||||
private $enabledLocales;
|
||||
|
||||
public function __construct(TranslationProviderCollection $providerCollection, TranslationWriterInterface $writer, TranslationReaderInterface $reader, string $defaultLocale, array $transPaths = [], array $enabledLocales = [])
|
||||
{
|
||||
$this->providerCollection = $providerCollection;
|
||||
$this->writer = $writer;
|
||||
$this->reader = $reader;
|
||||
$this->defaultLocale = $defaultLocale;
|
||||
$this->transPaths = $transPaths;
|
||||
$this->enabledLocales = $enabledLocales;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
|
||||
{
|
||||
if ($input->mustSuggestArgumentValuesFor('provider')) {
|
||||
$suggestions->suggestValues($this->providerCollection->keys());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($input->mustSuggestOptionValuesFor('domains')) {
|
||||
$provider = $this->providerCollection->get($input->getArgument('provider'));
|
||||
|
||||
if ($provider && method_exists($provider, 'getDomains')) {
|
||||
$domains = $provider->getDomains();
|
||||
$suggestions->suggestValues($domains);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($input->mustSuggestOptionValuesFor('locales')) {
|
||||
$suggestions->suggestValues($this->enabledLocales);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($input->mustSuggestOptionValuesFor('format')) {
|
||||
$suggestions->suggestValues(['php', 'xlf', 'xlf12', 'xlf20', 'po', 'mo', 'yml', 'yaml', 'ts', 'csv', 'json', 'ini', 'res']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$keys = $this->providerCollection->keys();
|
||||
$defaultProvider = 1 === \count($keys) ? $keys[0] : null;
|
||||
|
||||
$this
|
||||
->setDefinition([
|
||||
new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to pull translations from.', $defaultProvider),
|
||||
new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with provider ones (it will delete not synchronized messages).'),
|
||||
new InputOption('intl-icu', null, InputOption::VALUE_NONE, 'Associated to --force option, it will write messages in "%domain%+intl-icu.%locale%.xlf" files.'),
|
||||
new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to pull.'),
|
||||
new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to pull.'),
|
||||
new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format.', 'xlf12'),
|
||||
])
|
||||
->setHelp(<<<'EOF'
|
||||
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:
|
||||
|
||||
<info>php %command.full_name% --force provider</>
|
||||
|
||||
Full example:
|
||||
|
||||
<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
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$provider = $this->providerCollection->get($input->getArgument('provider'));
|
||||
$force = $input->getOption('force');
|
||||
$intlIcu = $input->getOption('intl-icu');
|
||||
$locales = $input->getOption('locales') ?: $this->enabledLocales;
|
||||
$domains = $input->getOption('domains');
|
||||
$format = $input->getOption('format');
|
||||
$xliffVersion = '1.2';
|
||||
|
||||
if ($intlIcu && !$force) {
|
||||
$io->note('--intl-icu option only has an effect when used with --force. Here, it will be ignored.');
|
||||
}
|
||||
|
||||
switch ($format) {
|
||||
case 'xlf20': $xliffVersion = '2.0';
|
||||
// no break
|
||||
case 'xlf12': $format = 'xlf';
|
||||
}
|
||||
|
||||
$writeOptions = [
|
||||
'path' => end($this->transPaths),
|
||||
'xliff_version' => $xliffVersion,
|
||||
'default_locale' => $this->defaultLocale,
|
||||
];
|
||||
|
||||
if (!$domains) {
|
||||
$domains = $provider->getDomains();
|
||||
}
|
||||
|
||||
$providerTranslations = $provider->read($domains, $locales);
|
||||
|
||||
if ($force) {
|
||||
foreach ($providerTranslations->getCatalogues() as $catalogue) {
|
||||
$operation = new TargetOperation(new MessageCatalogue($catalogue->getLocale()), $catalogue);
|
||||
if ($intlIcu) {
|
||||
$operation->moveMessagesToIntlDomainsIfPossible();
|
||||
}
|
||||
$this->writer->write($operation->getResult(), $format, $writeOptions);
|
||||
}
|
||||
|
||||
$io->success(sprintf('Local translations has been updated from "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths);
|
||||
|
||||
// Append pulled translations to local ones.
|
||||
$localTranslations->addBag($providerTranslations->diff($localTranslations));
|
||||
|
||||
foreach ($localTranslations->getCatalogues() as $catalogue) {
|
||||
$this->writer->write($catalogue, $format, $writeOptions);
|
||||
}
|
||||
|
||||
$io->success(sprintf('New translations from "%s" has been written locally (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<?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\Command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Translation\Provider\FilteringProvider;
|
||||
use Symfony\Component\Translation\Provider\TranslationProviderCollection;
|
||||
use Symfony\Component\Translation\Reader\TranslationReaderInterface;
|
||||
use Symfony\Component\Translation\TranslatorBag;
|
||||
|
||||
/**
|
||||
* @author Mathieu Santostefano <msantostefano@protonmail.com>
|
||||
*/
|
||||
final class TranslationPushCommand extends Command
|
||||
{
|
||||
use TranslationTrait;
|
||||
|
||||
protected static $defaultName = 'translation:push';
|
||||
protected static $defaultDescription = 'Push translations to a given provider.';
|
||||
|
||||
private $providers;
|
||||
private $reader;
|
||||
private $transPaths;
|
||||
private $enabledLocales;
|
||||
|
||||
public function __construct(TranslationProviderCollection $providers, TranslationReaderInterface $reader, array $transPaths = [], array $enabledLocales = [])
|
||||
{
|
||||
$this->providers = $providers;
|
||||
$this->reader = $reader;
|
||||
$this->transPaths = $transPaths;
|
||||
$this->enabledLocales = $enabledLocales;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
|
||||
{
|
||||
if ($input->mustSuggestArgumentValuesFor('provider')) {
|
||||
$suggestions->suggestValues($this->providers->keys());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($input->mustSuggestOptionValuesFor('domains')) {
|
||||
$provider = $this->providers->get($input->getArgument('provider'));
|
||||
|
||||
if ($provider && method_exists($provider, 'getDomains')) {
|
||||
$domains = $provider->getDomains();
|
||||
$suggestions->suggestValues($domains);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($input->mustSuggestOptionValuesFor('locales')) {
|
||||
$suggestions->suggestValues($this->enabledLocales);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$keys = $this->providers->keys();
|
||||
$defaultProvider = 1 === \count($keys) ? $keys[0] : null;
|
||||
|
||||
$this
|
||||
->setDefinition([
|
||||
new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to push translations to.', $defaultProvider),
|
||||
new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with local ones (it will delete not synchronized messages).'),
|
||||
new InputOption('delete-missing', null, InputOption::VALUE_NONE, 'Delete translations available on provider but not locally.'),
|
||||
new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to push.'),
|
||||
new InputOption('locales', null, InputOption::VALUE_OPTIONAL | 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.
|
||||
|
||||
You can overwrite existing translations by using the <comment>--force</> flag:
|
||||
|
||||
<info>php %command.full_name% --force provider</>
|
||||
|
||||
You can delete provider translations which are not present locally by using the <comment>--delete-missing</> flag:
|
||||
|
||||
<info>php %command.full_name% --delete-missing provider</>
|
||||
|
||||
Full example:
|
||||
|
||||
<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
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$provider = $this->providers->get($input->getArgument('provider'));
|
||||
|
||||
if (!$this->enabledLocales) {
|
||||
throw new InvalidArgumentException(sprintf('You must define "framework.translator.enabled_locales" or "framework.translator.providers.%s.locales" config key in order to work with translation providers.', parse_url($provider, \PHP_URL_SCHEME)));
|
||||
}
|
||||
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$domains = $input->getOption('domains');
|
||||
$locales = $input->getOption('locales');
|
||||
$force = $input->getOption('force');
|
||||
$deleteMissing = $input->getOption('delete-missing');
|
||||
|
||||
if (!$domains && $provider instanceof FilteringProvider) {
|
||||
$domains = $provider->getDomains();
|
||||
}
|
||||
|
||||
// Reading local translations must be done after retrieving the domains from the provider
|
||||
// in order to manage only translations from configured domains
|
||||
$localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths);
|
||||
|
||||
if (!$domains) {
|
||||
$domains = $this->getDomainsFromTranslatorBag($localTranslations);
|
||||
}
|
||||
|
||||
if (!$deleteMissing && $force) {
|
||||
$provider->write($localTranslations);
|
||||
|
||||
$io->success(sprintf('All local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$providerTranslations = $provider->read($domains, $locales);
|
||||
|
||||
if ($deleteMissing) {
|
||||
$provider->delete($providerTranslations->diff($localTranslations));
|
||||
|
||||
$io->success(sprintf('Missing translations on "%s" has been deleted (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
|
||||
|
||||
// Read provider translations again, after missing translations deletion,
|
||||
// to avoid push freshly deleted translations.
|
||||
$providerTranslations = $provider->read($domains, $locales);
|
||||
}
|
||||
|
||||
$translationsToWrite = $localTranslations->diff($providerTranslations);
|
||||
|
||||
if ($force) {
|
||||
$translationsToWrite->addBag($localTranslations->intersect($providerTranslations));
|
||||
}
|
||||
|
||||
$provider->write($translationsToWrite);
|
||||
|
||||
$io->success(sprintf('%s local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', $force ? 'All' : 'New', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function getDomainsFromTranslatorBag(TranslatorBag $translatorBag): array
|
||||
{
|
||||
$domains = [];
|
||||
|
||||
foreach ($translatorBag->getCatalogues() as $catalogue) {
|
||||
$domains += $catalogue->getDomains();
|
||||
}
|
||||
|
||||
return array_unique($domains);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?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\Command;
|
||||
|
||||
use Symfony\Component\Translation\MessageCatalogue;
|
||||
use Symfony\Component\Translation\MessageCatalogueInterface;
|
||||
use Symfony\Component\Translation\TranslatorBag;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
trait TranslationTrait
|
||||
{
|
||||
private function readLocalTranslations(array $locales, array $domains, array $transPaths): TranslatorBag
|
||||
{
|
||||
$bag = new TranslatorBag();
|
||||
|
||||
foreach ($locales as $locale) {
|
||||
$catalogue = new MessageCatalogue($locale);
|
||||
foreach ($transPaths as $path) {
|
||||
$this->reader->read($path, $catalogue);
|
||||
}
|
||||
|
||||
if ($domains) {
|
||||
foreach ($domains as $domain) {
|
||||
$bag->addCatalogue($this->filterCatalogue($catalogue, $domain));
|
||||
}
|
||||
} else {
|
||||
$bag->addCatalogue($catalogue);
|
||||
}
|
||||
}
|
||||
|
||||
return $bag;
|
||||
}
|
||||
|
||||
private function filterCatalogue(MessageCatalogue $catalogue, string $domain): MessageCatalogue
|
||||
{
|
||||
$filteredCatalogue = new MessageCatalogue($catalogue->getLocale());
|
||||
|
||||
// extract intl-icu messages only
|
||||
$intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
|
||||
if ($intlMessages = $catalogue->all($intlDomain)) {
|
||||
$filteredCatalogue->add($intlMessages, $intlDomain);
|
||||
}
|
||||
|
||||
// extract all messages and subtract intl-icu messages
|
||||
if ($messages = array_diff($catalogue->all($domain), $intlMessages)) {
|
||||
$filteredCatalogue->add($messages, $domain);
|
||||
}
|
||||
foreach ($catalogue->getResources() as $resource) {
|
||||
$filteredCatalogue->addResource($resource);
|
||||
}
|
||||
|
||||
if ($metadata = $catalogue->getMetadata('', $intlDomain)) {
|
||||
foreach ($metadata as $k => $v) {
|
||||
$filteredCatalogue->setMetadata($k, $v, $intlDomain);
|
||||
}
|
||||
}
|
||||
|
||||
if ($metadata = $catalogue->getMetadata('', $domain)) {
|
||||
foreach ($metadata as $k => $v) {
|
||||
$filteredCatalogue->setMetadata($k, $v, $domain);
|
||||
}
|
||||
}
|
||||
|
||||
return $filteredCatalogue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
<?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\Command;
|
||||
|
||||
use Symfony\Component\Console\CI\GithubActionReporter;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Translation\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Translation\Util\XliffUtils;
|
||||
|
||||
/**
|
||||
* Validates XLIFF files syntax and outputs encountered errors.
|
||||
*
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
* @author Robin Chalas <robin.chalas@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class XliffLintCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'lint:xliff';
|
||||
protected static $defaultDescription = 'Lint an XLIFF file and outputs encountered errors';
|
||||
|
||||
private $format;
|
||||
private $displayCorrectFiles;
|
||||
private $directoryIteratorProvider;
|
||||
private $isReadableProvider;
|
||||
private $requireStrictFileNames;
|
||||
|
||||
public function __construct(?string $name = null, ?callable $directoryIteratorProvider = null, ?callable $isReadableProvider = null, bool $requireStrictFileNames = true)
|
||||
{
|
||||
parent::__construct($name);
|
||||
|
||||
$this->directoryIteratorProvider = $directoryIteratorProvider;
|
||||
$this->isReadableProvider = $isReadableProvider;
|
||||
$this->requireStrictFileNames = $requireStrictFileNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setDescription(self::$defaultDescription)
|
||||
->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN')
|
||||
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format')
|
||||
->setHelp(<<<EOF
|
||||
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:
|
||||
|
||||
<info>cat filename | php %command.full_name% -</info>
|
||||
|
||||
You can also validate the syntax of a file:
|
||||
|
||||
<info>php %command.full_name% filename</info>
|
||||
|
||||
Or of a whole directory:
|
||||
|
||||
<info>php %command.full_name% dirname</info>
|
||||
<info>php %command.full_name% dirname --format=json</info>
|
||||
|
||||
EOF
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$filenames = (array) $input->getArgument('filename');
|
||||
$this->format = $input->getOption('format') ?? (GithubActionReporter::isGithubActionEnvironment() ? 'github' : 'txt');
|
||||
$this->displayCorrectFiles = $output->isVerbose();
|
||||
|
||||
if (['-'] === $filenames) {
|
||||
return $this->display($io, [$this->validate(file_get_contents('php://stdin'))]);
|
||||
}
|
||||
|
||||
if (!$filenames) {
|
||||
throw new RuntimeException('Please provide a filename or pipe file content to STDIN.');
|
||||
}
|
||||
|
||||
$filesInfo = [];
|
||||
foreach ($filenames as $filename) {
|
||||
if (!$this->isReadable($filename)) {
|
||||
throw new RuntimeException(sprintf('File or directory "%s" is not readable.', $filename));
|
||||
}
|
||||
|
||||
foreach ($this->getFiles($filename) as $file) {
|
||||
$filesInfo[] = $this->validate(file_get_contents($file), $file);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->display($io, $filesInfo);
|
||||
}
|
||||
|
||||
private function validate(string $content, ?string $file = null): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
// Avoid: Warning DOMDocument::loadXML(): Empty string supplied as input
|
||||
if ('' === trim($content)) {
|
||||
return ['file' => $file, 'valid' => true];
|
||||
}
|
||||
|
||||
$internal = libxml_use_internal_errors(true);
|
||||
|
||||
$document = new \DOMDocument();
|
||||
$document->loadXML($content);
|
||||
|
||||
if (null !== $targetLanguage = $this->getTargetLanguageFromFile($document)) {
|
||||
$normalizedLocalePattern = sprintf('(%s|%s)', preg_quote($targetLanguage, '/'), preg_quote(str_replace('-', '_', $targetLanguage), '/'));
|
||||
// strict file names require translation files to be named '____.locale.xlf'
|
||||
// otherwise, both '____.locale.xlf' and 'locale.____.xlf' are allowed
|
||||
// also, the regexp matching must be case-insensitive, as defined for 'target-language' values
|
||||
// http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html#target-language
|
||||
$expectedFilenamePattern = $this->requireStrictFileNames ? sprintf('/^.*\.(?i:%s)\.(?:xlf|xliff)/', $normalizedLocalePattern) : sprintf('/^(?:.*\.(?i:%s)|(?i:%s)\..*)\.(?:xlf|xliff)/', $normalizedLocalePattern, $normalizedLocalePattern);
|
||||
|
||||
if (0 === preg_match($expectedFilenamePattern, basename($file))) {
|
||||
$errors[] = [
|
||||
'line' => -1,
|
||||
'column' => -1,
|
||||
'message' => sprintf('There is a mismatch between the language included in the file name ("%s") and the "%s" value used in the "target-language" attribute of the file.', basename($file), $targetLanguage),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
foreach (XliffUtils::validateSchema($document) as $xmlError) {
|
||||
$errors[] = [
|
||||
'line' => $xmlError['line'],
|
||||
'column' => $xmlError['column'],
|
||||
'message' => $xmlError['message'],
|
||||
];
|
||||
}
|
||||
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($internal);
|
||||
|
||||
return ['file' => $file, 'valid' => 0 === \count($errors), 'messages' => $errors];
|
||||
}
|
||||
|
||||
private function display(SymfonyStyle $io, array $files)
|
||||
{
|
||||
switch ($this->format) {
|
||||
case 'txt':
|
||||
return $this->displayTxt($io, $files);
|
||||
case 'json':
|
||||
return $this->displayJson($io, $files);
|
||||
case 'github':
|
||||
return $this->displayTxt($io, $files, true);
|
||||
default:
|
||||
throw new InvalidArgumentException(sprintf('The format "%s" is not supported.', $this->format));
|
||||
}
|
||||
}
|
||||
|
||||
private function displayTxt(SymfonyStyle $io, array $filesInfo, bool $errorAsGithubAnnotations = false)
|
||||
{
|
||||
$countFiles = \count($filesInfo);
|
||||
$erroredFiles = 0;
|
||||
$githubReporter = $errorAsGithubAnnotations ? new GithubActionReporter($io) : null;
|
||||
|
||||
foreach ($filesInfo as $info) {
|
||||
if ($info['valid'] && $this->displayCorrectFiles) {
|
||||
$io->comment('<info>OK</info>'.($info['file'] ? sprintf(' in %s', $info['file']) : ''));
|
||||
} 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) {
|
||||
// general document errors have a '-1' line number
|
||||
$line = -1 === $error['line'] ? null : $error['line'];
|
||||
|
||||
if ($githubReporter) {
|
||||
$githubReporter->error($error['message'], $info['file'], $line, null !== $line ? $error['column'] : null);
|
||||
}
|
||||
|
||||
return null === $line ? $error['message'] : sprintf('Line %d, Column %d: %s', $line, $error['column'], $error['message']);
|
||||
}, $info['messages']));
|
||||
}
|
||||
}
|
||||
|
||||
if (0 === $erroredFiles) {
|
||||
$io->success(sprintf('All %d XLIFF files contain valid syntax.', $countFiles));
|
||||
} else {
|
||||
$io->warning(sprintf('%d XLIFF files have valid syntax and %d contain errors.', $countFiles - $erroredFiles, $erroredFiles));
|
||||
}
|
||||
|
||||
return min($erroredFiles, 1);
|
||||
}
|
||||
|
||||
private function displayJson(SymfonyStyle $io, array $filesInfo)
|
||||
{
|
||||
$errors = 0;
|
||||
|
||||
array_walk($filesInfo, function (&$v) use (&$errors) {
|
||||
$v['file'] = (string) $v['file'];
|
||||
if (!$v['valid']) {
|
||||
++$errors;
|
||||
}
|
||||
});
|
||||
|
||||
$io->writeln(json_encode($filesInfo, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES));
|
||||
|
||||
return min($errors, 1);
|
||||
}
|
||||
|
||||
private function getFiles(string $fileOrDirectory)
|
||||
{
|
||||
if (is_file($fileOrDirectory)) {
|
||||
yield new \SplFileInfo($fileOrDirectory);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->getDirectoryIterator($fileOrDirectory) as $file) {
|
||||
if (!\in_array($file->getExtension(), ['xlf', 'xliff'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield $file;
|
||||
}
|
||||
}
|
||||
|
||||
private function getDirectoryIterator(string $directory)
|
||||
{
|
||||
$default = function ($directory) {
|
||||
return new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS),
|
||||
\RecursiveIteratorIterator::LEAVES_ONLY
|
||||
);
|
||||
};
|
||||
|
||||
if (null !== $this->directoryIteratorProvider) {
|
||||
return ($this->directoryIteratorProvider)($directory, $default);
|
||||
}
|
||||
|
||||
return $default($directory);
|
||||
}
|
||||
|
||||
private function isReadable(string $fileOrDirectory)
|
||||
{
|
||||
$default = function ($fileOrDirectory) {
|
||||
return is_readable($fileOrDirectory);
|
||||
};
|
||||
|
||||
if (null !== $this->isReadableProvider) {
|
||||
return ($this->isReadableProvider)($fileOrDirectory, $default);
|
||||
}
|
||||
|
||||
return $default($fileOrDirectory);
|
||||
}
|
||||
|
||||
private function getTargetLanguageFromFile(\DOMDocument $xliffContents): ?string
|
||||
{
|
||||
foreach ($xliffContents->getElementsByTagName('file')[0]->attributes ?? [] as $attribute) {
|
||||
if ('target-language' === $attribute->nodeName) {
|
||||
return $attribute->nodeValue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
|
||||
{
|
||||
if ($input->mustSuggestOptionValuesFor('format')) {
|
||||
$suggestions->suggestValues(['txt', 'json', 'github']);
|
||||
}
|
||||
}
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
<?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\DataCollector;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
|
||||
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
|
||||
use Symfony\Component\Translation\DataCollectorTranslator;
|
||||
use Symfony\Component\VarDumper\Cloner\Data;
|
||||
|
||||
/**
|
||||
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class TranslationDataCollector extends DataCollector implements LateDataCollectorInterface
|
||||
{
|
||||
private $translator;
|
||||
|
||||
public function __construct(DataCollectorTranslator $translator)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function lateCollect()
|
||||
{
|
||||
$messages = $this->sanitizeCollectedMessages($this->translator->getCollectedMessages());
|
||||
|
||||
$this->data += $this->computeCount($messages);
|
||||
$this->data['messages'] = $messages;
|
||||
|
||||
$this->data = $this->cloneVar($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function collect(Request $request, Response $response, ?\Throwable $exception = null)
|
||||
{
|
||||
$this->data['locale'] = $this->translator->getLocale();
|
||||
$this->data['fallback_locales'] = $this->translator->getFallbackLocales();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
$this->data = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|Data
|
||||
*/
|
||||
public function getMessages()
|
||||
{
|
||||
return $this->data['messages'] ?? [];
|
||||
}
|
||||
|
||||
public function getCountMissings(): int
|
||||
{
|
||||
return $this->data[DataCollectorTranslator::MESSAGE_MISSING] ?? 0;
|
||||
}
|
||||
|
||||
public function getCountFallbacks(): int
|
||||
{
|
||||
return $this->data[DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK] ?? 0;
|
||||
}
|
||||
|
||||
public function getCountDefines(): int
|
||||
{
|
||||
return $this->data[DataCollectorTranslator::MESSAGE_DEFINED] ?? 0;
|
||||
}
|
||||
|
||||
public function getLocale()
|
||||
{
|
||||
return !empty($this->data['locale']) ? $this->data['locale'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getFallbackLocales()
|
||||
{
|
||||
return (isset($this->data['fallback_locales']) && \count($this->data['fallback_locales']) > 0) ? $this->data['fallback_locales'] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'translation';
|
||||
}
|
||||
|
||||
private function sanitizeCollectedMessages(array $messages)
|
||||
{
|
||||
$result = [];
|
||||
foreach ($messages as $key => $message) {
|
||||
$messageId = $message['locale'].$message['domain'].$message['id'];
|
||||
|
||||
if (!isset($result[$messageId])) {
|
||||
$message['count'] = 1;
|
||||
$message['parameters'] = !empty($message['parameters']) ? [$message['parameters']] : [];
|
||||
$messages[$key]['translation'] = $this->sanitizeString($message['translation']);
|
||||
$result[$messageId] = $message;
|
||||
} else {
|
||||
if (!empty($message['parameters'])) {
|
||||
$result[$messageId]['parameters'][] = $message['parameters'];
|
||||
}
|
||||
|
||||
++$result[$messageId]['count'];
|
||||
}
|
||||
|
||||
unset($messages[$key]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function computeCount(array $messages)
|
||||
{
|
||||
$count = [
|
||||
DataCollectorTranslator::MESSAGE_DEFINED => 0,
|
||||
DataCollectorTranslator::MESSAGE_MISSING => 0,
|
||||
DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK => 0,
|
||||
];
|
||||
|
||||
foreach ($messages as $message) {
|
||||
++$count[$message['state']];
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
private function sanitizeString(string $string, int $length = 80)
|
||||
{
|
||||
$string = trim(preg_replace('/\s+/', ' ', $string));
|
||||
|
||||
if (false !== $encoding = mb_detect_encoding($string, null, true)) {
|
||||
if (mb_strlen($string, $encoding) > $length) {
|
||||
return mb_substr($string, 0, $length - 3, $encoding).'...';
|
||||
}
|
||||
} elseif (\strlen($string) > $length) {
|
||||
return substr($string, 0, $length - 3).'...';
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?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\Component\HttpKernel\CacheWarmer\WarmableInterface;
|
||||
use Symfony\Component\Translation\Exception\InvalidArgumentException;
|
||||
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
|
||||
*/
|
||||
class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface, WarmableInterface
|
||||
{
|
||||
public const MESSAGE_DEFINED = 0;
|
||||
public const MESSAGE_MISSING = 1;
|
||||
public const MESSAGE_EQUALS_FALLBACK = 2;
|
||||
|
||||
private $translator;
|
||||
private $messages = [];
|
||||
|
||||
/**
|
||||
* @param TranslatorInterface&TranslatorBagInterface&LocaleAwareInterface $translator
|
||||
*/
|
||||
public function __construct(TranslatorInterface $translator)
|
||||
{
|
||||
if (!$translator instanceof TranslatorBagInterface || !$translator instanceof LocaleAwareInterface) {
|
||||
throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface, TranslatorBagInterface and LocaleAwareInterface.', get_debug_type($translator)));
|
||||
}
|
||||
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function trans(?string $id, array $parameters = [], ?string $domain = null, ?string $locale = null)
|
||||
{
|
||||
$trans = $this->translator->trans($id = (string) $id, $parameters, $domain, $locale);
|
||||
$this->collectMessage($locale, $domain, $id, $trans, $parameters);
|
||||
|
||||
return $trans;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setLocale(string $locale)
|
||||
{
|
||||
$this->translator->setLocale($locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getLocale()
|
||||
{
|
||||
return $this->translator->getLocale();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCatalogue(?string $locale = null)
|
||||
{
|
||||
return $this->translator->getCatalogue($locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCatalogues(): array
|
||||
{
|
||||
return $this->translator->getCatalogues();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function warmUp(string $cacheDir)
|
||||
{
|
||||
if ($this->translator instanceof WarmableInterface) {
|
||||
return (array) $this->translator->warmUp($cacheDir);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the fallback locales.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFallbackLocales()
|
||||
{
|
||||
if ($this->translator instanceof Translator || method_exists($this->translator, 'getFallbackLocales')) {
|
||||
return $this->translator->getFallbackLocales();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Passes through all unknown calls onto the translator object.
|
||||
*/
|
||||
public function __call(string $method, array $args)
|
||||
{
|
||||
return $this->translator->{$method}(...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getCollectedMessages()
|
||||
{
|
||||
return $this->messages;
|
||||
}
|
||||
|
||||
private function collectMessage(?string $locale, ?string $domain, string $id, string $translation, ?array $parameters = [])
|
||||
{
|
||||
if (null === $domain) {
|
||||
$domain = 'messages';
|
||||
}
|
||||
|
||||
$catalogue = $this->translator->getCatalogue($locale);
|
||||
$locale = $catalogue->getLocale();
|
||||
$fallbackLocale = null;
|
||||
if ($catalogue->defines($id, $domain)) {
|
||||
$state = self::MESSAGE_DEFINED;
|
||||
} elseif ($catalogue->has($id, $domain)) {
|
||||
$state = self::MESSAGE_EQUALS_FALLBACK;
|
||||
|
||||
$fallbackCatalogue = $catalogue->getFallbackCatalogue();
|
||||
while ($fallbackCatalogue) {
|
||||
if ($fallbackCatalogue->defines($id, $domain)) {
|
||||
$fallbackLocale = $fallbackCatalogue->getLocale();
|
||||
break;
|
||||
}
|
||||
$fallbackCatalogue = $fallbackCatalogue->getFallbackCatalogue();
|
||||
}
|
||||
} else {
|
||||
$state = self::MESSAGE_MISSING;
|
||||
}
|
||||
|
||||
$this->messages[] = [
|
||||
'locale' => $locale,
|
||||
'fallbackLocale' => $fallbackLocale,
|
||||
'domain' => $domain,
|
||||
'id' => $id,
|
||||
'translation' => $translation,
|
||||
'parameters' => $parameters,
|
||||
'state' => $state,
|
||||
'transChoiceNumber' => isset($parameters['%count%']) && is_numeric($parameters['%count%']) ? $parameters['%count%'] : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<?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\DependencyInjection;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
/**
|
||||
* Adds tagged translation.formatter services to translation writer.
|
||||
*/
|
||||
class TranslationDumperPass implements CompilerPassInterface
|
||||
{
|
||||
private $writerServiceId;
|
||||
private $dumperTag;
|
||||
|
||||
public function __construct(string $writerServiceId = 'translation.writer', string $dumperTag = 'translation.dumper')
|
||||
{
|
||||
if (1 < \func_num_args()) {
|
||||
trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
|
||||
}
|
||||
|
||||
$this->writerServiceId = $writerServiceId;
|
||||
$this->dumperTag = $dumperTag;
|
||||
}
|
||||
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
if (!$container->hasDefinition($this->writerServiceId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$definition = $container->getDefinition($this->writerServiceId);
|
||||
|
||||
foreach ($container->findTaggedServiceIds($this->dumperTag, true) as $id => $attributes) {
|
||||
$definition->addMethodCall('addDumper', [$attributes[0]['alias'], new Reference($id)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
<?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\DependencyInjection;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
/**
|
||||
* Adds tagged translation.extractor services to translation extractor.
|
||||
*/
|
||||
class TranslationExtractorPass implements CompilerPassInterface
|
||||
{
|
||||
private $extractorServiceId;
|
||||
private $extractorTag;
|
||||
|
||||
public function __construct(string $extractorServiceId = 'translation.extractor', string $extractorTag = 'translation.extractor')
|
||||
{
|
||||
if (0 < \func_num_args()) {
|
||||
trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
|
||||
}
|
||||
|
||||
$this->extractorServiceId = $extractorServiceId;
|
||||
$this->extractorTag = $extractorTag;
|
||||
}
|
||||
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
if (!$container->hasDefinition($this->extractorServiceId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$definition = $container->getDefinition($this->extractorServiceId);
|
||||
|
||||
foreach ($container->findTaggedServiceIds($this->extractorTag, 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)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?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\DependencyInjection;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
class TranslatorPass implements CompilerPassInterface
|
||||
{
|
||||
private $translatorServiceId;
|
||||
private $readerServiceId;
|
||||
private $loaderTag;
|
||||
private $debugCommandServiceId;
|
||||
private $updateCommandServiceId;
|
||||
|
||||
public function __construct(string $translatorServiceId = 'translator.default', string $readerServiceId = 'translation.reader', string $loaderTag = 'translation.loader', string $debugCommandServiceId = 'console.command.translation_debug', string $updateCommandServiceId = 'console.command.translation_extract')
|
||||
{
|
||||
if (0 < \func_num_args()) {
|
||||
trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
|
||||
}
|
||||
|
||||
$this->translatorServiceId = $translatorServiceId;
|
||||
$this->readerServiceId = $readerServiceId;
|
||||
$this->loaderTag = $loaderTag;
|
||||
$this->debugCommandServiceId = $debugCommandServiceId;
|
||||
$this->updateCommandServiceId = $updateCommandServiceId;
|
||||
}
|
||||
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
if (!$container->hasDefinition($this->translatorServiceId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$loaders = [];
|
||||
$loaderRefs = [];
|
||||
foreach ($container->findTaggedServiceIds($this->loaderTag, true) as $id => $attributes) {
|
||||
$loaderRefs[$id] = new Reference($id);
|
||||
$loaders[$id][] = $attributes[0]['alias'];
|
||||
if (isset($attributes[0]['legacy-alias'])) {
|
||||
$loaders[$id][] = $attributes[0]['legacy-alias'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($container->hasDefinition($this->readerServiceId)) {
|
||||
$definition = $container->getDefinition($this->readerServiceId);
|
||||
foreach ($loaders as $id => $formats) {
|
||||
foreach ($formats as $format) {
|
||||
$definition->addMethodCall('addLoader', [$format, $loaderRefs[$id]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$container
|
||||
->findDefinition($this->translatorServiceId)
|
||||
->replaceArgument(0, ServiceLocatorTagPass::register($container, $loaderRefs))
|
||||
->replaceArgument(3, $loaders)
|
||||
;
|
||||
|
||||
if (!$container->hasParameter('twig.default_path')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$paths = array_keys($container->getDefinition('twig.template_iterator')->getArgument(1));
|
||||
if ($container->hasDefinition($this->debugCommandServiceId)) {
|
||||
$definition = $container->getDefinition($this->debugCommandServiceId);
|
||||
$definition->replaceArgument(4, $container->getParameter('twig.default_path'));
|
||||
|
||||
if (\count($definition->getArguments()) > 6) {
|
||||
$definition->replaceArgument(6, $paths);
|
||||
}
|
||||
}
|
||||
if ($container->hasDefinition($this->updateCommandServiceId)) {
|
||||
$definition = $container->getDefinition($this->updateCommandServiceId);
|
||||
$definition->replaceArgument(5, $container->getParameter('twig.default_path'));
|
||||
|
||||
if (\count($definition->getArguments()) > 7) {
|
||||
$definition->replaceArgument(7, $paths);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
<?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\DependencyInjection;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Compiler\AbstractRecursivePass;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\DependencyInjection\ServiceLocator;
|
||||
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\TraceableValueResolver;
|
||||
|
||||
/**
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
class TranslatorPathsPass extends AbstractRecursivePass
|
||||
{
|
||||
private $translatorServiceId;
|
||||
private $debugCommandServiceId;
|
||||
private $updateCommandServiceId;
|
||||
private $resolverServiceId;
|
||||
private $level = 0;
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private $paths = [];
|
||||
|
||||
/**
|
||||
* @var array<int, Definition>
|
||||
*/
|
||||
private $definitions = [];
|
||||
|
||||
/**
|
||||
* @var array<string, array<string, bool>>
|
||||
*/
|
||||
private $controllers = [];
|
||||
|
||||
public function __construct(string $translatorServiceId = 'translator', string $debugCommandServiceId = 'console.command.translation_debug', string $updateCommandServiceId = 'console.command.translation_extract', string $resolverServiceId = 'argument_resolver.service')
|
||||
{
|
||||
if (0 < \func_num_args()) {
|
||||
trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
|
||||
}
|
||||
|
||||
$this->translatorServiceId = $translatorServiceId;
|
||||
$this->debugCommandServiceId = $debugCommandServiceId;
|
||||
$this->updateCommandServiceId = $updateCommandServiceId;
|
||||
$this->resolverServiceId = $resolverServiceId;
|
||||
}
|
||||
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
if (!$container->hasDefinition($this->translatorServiceId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->findControllerArguments($container) as $controller => $argument) {
|
||||
$id = substr($controller, 0, strpos($controller, ':') ?: \strlen($controller));
|
||||
if ($container->hasDefinition($id)) {
|
||||
[$locatorRef] = $argument->getValues();
|
||||
$this->controllers[(string) $locatorRef][$container->getDefinition($id)->getClass()] = true;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
parent::process($container);
|
||||
|
||||
$paths = [];
|
||||
foreach ($this->paths as $class => $_) {
|
||||
if (($r = $container->getReflectionClass($class)) && !$r->isInterface()) {
|
||||
$paths[] = $r->getFileName();
|
||||
foreach ($r->getTraits() as $trait) {
|
||||
$paths[] = $trait->getFileName();
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($paths) {
|
||||
if ($container->hasDefinition($this->debugCommandServiceId)) {
|
||||
$definition = $container->getDefinition($this->debugCommandServiceId);
|
||||
$definition->replaceArgument(6, array_merge($definition->getArgument(6), $paths));
|
||||
}
|
||||
if ($container->hasDefinition($this->updateCommandServiceId)) {
|
||||
$definition = $container->getDefinition($this->updateCommandServiceId);
|
||||
$definition->replaceArgument(7, array_merge($definition->getArgument(7), $paths));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
$this->level = 0;
|
||||
$this->paths = [];
|
||||
$this->definitions = [];
|
||||
}
|
||||
}
|
||||
|
||||
protected function processValue($value, bool $isRoot = false)
|
||||
{
|
||||
if ($value instanceof Reference) {
|
||||
if ((string) $value === $this->translatorServiceId) {
|
||||
for ($i = $this->level - 1; $i >= 0; --$i) {
|
||||
$class = $this->definitions[$i]->getClass();
|
||||
|
||||
if (ServiceLocator::class === $class) {
|
||||
if (!isset($this->controllers[$this->currentId])) {
|
||||
continue;
|
||||
}
|
||||
foreach ($this->controllers[$this->currentId] as $class => $_) {
|
||||
$this->paths[$class] = true;
|
||||
}
|
||||
} else {
|
||||
$this->paths[$class] = true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($value instanceof Definition) {
|
||||
$this->definitions[$this->level++] = $value;
|
||||
$value = parent::processValue($value, $isRoot);
|
||||
unset($this->definitions[--$this->level]);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
return parent::processValue($value, $isRoot);
|
||||
}
|
||||
|
||||
private function findControllerArguments(ContainerBuilder $container): array
|
||||
{
|
||||
if (!$container->has($this->resolverServiceId)) {
|
||||
return [];
|
||||
}
|
||||
$resolverDef = $container->findDefinition($this->resolverServiceId);
|
||||
|
||||
if (TraceableValueResolver::class === $resolverDef->getClass()) {
|
||||
$resolverDef = $container->getDefinition($resolverDef->getArgument(0));
|
||||
}
|
||||
|
||||
$argument = $resolverDef->getArgument(0);
|
||||
if ($argument instanceof Reference) {
|
||||
$argument = $container->getDefinition($argument);
|
||||
}
|
||||
|
||||
return $argument->getArgument(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?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\Dumper;
|
||||
|
||||
use Symfony\Component\Translation\MessageCatalogue;
|
||||
|
||||
/**
|
||||
* CsvFileDumper generates a csv formatted string representation of a message catalogue.
|
||||
*
|
||||
* @author Stealth35
|
||||
*/
|
||||
class CsvFileDumper extends FileDumper
|
||||
{
|
||||
private $delimiter = ';';
|
||||
private $enclosure = '"';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = [])
|
||||
{
|
||||
$handle = fopen('php://memory', 'r+');
|
||||
|
||||
foreach ($messages->all($domain) as $source => $target) {
|
||||
fputcsv($handle, [$source, $target], $this->delimiter, $this->enclosure);
|
||||
}
|
||||
|
||||
rewind($handle);
|
||||
$output = stream_get_contents($handle);
|
||||
fclose($handle);
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the delimiter and escape character for CSV.
|
||||
*/
|
||||
public function setCsvControl(string $delimiter = ';', string $enclosure = '"')
|
||||
{
|
||||
$this->delimiter = $delimiter;
|
||||
$this->enclosure = $enclosure;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExtension()
|
||||
{
|
||||
return 'csv';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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\Dumper;
|
||||
|
||||
use Symfony\Component\Translation\MessageCatalogue;
|
||||
|
||||
/**
|
||||
* DumperInterface is the interface implemented by all translation dumpers.
|
||||
* There is no common option.
|
||||
*
|
||||
* @author Michel Salib <michelsalib@hotmail.com>
|
||||
*/
|
||||
interface DumperInterface
|
||||
{
|
||||
/**
|
||||
* Dumps the message catalogue.
|
||||
*
|
||||
* @param array $options Options that are used by the dumper
|
||||
*/
|
||||
public function dump(MessageCatalogue $messages, array $options = []);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?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\Dumper;
|
||||
|
||||
use Symfony\Component\Translation\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Translation\Exception\RuntimeException;
|
||||
use Symfony\Component\Translation\MessageCatalogue;
|
||||
|
||||
/**
|
||||
* FileDumper is an implementation of DumperInterface that dump a message catalogue to file(s).
|
||||
*
|
||||
* Options:
|
||||
* - path (mandatory): the directory where the files should be saved
|
||||
*
|
||||
* @author Michel Salib <michelsalib@hotmail.com>
|
||||
*/
|
||||
abstract class FileDumper implements DumperInterface
|
||||
{
|
||||
/**
|
||||
* A template for the relative paths to files.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $relativePathTemplate = '%domain%.%locale%.%extension%';
|
||||
|
||||
/**
|
||||
* Sets the template for the relative paths to files.
|
||||
*
|
||||
* @param string $relativePathTemplate A template for the relative paths to files
|
||||
*/
|
||||
public function setRelativePathTemplate(string $relativePathTemplate)
|
||||
{
|
||||
$this->relativePathTemplate = $relativePathTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function dump(MessageCatalogue $messages, array $options = [])
|
||||
{
|
||||
if (!\array_key_exists('path', $options)) {
|
||||
throw new InvalidArgumentException('The file dumper needs a path option.');
|
||||
}
|
||||
|
||||
// save a file for each domain
|
||||
foreach ($messages->getDomains() as $domain) {
|
||||
$fullpath = $options['path'].'/'.$this->getRelativePath($domain, $messages->getLocale());
|
||||
if (!file_exists($fullpath)) {
|
||||
$directory = \dirname($fullpath);
|
||||
if (!file_exists($directory) && !@mkdir($directory, 0777, true)) {
|
||||
throw new RuntimeException(sprintf('Unable to create directory "%s".', $directory));
|
||||
}
|
||||
}
|
||||
|
||||
$intlDomain = $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX;
|
||||
$intlMessages = $messages->all($intlDomain);
|
||||
|
||||
if ($intlMessages) {
|
||||
$intlPath = $options['path'].'/'.$this->getRelativePath($intlDomain, $messages->getLocale());
|
||||
file_put_contents($intlPath, $this->formatCatalogue($messages, $intlDomain, $options));
|
||||
|
||||
$messages->replace([], $intlDomain);
|
||||
|
||||
try {
|
||||
if ($messages->all($domain)) {
|
||||
file_put_contents($fullpath, $this->formatCatalogue($messages, $domain, $options));
|
||||
}
|
||||
continue;
|
||||
} finally {
|
||||
$messages->replace($intlMessages, $intlDomain);
|
||||
}
|
||||
}
|
||||
|
||||
file_put_contents($fullpath, $this->formatCatalogue($messages, $domain, $options));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a domain of a message catalogue to its string representation.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []);
|
||||
|
||||
/**
|
||||
* Gets the file extension of the dumper.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function getExtension();
|
||||
|
||||
/**
|
||||
* Gets the relative file path using the template.
|
||||
*/
|
||||
private function getRelativePath(string $domain, string $locale): string
|
||||
{
|
||||
return strtr($this->relativePathTemplate, [
|
||||
'%domain%' => $domain,
|
||||
'%locale%' => $locale,
|
||||
'%extension%' => $this->getExtension(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?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\Dumper;
|
||||
|
||||
use Symfony\Component\Translation\MessageCatalogue;
|
||||
|
||||
/**
|
||||
* IcuResDumper generates an ICU ResourceBundle formatted string representation of a message catalogue.
|
||||
*
|
||||
* @author Stealth35
|
||||
*/
|
||||
class IcuResFileDumper extends FileDumper
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $relativePathTemplate = '%domain%/%locale%.%extension%';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = [])
|
||||
{
|
||||
$data = $indexes = $resources = '';
|
||||
|
||||
foreach ($messages->all($domain) as $source => $target) {
|
||||
$indexes .= pack('v', \strlen($data) + 28);
|
||||
$data .= $source."\0";
|
||||
}
|
||||
|
||||
$data .= $this->writePadding($data);
|
||||
|
||||
$keyTop = $this->getPosition($data);
|
||||
|
||||
foreach ($messages->all($domain) as $source => $target) {
|
||||
$resources .= pack('V', $this->getPosition($data));
|
||||
|
||||
$data .= pack('V', \strlen($target))
|
||||
.mb_convert_encoding($target."\0", 'UTF-16LE', 'UTF-8')
|
||||
.$this->writePadding($data)
|
||||
;
|
||||
}
|
||||
|
||||
$resOffset = $this->getPosition($data);
|
||||
|
||||
$data .= pack('v', \count($messages->all($domain)))
|
||||
.$indexes
|
||||
.$this->writePadding($data)
|
||||
.$resources
|
||||
;
|
||||
|
||||
$bundleTop = $this->getPosition($data);
|
||||
|
||||
$root = pack('V7',
|
||||
$resOffset + (2 << 28), // Resource Offset + Resource Type
|
||||
6, // Index length
|
||||
$keyTop, // Index keys top
|
||||
$bundleTop, // Index resources top
|
||||
$bundleTop, // Index bundle top
|
||||
\count($messages->all($domain)), // Index max table length
|
||||
0 // Index attributes
|
||||
);
|
||||
|
||||
$header = pack('vC2v4C12@32',
|
||||
32, // Header size
|
||||
0xDA, 0x27, // Magic number 1 and 2
|
||||
20, 0, 0, 2, // Rest of the header, ..., Size of a char
|
||||
0x52, 0x65, 0x73, 0x42, // Data format identifier
|
||||
1, 2, 0, 0, // Data version
|
||||
1, 4, 0, 0 // Unicode version
|
||||
);
|
||||
|
||||
return $header.$root.$data;
|
||||
}
|
||||
|
||||
private function writePadding(string $data): ?string
|
||||
{
|
||||
$padding = \strlen($data) % 4;
|
||||
|
||||
return $padding ? str_repeat("\xAA", 4 - $padding) : null;
|
||||
}
|
||||
|
||||
private function getPosition(string $data)
|
||||
{
|
||||
return (\strlen($data) + 28) / 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExtension()
|
||||
{
|
||||
return 'res';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?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\Dumper;
|
||||
|
||||
use Symfony\Component\Translation\MessageCatalogue;
|
||||
|
||||
/**
|
||||
* IniFileDumper generates an ini formatted string representation of a message catalogue.
|
||||
*
|
||||
* @author Stealth35
|
||||
*/
|
||||
class IniFileDumper extends FileDumper
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = [])
|
||||
{
|
||||
$output = '';
|
||||
|
||||
foreach ($messages->all($domain) as $source => $target) {
|
||||
$escapeTarget = str_replace('"', '\"', $target);
|
||||
$output .= $source.'="'.$escapeTarget."\"\n";
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExtension()
|
||||
{
|
||||
return 'ini';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?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\Dumper;
|
||||
|
||||
use Symfony\Component\Translation\MessageCatalogue;
|
||||
|
||||
/**
|
||||
* JsonFileDumper generates an json formatted string representation of a message catalogue.
|
||||
*
|
||||
* @author singles
|
||||
*/
|
||||
class JsonFileDumper extends FileDumper
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = [])
|
||||
{
|
||||
$flags = $options['json_encoding'] ?? \JSON_PRETTY_PRINT;
|
||||
|
||||
return json_encode($messages->all($domain), $flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExtension()
|
||||
{
|
||||
return 'json';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?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\Dumper;
|
||||
|
||||
use Symfony\Component\Translation\Loader\MoFileLoader;
|
||||
use Symfony\Component\Translation\MessageCatalogue;
|
||||
|
||||
/**
|
||||
* MoFileDumper generates a gettext formatted string representation of a message catalogue.
|
||||
*
|
||||
* @author Stealth35
|
||||
*/
|
||||
class MoFileDumper extends FileDumper
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = [])
|
||||
{
|
||||
$sources = $targets = $sourceOffsets = $targetOffsets = '';
|
||||
$offsets = [];
|
||||
$size = 0;
|
||||
|
||||
foreach ($messages->all($domain) as $source => $target) {
|
||||
$offsets[] = array_map('strlen', [$sources, $source, $targets, $target]);
|
||||
$sources .= "\0".$source;
|
||||
$targets .= "\0".$target;
|
||||
++$size;
|
||||
}
|
||||
|
||||
$header = [
|
||||
'magicNumber' => MoFileLoader::MO_LITTLE_ENDIAN_MAGIC,
|
||||
'formatRevision' => 0,
|
||||
'count' => $size,
|
||||
'offsetId' => MoFileLoader::MO_HEADER_SIZE,
|
||||
'offsetTranslated' => MoFileLoader::MO_HEADER_SIZE + (8 * $size),
|
||||
'sizeHashes' => 0,
|
||||
'offsetHashes' => MoFileLoader::MO_HEADER_SIZE + (16 * $size),
|
||||
];
|
||||
|
||||
$sourcesSize = \strlen($sources);
|
||||
$sourcesStart = $header['offsetHashes'] + 1;
|
||||
|
||||
foreach ($offsets as $offset) {
|
||||
$sourceOffsets .= $this->writeLong($offset[1])
|
||||
.$this->writeLong($offset[0] + $sourcesStart);
|
||||
$targetOffsets .= $this->writeLong($offset[3])
|
||||
.$this->writeLong($offset[2] + $sourcesStart + $sourcesSize);
|
||||
}
|
||||
|
||||
$output = implode('', array_map([$this, 'writeLong'], $header))
|
||||
.$sourceOffsets
|
||||
.$targetOffsets
|
||||
.$sources
|
||||
.$targets
|
||||
;
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExtension()
|
||||
{
|
||||
return 'mo';
|
||||
}
|
||||
|
||||
private function writeLong($str): string
|
||||
{
|
||||
return pack('V*', $str);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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\Dumper;
|
||||
|
||||
use Symfony\Component\Translation\MessageCatalogue;
|
||||
|
||||
/**
|
||||
* PhpFileDumper generates PHP files from a message catalogue.
|
||||
*
|
||||
* @author Michel Salib <michelsalib@hotmail.com>
|
||||
*/
|
||||
class PhpFileDumper extends FileDumper
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = [])
|
||||
{
|
||||
return "<?php\n\nreturn ".var_export($messages->all($domain), true).";\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExtension()
|
||||
{
|
||||
return 'php';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?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\Dumper;
|
||||
|
||||
use Symfony\Component\Translation\MessageCatalogue;
|
||||
|
||||
/**
|
||||
* PoFileDumper generates a gettext formatted string representation of a message catalogue.
|
||||
*
|
||||
* @author Stealth35
|
||||
*/
|
||||
class PoFileDumper extends FileDumper
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = [])
|
||||
{
|
||||
$output = 'msgid ""'."\n";
|
||||
$output .= 'msgstr ""'."\n";
|
||||
$output .= '"Content-Type: text/plain; charset=UTF-8\n"'."\n";
|
||||
$output .= '"Content-Transfer-Encoding: 8bit\n"'."\n";
|
||||
$output .= '"Language: '.$messages->getLocale().'\n"'."\n";
|
||||
$output .= "\n";
|
||||
|
||||
$newLine = false;
|
||||
foreach ($messages->all($domain) as $source => $target) {
|
||||
if ($newLine) {
|
||||
$output .= "\n";
|
||||
} else {
|
||||
$newLine = true;
|
||||
}
|
||||
$metadata = $messages->getMetadata($source, $domain);
|
||||
|
||||
if (isset($metadata['comments'])) {
|
||||
$output .= $this->formatComments($metadata['comments']);
|
||||
}
|
||||
if (isset($metadata['flags'])) {
|
||||
$output .= $this->formatComments(implode(',', (array) $metadata['flags']), ',');
|
||||
}
|
||||
if (isset($metadata['sources'])) {
|
||||
$output .= $this->formatComments(implode(' ', (array) $metadata['sources']), ':');
|
||||
}
|
||||
|
||||
$sourceRules = $this->getStandardRules($source);
|
||||
$targetRules = $this->getStandardRules($target);
|
||||
if (2 == \count($sourceRules) && [] !== $targetRules) {
|
||||
$output .= sprintf('msgid "%s"'."\n", $this->escape($sourceRules[0]));
|
||||
$output .= sprintf('msgid_plural "%s"'."\n", $this->escape($sourceRules[1]));
|
||||
foreach ($targetRules as $i => $targetRule) {
|
||||
$output .= sprintf('msgstr[%d] "%s"'."\n", $i, $this->escape($targetRule));
|
||||
}
|
||||
} else {
|
||||
$output .= sprintf('msgid "%s"'."\n", $this->escape($source));
|
||||
$output .= sprintf('msgstr "%s"'."\n", $this->escape($target));
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
private function getStandardRules(string $id)
|
||||
{
|
||||
// Partly copied from TranslatorTrait::trans.
|
||||
$parts = [];
|
||||
if (preg_match('/^\|++$/', $id)) {
|
||||
$parts = explode('|', $id);
|
||||
} elseif (preg_match_all('/(?:\|\||[^\|])++/', $id, $matches)) {
|
||||
$parts = $matches[0];
|
||||
}
|
||||
|
||||
$intervalRegexp = <<<'EOF'
|
||||
/^(?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;
|
||||
|
||||
$standardRules = [];
|
||||
foreach ($parts as $part) {
|
||||
$part = trim(str_replace('||', '|', $part));
|
||||
|
||||
if (preg_match($intervalRegexp, $part)) {
|
||||
// Explicit rule is not a standard rule.
|
||||
return [];
|
||||
} else {
|
||||
$standardRules[] = $part;
|
||||
}
|
||||
}
|
||||
|
||||
return $standardRules;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExtension()
|
||||
{
|
||||
return 'po';
|
||||
}
|
||||
|
||||
private function escape(string $str): string
|
||||
{
|
||||
return addcslashes($str, "\0..\37\42\134");
|
||||
}
|
||||
|
||||
private function formatComments($comments, string $prefix = ''): ?string
|
||||
{
|
||||
$output = null;
|
||||
|
||||
foreach ((array) $comments as $comment) {
|
||||
$output .= sprintf('#%s %s'."\n", $prefix, $comment);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?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\Dumper;
|
||||
|
||||
use Symfony\Component\Translation\MessageCatalogue;
|
||||
|
||||
/**
|
||||
* QtFileDumper generates ts files from a message catalogue.
|
||||
*
|
||||
* @author Benjamin Eberlei <kontakt@beberlei.de>
|
||||
*/
|
||||
class QtFileDumper extends FileDumper
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = [])
|
||||
{
|
||||
$dom = new \DOMDocument('1.0', 'utf-8');
|
||||
$dom->formatOutput = true;
|
||||
$ts = $dom->appendChild($dom->createElement('TS'));
|
||||
$context = $ts->appendChild($dom->createElement('context'));
|
||||
$context->appendChild($dom->createElement('name', $domain));
|
||||
|
||||
foreach ($messages->all($domain) as $source => $target) {
|
||||
$message = $context->appendChild($dom->createElement('message'));
|
||||
$metadata = $messages->getMetadata($source, $domain);
|
||||
if (isset($metadata['sources'])) {
|
||||
foreach ((array) $metadata['sources'] as $location) {
|
||||
$loc = explode(':', $location, 2);
|
||||
$location = $message->appendChild($dom->createElement('location'));
|
||||
$location->setAttribute('filename', $loc[0]);
|
||||
if (isset($loc[1])) {
|
||||
$location->setAttribute('line', $loc[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
$message->appendChild($dom->createElement('source', $source));
|
||||
$message->appendChild($dom->createElement('translation', $target));
|
||||
}
|
||||
|
||||
return $dom->saveXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExtension()
|
||||
{
|
||||
return 'ts';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
<?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\Dumper;
|
||||
|
||||
use Symfony\Component\Translation\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Translation\MessageCatalogue;
|
||||
|
||||
/**
|
||||
* XliffFileDumper generates xliff files from a message catalogue.
|
||||
*
|
||||
* @author Michel Salib <michelsalib@hotmail.com>
|
||||
*/
|
||||
class XliffFileDumper extends FileDumper
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = [])
|
||||
{
|
||||
$xliffVersion = '1.2';
|
||||
if (\array_key_exists('xliff_version', $options)) {
|
||||
$xliffVersion = $options['xliff_version'];
|
||||
}
|
||||
|
||||
if (\array_key_exists('default_locale', $options)) {
|
||||
$defaultLocale = $options['default_locale'];
|
||||
} else {
|
||||
$defaultLocale = \Locale::getDefault();
|
||||
}
|
||||
|
||||
if ('1.2' === $xliffVersion) {
|
||||
return $this->dumpXliff1($defaultLocale, $messages, $domain, $options);
|
||||
}
|
||||
if ('2.0' === $xliffVersion) {
|
||||
return $this->dumpXliff2($defaultLocale, $messages, $domain);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf('No support implemented for dumping XLIFF version "%s".', $xliffVersion));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExtension()
|
||||
{
|
||||
return 'xlf';
|
||||
}
|
||||
|
||||
private function dumpXliff1(string $defaultLocale, MessageCatalogue $messages, ?string $domain, array $options = [])
|
||||
{
|
||||
$toolInfo = ['tool-id' => 'symfony', 'tool-name' => 'Symfony'];
|
||||
if (\array_key_exists('tool_info', $options)) {
|
||||
$toolInfo = array_merge($toolInfo, $options['tool_info']);
|
||||
}
|
||||
|
||||
$dom = new \DOMDocument('1.0', 'utf-8');
|
||||
$dom->formatOutput = true;
|
||||
|
||||
$xliff = $dom->appendChild($dom->createElement('xliff'));
|
||||
$xliff->setAttribute('version', '1.2');
|
||||
$xliff->setAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:1.2');
|
||||
|
||||
$xliffFile = $xliff->appendChild($dom->createElement('file'));
|
||||
$xliffFile->setAttribute('source-language', str_replace('_', '-', $defaultLocale));
|
||||
$xliffFile->setAttribute('target-language', str_replace('_', '-', $messages->getLocale()));
|
||||
$xliffFile->setAttribute('datatype', 'plaintext');
|
||||
$xliffFile->setAttribute('original', 'file.ext');
|
||||
|
||||
$xliffHead = $xliffFile->appendChild($dom->createElement('header'));
|
||||
$xliffTool = $xliffHead->appendChild($dom->createElement('tool'));
|
||||
foreach ($toolInfo as $id => $value) {
|
||||
$xliffTool->setAttribute($id, $value);
|
||||
}
|
||||
|
||||
$xliffBody = $xliffFile->appendChild($dom->createElement('body'));
|
||||
foreach ($messages->all($domain) as $source => $target) {
|
||||
$translation = $dom->createElement('trans-unit');
|
||||
|
||||
$translation->setAttribute('id', strtr(substr(base64_encode(hash('sha256', $source, true)), 0, 7), '/+', '._'));
|
||||
$translation->setAttribute('resname', $source);
|
||||
|
||||
$s = $translation->appendChild($dom->createElement('source'));
|
||||
$s->appendChild($dom->createTextNode($source));
|
||||
|
||||
// Does the target contain characters requiring a CDATA section?
|
||||
$text = 1 === preg_match('/[&<>]/', $target) ? $dom->createCDATASection($target) : $dom->createTextNode($target);
|
||||
|
||||
$targetElement = $dom->createElement('target');
|
||||
$metadata = $messages->getMetadata($source, $domain);
|
||||
if ($this->hasMetadataArrayInfo('target-attributes', $metadata)) {
|
||||
foreach ($metadata['target-attributes'] as $name => $value) {
|
||||
$targetElement->setAttribute($name, $value);
|
||||
}
|
||||
}
|
||||
$t = $translation->appendChild($targetElement);
|
||||
$t->appendChild($text);
|
||||
|
||||
if ($this->hasMetadataArrayInfo('notes', $metadata)) {
|
||||
foreach ($metadata['notes'] as $note) {
|
||||
if (!isset($note['content'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$n = $translation->appendChild($dom->createElement('note'));
|
||||
$n->appendChild($dom->createTextNode($note['content']));
|
||||
|
||||
if (isset($note['priority'])) {
|
||||
$n->setAttribute('priority', $note['priority']);
|
||||
}
|
||||
|
||||
if (isset($note['from'])) {
|
||||
$n->setAttribute('from', $note['from']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$xliffBody->appendChild($translation);
|
||||
}
|
||||
|
||||
return $dom->saveXML();
|
||||
}
|
||||
|
||||
private function dumpXliff2(string $defaultLocale, MessageCatalogue $messages, ?string $domain)
|
||||
{
|
||||
$dom = new \DOMDocument('1.0', 'utf-8');
|
||||
$dom->formatOutput = true;
|
||||
|
||||
$xliff = $dom->appendChild($dom->createElement('xliff'));
|
||||
$xliff->setAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:2.0');
|
||||
$xliff->setAttribute('version', '2.0');
|
||||
$xliff->setAttribute('srcLang', str_replace('_', '-', $defaultLocale));
|
||||
$xliff->setAttribute('trgLang', str_replace('_', '-', $messages->getLocale()));
|
||||
|
||||
$xliffFile = $xliff->appendChild($dom->createElement('file'));
|
||||
if (str_ends_with($domain, MessageCatalogue::INTL_DOMAIN_SUFFIX)) {
|
||||
$xliffFile->setAttribute('id', substr($domain, 0, -\strlen(MessageCatalogue::INTL_DOMAIN_SUFFIX)).'.'.$messages->getLocale());
|
||||
} else {
|
||||
$xliffFile->setAttribute('id', $domain.'.'.$messages->getLocale());
|
||||
}
|
||||
|
||||
foreach ($messages->all($domain) as $source => $target) {
|
||||
$translation = $dom->createElement('unit');
|
||||
$translation->setAttribute('id', strtr(substr(base64_encode(hash('sha256', $source, true)), 0, 7), '/+', '._'));
|
||||
|
||||
if (\strlen($source) <= 80) {
|
||||
$translation->setAttribute('name', $source);
|
||||
}
|
||||
|
||||
$metadata = $messages->getMetadata($source, $domain);
|
||||
|
||||
// Add notes section
|
||||
if ($this->hasMetadataArrayInfo('notes', $metadata)) {
|
||||
$notesElement = $dom->createElement('notes');
|
||||
foreach ($metadata['notes'] as $note) {
|
||||
$n = $dom->createElement('note');
|
||||
$n->appendChild($dom->createTextNode($note['content'] ?? ''));
|
||||
unset($note['content']);
|
||||
|
||||
foreach ($note as $name => $value) {
|
||||
$n->setAttribute($name, $value);
|
||||
}
|
||||
$notesElement->appendChild($n);
|
||||
}
|
||||
$translation->appendChild($notesElement);
|
||||
}
|
||||
|
||||
$segment = $translation->appendChild($dom->createElement('segment'));
|
||||
|
||||
$s = $segment->appendChild($dom->createElement('source'));
|
||||
$s->appendChild($dom->createTextNode($source));
|
||||
|
||||
// Does the target contain characters requiring a CDATA section?
|
||||
$text = 1 === preg_match('/[&<>]/', $target) ? $dom->createCDATASection($target) : $dom->createTextNode($target);
|
||||
|
||||
$targetElement = $dom->createElement('target');
|
||||
if ($this->hasMetadataArrayInfo('target-attributes', $metadata)) {
|
||||
foreach ($metadata['target-attributes'] as $name => $value) {
|
||||
$targetElement->setAttribute($name, $value);
|
||||
}
|
||||
}
|
||||
$t = $segment->appendChild($targetElement);
|
||||
$t->appendChild($text);
|
||||
|
||||
$xliffFile->appendChild($translation);
|
||||
}
|
||||
|
||||
return $dom->saveXML();
|
||||
}
|
||||
|
||||
private function hasMetadataArrayInfo(string $key, ?array $metadata = null): bool
|
||||
{
|
||||
return is_iterable($metadata[$key] ?? null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?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\Dumper;
|
||||
|
||||
use Symfony\Component\Translation\Exception\LogicException;
|
||||
use Symfony\Component\Translation\MessageCatalogue;
|
||||
use Symfony\Component\Translation\Util\ArrayConverter;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
/**
|
||||
* YamlFileDumper generates yaml files from a message catalogue.
|
||||
*
|
||||
* @author Michel Salib <michelsalib@hotmail.com>
|
||||
*/
|
||||
class YamlFileDumper extends FileDumper
|
||||
{
|
||||
private $extension;
|
||||
|
||||
public function __construct(string $extension = 'yml')
|
||||
{
|
||||
$this->extension = $extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = [])
|
||||
{
|
||||
if (!class_exists(Yaml::class)) {
|
||||
throw new LogicException('Dumping translations in the YAML format requires the Symfony Yaml component.');
|
||||
}
|
||||
|
||||
$data = $messages->all($domain);
|
||||
|
||||
if (isset($options['as_tree']) && $options['as_tree']) {
|
||||
$data = ArrayConverter::expandToTree($data);
|
||||
}
|
||||
|
||||
if (isset($options['inline']) && ($inline = (int) $options['inline']) > 0) {
|
||||
return Yaml::dump($data, $inline);
|
||||
}
|
||||
|
||||
return Yaml::dump($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExtension()
|
||||
{
|
||||
return $this->extension;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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\Exception;
|
||||
|
||||
/**
|
||||
* Exception interface for all exceptions thrown by the component.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
interface ExceptionInterface extends \Throwable
|
||||
{
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Translation\Exception;
|
||||
|
||||
class IncompleteDsnException extends InvalidArgumentException
|
||||
{
|
||||
public function __construct(string $message, ?string $dsn = null, ?\Throwable $previous = null)
|
||||
{
|
||||
if ($dsn) {
|
||||
$message = sprintf('Invalid "%s" provider DSN: ', $dsn).$message;
|
||||
}
|
||||
|
||||
parent::__construct($message, 0, $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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\Exception;
|
||||
|
||||
/**
|
||||
* Base InvalidArgumentException for the Translation component.
|
||||
*
|
||||
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
|
||||
*/
|
||||
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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\Exception;
|
||||
|
||||
/**
|
||||
* Thrown when a resource cannot be loaded.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class InvalidResourceException extends \InvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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\Exception;
|
||||
|
||||
/**
|
||||
* Base LogicException for Translation component.
|
||||
*
|
||||
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
|
||||
*/
|
||||
class LogicException extends \LogicException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
+25
@@ -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.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Translation\Exception;
|
||||
|
||||
/**
|
||||
* @author Oskar Stark <oskarstark@googlemail.com>
|
||||
*/
|
||||
class MissingRequiredOptionException extends IncompleteDsnException
|
||||
{
|
||||
public function __construct(string $option, ?string $dsn = null, ?\Throwable $previous = null)
|
||||
{
|
||||
$message = sprintf('The option "%s" is required but missing.', $option);
|
||||
|
||||
parent::__construct($message, $dsn, $previous);
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<?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\Exception;
|
||||
|
||||
/**
|
||||
* Thrown when a resource does not exist.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class NotFoundResourceException extends \InvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -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 Symfony\Component\Translation\Exception;
|
||||
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class ProviderException extends RuntimeException implements ProviderExceptionInterface
|
||||
{
|
||||
private $response;
|
||||
private $debug;
|
||||
|
||||
public function __construct(string $message, ResponseInterface $response, int $code = 0, ?\Exception $previous = null)
|
||||
{
|
||||
$this->response = $response;
|
||||
$this->debug = $response->getInfo('debug') ?? '';
|
||||
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public function getResponse(): ResponseInterface
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
public function getDebug(): string
|
||||
{
|
||||
return $this->debug;
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?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\Exception;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
interface ProviderExceptionInterface extends ExceptionInterface
|
||||
{
|
||||
/*
|
||||
* Returns debug info coming from the Symfony\Contracts\HttpClient\ResponseInterface
|
||||
*/
|
||||
public function getDebug(): string;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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\Exception;
|
||||
|
||||
/**
|
||||
* Base RuntimeException for the Translation component.
|
||||
*
|
||||
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
|
||||
*/
|
||||
class RuntimeException extends \RuntimeException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
<?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\Exception;
|
||||
|
||||
use Symfony\Component\Translation\Bridge;
|
||||
use Symfony\Component\Translation\Provider\Dsn;
|
||||
|
||||
class UnsupportedSchemeException extends LogicException
|
||||
{
|
||||
private const SCHEME_TO_PACKAGE_MAP = [
|
||||
'crowdin' => [
|
||||
'class' => Bridge\Crowdin\CrowdinProviderFactory::class,
|
||||
'package' => 'symfony/crowdin-translation-provider',
|
||||
],
|
||||
'loco' => [
|
||||
'class' => Bridge\Loco\LocoProviderFactory::class,
|
||||
'package' => 'symfony/loco-translation-provider',
|
||||
],
|
||||
'lokalise' => [
|
||||
'class' => Bridge\Lokalise\LokaliseProviderFactory::class,
|
||||
'package' => 'symfony/lokalise-translation-provider',
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(Dsn $dsn, ?string $name = null, array $supported = [])
|
||||
{
|
||||
$provider = $dsn->getScheme();
|
||||
if (false !== $pos = strpos($provider, '+')) {
|
||||
$provider = substr($provider, 0, $pos);
|
||||
}
|
||||
$package = self::SCHEME_TO_PACKAGE_MAP[$provider] ?? null;
|
||||
if ($package && !class_exists($package['class'])) {
|
||||
parent::__construct(sprintf('Unable to synchronize translations via "%s" as the provider is not installed; try running "composer require %s".', $provider, $package['package']));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$message = sprintf('The "%s" scheme is not supported', $dsn->getScheme());
|
||||
if ($name && $supported) {
|
||||
$message .= sprintf('; supported schemes for translation provider "%s" are: "%s"', $name, implode('", "', $supported));
|
||||
}
|
||||
|
||||
parent::__construct($message.'.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?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\Extractor;
|
||||
|
||||
use Symfony\Component\Translation\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Base class used by classes that extract translation messages from files.
|
||||
*
|
||||
* @author Marcos D. Sánchez <marcosdsanchez@gmail.com>
|
||||
*/
|
||||
abstract class AbstractFileExtractor
|
||||
{
|
||||
/**
|
||||
* @param string|iterable $resource Files, a file or a directory
|
||||
*
|
||||
* @return iterable
|
||||
*/
|
||||
protected function extractFiles($resource)
|
||||
{
|
||||
if (is_iterable($resource)) {
|
||||
$files = [];
|
||||
foreach ($resource as $file) {
|
||||
if ($this->canBeExtracted($file)) {
|
||||
$files[] = $this->toSplFileInfo($file);
|
||||
}
|
||||
}
|
||||
} elseif (is_file($resource)) {
|
||||
$files = $this->canBeExtracted($resource) ? [$this->toSplFileInfo($resource)] : [];
|
||||
} else {
|
||||
$files = $this->extractFromDirectory($resource);
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
private function toSplFileInfo(string $file): \SplFileInfo
|
||||
{
|
||||
return new \SplFileInfo($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function isFile(string $file)
|
||||
{
|
||||
if (!is_file($file)) {
|
||||
throw new InvalidArgumentException(sprintf('The "%s" file does not exist.', $file));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
abstract protected function canBeExtracted(string $file);
|
||||
|
||||
/**
|
||||
* @param string|array $resource Files, a file or a directory
|
||||
*
|
||||
* @return iterable
|
||||
*/
|
||||
abstract protected function extractFromDirectory($resource);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?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\Extractor;
|
||||
|
||||
use Symfony\Component\Translation\MessageCatalogue;
|
||||
|
||||
/**
|
||||
* ChainExtractor extracts translation messages from template files.
|
||||
*
|
||||
* @author Michel Salib <michelsalib@hotmail.com>
|
||||
*/
|
||||
class ChainExtractor implements ExtractorInterface
|
||||
{
|
||||
/**
|
||||
* The extractors.
|
||||
*
|
||||
* @var ExtractorInterface[]
|
||||
*/
|
||||
private $extractors = [];
|
||||
|
||||
/**
|
||||
* Adds a loader to the translation extractor.
|
||||
*/
|
||||
public function addExtractor(string $format, ExtractorInterface $extractor)
|
||||
{
|
||||
$this->extractors[$format] = $extractor;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setPrefix(string $prefix)
|
||||
{
|
||||
foreach ($this->extractors as $extractor) {
|
||||
$extractor->setPrefix($prefix);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function extract($directory, MessageCatalogue $catalogue)
|
||||
{
|
||||
foreach ($this->extractors as $extractor) {
|
||||
$extractor->extract($directory, $catalogue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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\Extractor;
|
||||
|
||||
use Symfony\Component\Translation\MessageCatalogue;
|
||||
|
||||
/**
|
||||
* Extracts translation messages from a directory or files to the catalogue.
|
||||
* New found messages are injected to the catalogue using the prefix.
|
||||
*
|
||||
* @author Michel Salib <michelsalib@hotmail.com>
|
||||
*/
|
||||
interface ExtractorInterface
|
||||
{
|
||||
/**
|
||||
* Extracts translation messages from files, a file or a directory to the catalogue.
|
||||
*
|
||||
* @param string|iterable<string> $resource Files, a file or a directory
|
||||
*/
|
||||
public function extract($resource, MessageCatalogue $catalogue);
|
||||
|
||||
/**
|
||||
* Sets the prefix that should be used for new found messages.
|
||||
*/
|
||||
public function setPrefix(string $prefix);
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
<?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\Extractor;
|
||||
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Component\Translation\MessageCatalogue;
|
||||
|
||||
/**
|
||||
* PhpExtractor extracts translation messages from a PHP template.
|
||||
*
|
||||
* @author Michel Salib <michelsalib@hotmail.com>
|
||||
*/
|
||||
class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface
|
||||
{
|
||||
public const MESSAGE_TOKEN = 300;
|
||||
public const METHOD_ARGUMENTS_TOKEN = 1000;
|
||||
public const DOMAIN_TOKEN = 1001;
|
||||
|
||||
/**
|
||||
* Prefix for new found message.
|
||||
*/
|
||||
private $prefix = '';
|
||||
|
||||
/**
|
||||
* The sequence that captures translation messages.
|
||||
*/
|
||||
protected $sequences = [
|
||||
[
|
||||
'->',
|
||||
'trans',
|
||||
'(',
|
||||
self::MESSAGE_TOKEN,
|
||||
',',
|
||||
self::METHOD_ARGUMENTS_TOKEN,
|
||||
',',
|
||||
self::DOMAIN_TOKEN,
|
||||
],
|
||||
[
|
||||
'->',
|
||||
'trans',
|
||||
'(',
|
||||
self::MESSAGE_TOKEN,
|
||||
],
|
||||
[
|
||||
'new',
|
||||
'TranslatableMessage',
|
||||
'(',
|
||||
self::MESSAGE_TOKEN,
|
||||
',',
|
||||
self::METHOD_ARGUMENTS_TOKEN,
|
||||
',',
|
||||
self::DOMAIN_TOKEN,
|
||||
],
|
||||
[
|
||||
'new',
|
||||
'TranslatableMessage',
|
||||
'(',
|
||||
self::MESSAGE_TOKEN,
|
||||
],
|
||||
[
|
||||
'new',
|
||||
'\\',
|
||||
'Symfony',
|
||||
'\\',
|
||||
'Component',
|
||||
'\\',
|
||||
'Translation',
|
||||
'\\',
|
||||
'TranslatableMessage',
|
||||
'(',
|
||||
self::MESSAGE_TOKEN,
|
||||
',',
|
||||
self::METHOD_ARGUMENTS_TOKEN,
|
||||
',',
|
||||
self::DOMAIN_TOKEN,
|
||||
],
|
||||
[
|
||||
'new',
|
||||
'\Symfony\Component\Translation\TranslatableMessage',
|
||||
'(',
|
||||
self::MESSAGE_TOKEN,
|
||||
',',
|
||||
self::METHOD_ARGUMENTS_TOKEN,
|
||||
',',
|
||||
self::DOMAIN_TOKEN,
|
||||
],
|
||||
[
|
||||
'new',
|
||||
'\\',
|
||||
'Symfony',
|
||||
'\\',
|
||||
'Component',
|
||||
'\\',
|
||||
'Translation',
|
||||
'\\',
|
||||
'TranslatableMessage',
|
||||
'(',
|
||||
self::MESSAGE_TOKEN,
|
||||
],
|
||||
[
|
||||
'new',
|
||||
'\Symfony\Component\Translation\TranslatableMessage',
|
||||
'(',
|
||||
self::MESSAGE_TOKEN,
|
||||
],
|
||||
[
|
||||
't',
|
||||
'(',
|
||||
self::MESSAGE_TOKEN,
|
||||
',',
|
||||
self::METHOD_ARGUMENTS_TOKEN,
|
||||
',',
|
||||
self::DOMAIN_TOKEN,
|
||||
],
|
||||
[
|
||||
't',
|
||||
'(',
|
||||
self::MESSAGE_TOKEN,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function extract($resource, MessageCatalogue $catalog)
|
||||
{
|
||||
$files = $this->extractFiles($resource);
|
||||
foreach ($files as $file) {
|
||||
$this->parseTokens(token_get_all(file_get_contents($file)), $catalog, $file);
|
||||
|
||||
gc_mem_caches();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setPrefix(string $prefix)
|
||||
{
|
||||
$this->prefix = $prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a token.
|
||||
*
|
||||
* @param mixed $token
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
protected function normalizeToken($token)
|
||||
{
|
||||
if (isset($token[1]) && 'b"' !== $token) {
|
||||
return $token[1];
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeks to a non-whitespace token.
|
||||
*/
|
||||
private function seekToNextRelevantToken(\Iterator $tokenIterator)
|
||||
{
|
||||
for (; $tokenIterator->valid(); $tokenIterator->next()) {
|
||||
$t = $tokenIterator->current();
|
||||
if (\T_WHITESPACE !== $t[0]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function skipMethodArgument(\Iterator $tokenIterator)
|
||||
{
|
||||
$openBraces = 0;
|
||||
|
||||
for (; $tokenIterator->valid(); $tokenIterator->next()) {
|
||||
$t = $tokenIterator->current();
|
||||
|
||||
if ('[' === $t[0] || '(' === $t[0]) {
|
||||
++$openBraces;
|
||||
}
|
||||
|
||||
if (']' === $t[0] || ')' === $t[0]) {
|
||||
--$openBraces;
|
||||
}
|
||||
|
||||
if ((0 === $openBraces && ',' === $t[0]) || (-1 === $openBraces && ')' === $t[0])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the message from the iterator while the tokens
|
||||
* match allowed message tokens.
|
||||
*/
|
||||
private function getValue(\Iterator $tokenIterator)
|
||||
{
|
||||
$message = '';
|
||||
$docToken = '';
|
||||
$docPart = '';
|
||||
|
||||
for (; $tokenIterator->valid(); $tokenIterator->next()) {
|
||||
$t = $tokenIterator->current();
|
||||
if ('.' === $t) {
|
||||
// Concatenate with next token
|
||||
continue;
|
||||
}
|
||||
if (!isset($t[1])) {
|
||||
break;
|
||||
}
|
||||
|
||||
switch ($t[0]) {
|
||||
case \T_START_HEREDOC:
|
||||
$docToken = $t[1];
|
||||
break;
|
||||
case \T_ENCAPSED_AND_WHITESPACE:
|
||||
case \T_CONSTANT_ENCAPSED_STRING:
|
||||
if ('' === $docToken) {
|
||||
$message .= PhpStringTokenParser::parse($t[1]);
|
||||
} else {
|
||||
$docPart = $t[1];
|
||||
}
|
||||
break;
|
||||
case \T_END_HEREDOC:
|
||||
if ($indentation = strspn($t[1], ' ')) {
|
||||
$docPartWithLineBreaks = $docPart;
|
||||
$docPart = '';
|
||||
|
||||
foreach (preg_split('~(\r\n|\n|\r)~', $docPartWithLineBreaks, -1, \PREG_SPLIT_DELIM_CAPTURE) as $str) {
|
||||
if (\in_array($str, ["\r\n", "\n", "\r"], true)) {
|
||||
$docPart .= $str;
|
||||
} else {
|
||||
$docPart .= substr($str, $indentation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$message .= PhpStringTokenParser::parseDocString($docToken, $docPart);
|
||||
$docToken = '';
|
||||
$docPart = '';
|
||||
break;
|
||||
case \T_WHITESPACE:
|
||||
break;
|
||||
default:
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts trans message from PHP tokens.
|
||||
*/
|
||||
protected function parseTokens(array $tokens, MessageCatalogue $catalog, string $filename)
|
||||
{
|
||||
$tokenIterator = new \ArrayIterator($tokens);
|
||||
|
||||
for ($key = 0; $key < $tokenIterator->count(); ++$key) {
|
||||
foreach ($this->sequences as $sequence) {
|
||||
$message = '';
|
||||
$domain = 'messages';
|
||||
$tokenIterator->seek($key);
|
||||
|
||||
foreach ($sequence as $sequenceKey => $item) {
|
||||
$this->seekToNextRelevantToken($tokenIterator);
|
||||
|
||||
if ($this->normalizeToken($tokenIterator->current()) === $item) {
|
||||
$tokenIterator->next();
|
||||
continue;
|
||||
} elseif (self::MESSAGE_TOKEN === $item) {
|
||||
$message = $this->getValue($tokenIterator);
|
||||
|
||||
if (\count($sequence) === ($sequenceKey + 1)) {
|
||||
break;
|
||||
}
|
||||
} elseif (self::METHOD_ARGUMENTS_TOKEN === $item) {
|
||||
$this->skipMethodArgument($tokenIterator);
|
||||
} elseif (self::DOMAIN_TOKEN === $item) {
|
||||
$domainToken = $this->getValue($tokenIterator);
|
||||
if ('' !== $domainToken) {
|
||||
$domain = $domainToken;
|
||||
}
|
||||
|
||||
break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($message) {
|
||||
$catalog->set($message, $this->prefix.$message, $domain);
|
||||
$metadata = $catalog->getMetadata($message, $domain) ?? [];
|
||||
$normalizedFilename = preg_replace('{[\\\\/]+}', '/', $filename);
|
||||
$metadata['sources'][] = $normalizedFilename.':'.$tokens[$key][2];
|
||||
$catalog->setMetadata($message, $metadata, $domain);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function canBeExtracted(string $file)
|
||||
{
|
||||
return $this->isFile($file) && 'php' === pathinfo($file, \PATHINFO_EXTENSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function extractFromDirectory($directory)
|
||||
{
|
||||
if (!class_exists(Finder::class)) {
|
||||
throw new \LogicException(sprintf('You cannot use "%s" as the "symfony/finder" package is not installed. Try running "composer require symfony/finder".', static::class));
|
||||
}
|
||||
|
||||
$finder = new Finder();
|
||||
|
||||
return $finder->files()->name('*.php')->in($directory);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user