Completed
Push — develop ( 316159...00443b )
by Zack
20:22
created
vendor/symfony/string/Slugger/AsciiSlugger.php 1 patch
Indentation   +157 added lines, -157 removed lines patch added patch discarded remove patch
@@ -16,7 +16,7 @@  discard block
 block discarded – undo
16 16
 use Symfony\Contracts\Translation\LocaleAwareInterface;
17 17
 
18 18
 if (!interface_exists(LocaleAwareInterface::class)) {
19
-    throw new \LogicException('You cannot use the "Symfony\Component\String\Slugger\AsciiSlugger" as the "symfony/translation-contracts" package is not installed. Try running "composer require symfony/translation-contracts".');
19
+	throw new \LogicException('You cannot use the "Symfony\Component\String\Slugger\AsciiSlugger" as the "symfony/translation-contracts" package is not installed. Try running "composer require symfony/translation-contracts".');
20 20
 }
21 21
 
22 22
 /**
@@ -24,160 +24,160 @@  discard block
 block discarded – undo
24 24
  */
25 25
 class AsciiSlugger implements SluggerInterface, LocaleAwareInterface
26 26
 {
27
-    private const LOCALE_TO_TRANSLITERATOR_ID = [
28
-        'am' => 'Amharic-Latin',
29
-        'ar' => 'Arabic-Latin',
30
-        'az' => 'Azerbaijani-Latin',
31
-        'be' => 'Belarusian-Latin',
32
-        'bg' => 'Bulgarian-Latin',
33
-        'bn' => 'Bengali-Latin',
34
-        'de' => 'de-ASCII',
35
-        'el' => 'Greek-Latin',
36
-        'fa' => 'Persian-Latin',
37
-        'he' => 'Hebrew-Latin',
38
-        'hy' => 'Armenian-Latin',
39
-        'ka' => 'Georgian-Latin',
40
-        'kk' => 'Kazakh-Latin',
41
-        'ky' => 'Kirghiz-Latin',
42
-        'ko' => 'Korean-Latin',
43
-        'mk' => 'Macedonian-Latin',
44
-        'mn' => 'Mongolian-Latin',
45
-        'or' => 'Oriya-Latin',
46
-        'ps' => 'Pashto-Latin',
47
-        'ru' => 'Russian-Latin',
48
-        'sr' => 'Serbian-Latin',
49
-        'sr_Cyrl' => 'Serbian-Latin',
50
-        'th' => 'Thai-Latin',
51
-        'tk' => 'Turkmen-Latin',
52
-        'uk' => 'Ukrainian-Latin',
53
-        'uz' => 'Uzbek-Latin',
54
-        'zh' => 'Han-Latin',
55
-    ];
56
-
57
-    private $defaultLocale;
58
-    private $symbolsMap = [
59
-        'en' => ['@' => 'at', '&' => 'and'],
60
-    ];
61
-
62
-    /**
63
-     * Cache of transliterators per locale.
64
-     *
65
-     * @var \Transliterator[]
66
-     */
67
-    private $transliterators = [];
68
-
69
-    /**
70
-     * @param array|\Closure|null $symbolsMap
71
-     */
72
-    public function __construct(string $defaultLocale = null, $symbolsMap = null)
73
-    {
74
-        if (null !== $symbolsMap && !\is_array($symbolsMap) && !$symbolsMap instanceof \Closure) {
75
-            throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be array, Closure or null, "%s" given.', __METHOD__, \gettype($symbolsMap)));
76
-        }
77
-
78
-        $this->defaultLocale = $defaultLocale;
79
-        $this->symbolsMap = $symbolsMap ?? $this->symbolsMap;
80
-    }
81
-
82
-    /**
83
-     * {@inheritdoc}
84
-     */
85
-    public function setLocale($locale)
86
-    {
87
-        $this->defaultLocale = $locale;
88
-    }
89
-
90
-    /**
91
-     * {@inheritdoc}
92
-     */
93
-    public function getLocale()
94
-    {
95
-        return $this->defaultLocale;
96
-    }
97
-
98
-    /**
99
-     * {@inheritdoc}
100
-     */
101
-    public function slug(string $string, string $separator = '-', string $locale = null): AbstractUnicodeString
102
-    {
103
-        $locale = $locale ?? $this->defaultLocale;
104
-
105
-        $transliterator = [];
106
-        if ($locale && ('de' === $locale || 0 === strpos($locale, 'de_'))) {
107
-            // Use the shortcut for German in UnicodeString::ascii() if possible (faster and no requirement on intl)
108
-            $transliterator = ['de-ASCII'];
109
-        } elseif (\function_exists('transliterator_transliterate') && $locale) {
110
-            $transliterator = (array) $this->createTransliterator($locale);
111
-        }
112
-
113
-        if ($this->symbolsMap instanceof \Closure) {
114
-            // If the symbols map is passed as a closure, there is no need to fallback to the parent locale
115
-            // as the closure can just provide substitutions for all locales of interest.
116
-            $symbolsMap = $this->symbolsMap;
117
-            array_unshift($transliterator, static function ($s) use ($symbolsMap, $locale) {
118
-                return $symbolsMap($s, $locale);
119
-            });
120
-        }
121
-
122
-        $unicodeString = (new UnicodeString($string))->ascii($transliterator);
123
-
124
-        if (\is_array($this->symbolsMap)) {
125
-            $map = null;
126
-            if (isset($this->symbolsMap[$locale])) {
127
-                $map = $this->symbolsMap[$locale];
128
-            } else {
129
-                $parent = self::getParentLocale($locale);
130
-                if ($parent && isset($this->symbolsMap[$parent])) {
131
-                    $map = $this->symbolsMap[$parent];
132
-                }
133
-            }
134
-            if ($map) {
135
-                foreach ($map as $char => $replace) {
136
-                    $unicodeString = $unicodeString->replace($char, ' '.$replace.' ');
137
-                }
138
-            }
139
-        }
140
-
141
-        return $unicodeString
142
-            ->replaceMatches('/[^A-Za-z0-9]++/', $separator)
143
-            ->trim($separator)
144
-        ;
145
-    }
146
-
147
-    private function createTransliterator(string $locale): ?\Transliterator
148
-    {
149
-        if (\array_key_exists($locale, $this->transliterators)) {
150
-            return $this->transliterators[$locale];
151
-        }
152
-
153
-        // Exact locale supported, cache and return
154
-        if ($id = self::LOCALE_TO_TRANSLITERATOR_ID[$locale] ?? null) {
155
-            return $this->transliterators[$locale] = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id);
156
-        }
157
-
158
-        // Locale not supported and no parent, fallback to any-latin
159
-        if (!$parent = self::getParentLocale($locale)) {
160
-            return $this->transliterators[$locale] = null;
161
-        }
162
-
163
-        // Try to use the parent locale (ie. try "de" for "de_AT") and cache both locales
164
-        if ($id = self::LOCALE_TO_TRANSLITERATOR_ID[$parent] ?? null) {
165
-            $transliterator = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id);
166
-        }
167
-
168
-        return $this->transliterators[$locale] = $this->transliterators[$parent] = $transliterator ?? null;
169
-    }
170
-
171
-    private static function getParentLocale(?string $locale): ?string
172
-    {
173
-        if (!$locale) {
174
-            return null;
175
-        }
176
-        if (false === $str = strrchr($locale, '_')) {
177
-            // no parent locale
178
-            return null;
179
-        }
180
-
181
-        return substr($locale, 0, -\strlen($str));
182
-    }
27
+	private const LOCALE_TO_TRANSLITERATOR_ID = [
28
+		'am' => 'Amharic-Latin',
29
+		'ar' => 'Arabic-Latin',
30
+		'az' => 'Azerbaijani-Latin',
31
+		'be' => 'Belarusian-Latin',
32
+		'bg' => 'Bulgarian-Latin',
33
+		'bn' => 'Bengali-Latin',
34
+		'de' => 'de-ASCII',
35
+		'el' => 'Greek-Latin',
36
+		'fa' => 'Persian-Latin',
37
+		'he' => 'Hebrew-Latin',
38
+		'hy' => 'Armenian-Latin',
39
+		'ka' => 'Georgian-Latin',
40
+		'kk' => 'Kazakh-Latin',
41
+		'ky' => 'Kirghiz-Latin',
42
+		'ko' => 'Korean-Latin',
43
+		'mk' => 'Macedonian-Latin',
44
+		'mn' => 'Mongolian-Latin',
45
+		'or' => 'Oriya-Latin',
46
+		'ps' => 'Pashto-Latin',
47
+		'ru' => 'Russian-Latin',
48
+		'sr' => 'Serbian-Latin',
49
+		'sr_Cyrl' => 'Serbian-Latin',
50
+		'th' => 'Thai-Latin',
51
+		'tk' => 'Turkmen-Latin',
52
+		'uk' => 'Ukrainian-Latin',
53
+		'uz' => 'Uzbek-Latin',
54
+		'zh' => 'Han-Latin',
55
+	];
56
+
57
+	private $defaultLocale;
58
+	private $symbolsMap = [
59
+		'en' => ['@' => 'at', '&' => 'and'],
60
+	];
61
+
62
+	/**
63
+	 * Cache of transliterators per locale.
64
+	 *
65
+	 * @var \Transliterator[]
66
+	 */
67
+	private $transliterators = [];
68
+
69
+	/**
70
+	 * @param array|\Closure|null $symbolsMap
71
+	 */
72
+	public function __construct(string $defaultLocale = null, $symbolsMap = null)
73
+	{
74
+		if (null !== $symbolsMap && !\is_array($symbolsMap) && !$symbolsMap instanceof \Closure) {
75
+			throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be array, Closure or null, "%s" given.', __METHOD__, \gettype($symbolsMap)));
76
+		}
77
+
78
+		$this->defaultLocale = $defaultLocale;
79
+		$this->symbolsMap = $symbolsMap ?? $this->symbolsMap;
80
+	}
81
+
82
+	/**
83
+	 * {@inheritdoc}
84
+	 */
85
+	public function setLocale($locale)
86
+	{
87
+		$this->defaultLocale = $locale;
88
+	}
89
+
90
+	/**
91
+	 * {@inheritdoc}
92
+	 */
93
+	public function getLocale()
94
+	{
95
+		return $this->defaultLocale;
96
+	}
97
+
98
+	/**
99
+	 * {@inheritdoc}
100
+	 */
101
+	public function slug(string $string, string $separator = '-', string $locale = null): AbstractUnicodeString
102
+	{
103
+		$locale = $locale ?? $this->defaultLocale;
104
+
105
+		$transliterator = [];
106
+		if ($locale && ('de' === $locale || 0 === strpos($locale, 'de_'))) {
107
+			// Use the shortcut for German in UnicodeString::ascii() if possible (faster and no requirement on intl)
108
+			$transliterator = ['de-ASCII'];
109
+		} elseif (\function_exists('transliterator_transliterate') && $locale) {
110
+			$transliterator = (array) $this->createTransliterator($locale);
111
+		}
112
+
113
+		if ($this->symbolsMap instanceof \Closure) {
114
+			// If the symbols map is passed as a closure, there is no need to fallback to the parent locale
115
+			// as the closure can just provide substitutions for all locales of interest.
116
+			$symbolsMap = $this->symbolsMap;
117
+			array_unshift($transliterator, static function ($s) use ($symbolsMap, $locale) {
118
+				return $symbolsMap($s, $locale);
119
+			});
120
+		}
121
+
122
+		$unicodeString = (new UnicodeString($string))->ascii($transliterator);
123
+
124
+		if (\is_array($this->symbolsMap)) {
125
+			$map = null;
126
+			if (isset($this->symbolsMap[$locale])) {
127
+				$map = $this->symbolsMap[$locale];
128
+			} else {
129
+				$parent = self::getParentLocale($locale);
130
+				if ($parent && isset($this->symbolsMap[$parent])) {
131
+					$map = $this->symbolsMap[$parent];
132
+				}
133
+			}
134
+			if ($map) {
135
+				foreach ($map as $char => $replace) {
136
+					$unicodeString = $unicodeString->replace($char, ' '.$replace.' ');
137
+				}
138
+			}
139
+		}
140
+
141
+		return $unicodeString
142
+			->replaceMatches('/[^A-Za-z0-9]++/', $separator)
143
+			->trim($separator)
144
+		;
145
+	}
146
+
147
+	private function createTransliterator(string $locale): ?\Transliterator
148
+	{
149
+		if (\array_key_exists($locale, $this->transliterators)) {
150
+			return $this->transliterators[$locale];
151
+		}
152
+
153
+		// Exact locale supported, cache and return
154
+		if ($id = self::LOCALE_TO_TRANSLITERATOR_ID[$locale] ?? null) {
155
+			return $this->transliterators[$locale] = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id);
156
+		}
157
+
158
+		// Locale not supported and no parent, fallback to any-latin
159
+		if (!$parent = self::getParentLocale($locale)) {
160
+			return $this->transliterators[$locale] = null;
161
+		}
162
+
163
+		// Try to use the parent locale (ie. try "de" for "de_AT") and cache both locales
164
+		if ($id = self::LOCALE_TO_TRANSLITERATOR_ID[$parent] ?? null) {
165
+			$transliterator = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id);
166
+		}
167
+
168
+		return $this->transliterators[$locale] = $this->transliterators[$parent] = $transliterator ?? null;
169
+	}
170
+
171
+	private static function getParentLocale(?string $locale): ?string
172
+	{
173
+		if (!$locale) {
174
+			return null;
175
+		}
176
+		if (false === $str = strrchr($locale, '_')) {
177
+			// no parent locale
178
+			return null;
179
+		}
180
+
181
+		return substr($locale, 0, -\strlen($str));
182
+	}
183 183
 }
Please login to merge, or discard this patch.
vendor/symfony/string/ByteString.php 1 patch
Indentation   +457 added lines, -457 removed lines patch added patch discarded remove patch
@@ -25,14 +25,14 @@  discard block
 block discarded – undo
25 25
  */
26 26
 class ByteString extends AbstractString
27 27
 {
28
-    private const ALPHABET_ALPHANUMERIC = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
28
+	private const ALPHABET_ALPHANUMERIC = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
29 29
 
30
-    public function __construct(string $string = '')
31
-    {
32
-        $this->string = $string;
33
-    }
30
+	public function __construct(string $string = '')
31
+	{
32
+		$this->string = $string;
33
+	}
34 34
 
35
-    /*
35
+	/*
36 36
      * The following method was derived from code of the Hack Standard Library (v4.40 - 2020-05-03)
37 37
      *
38 38
      * https://github.com/hhvm/hsl/blob/80a42c02f036f72a42f0415e80d6b847f4bf62d5/src/random/private.php#L16
@@ -42,465 +42,465 @@  discard block
 block discarded – undo
42 42
      * Copyright (c) 2004-2020, Facebook, Inc. (https://www.facebook.com/)
43 43
      */
44 44
 
45
-    public static function fromRandom(int $length = 16, string $alphabet = null): self
46
-    {
47
-        if ($length <= 0) {
48
-            throw new InvalidArgumentException(sprintf('A strictly positive length is expected, "%d" given.', $length));
49
-        }
50
-
51
-        $alphabet = $alphabet ?? self::ALPHABET_ALPHANUMERIC;
52
-        $alphabetSize = \strlen($alphabet);
53
-        $bits = (int) ceil(log($alphabetSize, 2.0));
54
-        if ($bits <= 0 || $bits > 56) {
55
-            throw new InvalidArgumentException('The length of the alphabet must in the [2^1, 2^56] range.');
56
-        }
57
-
58
-        $ret = '';
59
-        while ($length > 0) {
60
-            $urandomLength = (int) ceil(2 * $length * $bits / 8.0);
61
-            $data = random_bytes($urandomLength);
62
-            $unpackedData = 0;
63
-            $unpackedBits = 0;
64
-            for ($i = 0; $i < $urandomLength && $length > 0; ++$i) {
65
-                // Unpack 8 bits
66
-                $unpackedData = ($unpackedData << 8) | \ord($data[$i]);
67
-                $unpackedBits += 8;
68
-
69
-                // While we have enough bits to select a character from the alphabet, keep
70
-                // consuming the random data
71
-                for (; $unpackedBits >= $bits && $length > 0; $unpackedBits -= $bits) {
72
-                    $index = ($unpackedData & ((1 << $bits) - 1));
73
-                    $unpackedData >>= $bits;
74
-                    // Unfortunately, the alphabet size is not necessarily a power of two.
75
-                    // Worst case, it is 2^k + 1, which means we need (k+1) bits and we
76
-                    // have around a 50% chance of missing as k gets larger
77
-                    if ($index < $alphabetSize) {
78
-                        $ret .= $alphabet[$index];
79
-                        --$length;
80
-                    }
81
-                }
82
-            }
83
-        }
84
-
85
-        return new static($ret);
86
-    }
87
-
88
-    public function bytesAt(int $offset): array
89
-    {
90
-        $str = $this->string[$offset] ?? '';
91
-
92
-        return '' === $str ? [] : [\ord($str)];
93
-    }
94
-
95
-    public function append(string ...$suffix): parent
96
-    {
97
-        $str = clone $this;
98
-        $str->string .= 1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix);
99
-
100
-        return $str;
101
-    }
102
-
103
-    public function camel(): parent
104
-    {
105
-        $str = clone $this;
106
-        $str->string = lcfirst(str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $this->string))));
107
-
108
-        return $str;
109
-    }
110
-
111
-    public function chunk(int $length = 1): array
112
-    {
113
-        if (1 > $length) {
114
-            throw new InvalidArgumentException('The chunk length must be greater than zero.');
115
-        }
116
-
117
-        if ('' === $this->string) {
118
-            return [];
119
-        }
120
-
121
-        $str = clone $this;
122
-        $chunks = [];
123
-
124
-        foreach (str_split($this->string, $length) as $chunk) {
125
-            $str->string = $chunk;
126
-            $chunks[] = clone $str;
127
-        }
128
-
129
-        return $chunks;
130
-    }
131
-
132
-    public function endsWith($suffix): bool
133
-    {
134
-        if ($suffix instanceof parent) {
135
-            $suffix = $suffix->string;
136
-        } elseif (\is_array($suffix) || $suffix instanceof \Traversable) {
137
-            return parent::endsWith($suffix);
138
-        } else {
139
-            $suffix = (string) $suffix;
140
-        }
141
-
142
-        return '' !== $suffix && \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix), null, $this->ignoreCase);
143
-    }
144
-
145
-    public function equalsTo($string): bool
146
-    {
147
-        if ($string instanceof parent) {
148
-            $string = $string->string;
149
-        } elseif (\is_array($string) || $string instanceof \Traversable) {
150
-            return parent::equalsTo($string);
151
-        } else {
152
-            $string = (string) $string;
153
-        }
154
-
155
-        if ('' !== $string && $this->ignoreCase) {
156
-            return 0 === strcasecmp($string, $this->string);
157
-        }
158
-
159
-        return $string === $this->string;
160
-    }
161
-
162
-    public function folded(): parent
163
-    {
164
-        $str = clone $this;
165
-        $str->string = strtolower($str->string);
166
-
167
-        return $str;
168
-    }
169
-
170
-    public function indexOf($needle, int $offset = 0): ?int
171
-    {
172
-        if ($needle instanceof parent) {
173
-            $needle = $needle->string;
174
-        } elseif (\is_array($needle) || $needle instanceof \Traversable) {
175
-            return parent::indexOf($needle, $offset);
176
-        } else {
177
-            $needle = (string) $needle;
178
-        }
179
-
180
-        if ('' === $needle) {
181
-            return null;
182
-        }
183
-
184
-        $i = $this->ignoreCase ? stripos($this->string, $needle, $offset) : strpos($this->string, $needle, $offset);
185
-
186
-        return false === $i ? null : $i;
187
-    }
188
-
189
-    public function indexOfLast($needle, int $offset = 0): ?int
190
-    {
191
-        if ($needle instanceof parent) {
192
-            $needle = $needle->string;
193
-        } elseif (\is_array($needle) || $needle instanceof \Traversable) {
194
-            return parent::indexOfLast($needle, $offset);
195
-        } else {
196
-            $needle = (string) $needle;
197
-        }
198
-
199
-        if ('' === $needle) {
200
-            return null;
201
-        }
202
-
203
-        $i = $this->ignoreCase ? strripos($this->string, $needle, $offset) : strrpos($this->string, $needle, $offset);
204
-
205
-        return false === $i ? null : $i;
206
-    }
207
-
208
-    public function isUtf8(): bool
209
-    {
210
-        return '' === $this->string || preg_match('//u', $this->string);
211
-    }
212
-
213
-    public function join(array $strings, string $lastGlue = null): parent
214
-    {
215
-        $str = clone $this;
216
-
217
-        $tail = null !== $lastGlue && 1 < \count($strings) ? $lastGlue.array_pop($strings) : '';
218
-        $str->string = implode($this->string, $strings).$tail;
219
-
220
-        return $str;
221
-    }
222
-
223
-    public function length(): int
224
-    {
225
-        return \strlen($this->string);
226
-    }
227
-
228
-    public function lower(): parent
229
-    {
230
-        $str = clone $this;
231
-        $str->string = strtolower($str->string);
232
-
233
-        return $str;
234
-    }
235
-
236
-    public function match(string $regexp, int $flags = 0, int $offset = 0): array
237
-    {
238
-        $match = ((\PREG_PATTERN_ORDER | \PREG_SET_ORDER) & $flags) ? 'preg_match_all' : 'preg_match';
239
-
240
-        if ($this->ignoreCase) {
241
-            $regexp .= 'i';
242
-        }
243
-
244
-        set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); });
245
-
246
-        try {
247
-            if (false === $match($regexp, $this->string, $matches, $flags | \PREG_UNMATCHED_AS_NULL, $offset)) {
248
-                $lastError = preg_last_error();
249
-
250
-                foreach (get_defined_constants(true)['pcre'] as $k => $v) {
251
-                    if ($lastError === $v && '_ERROR' === substr($k, -6)) {
252
-                        throw new RuntimeException('Matching failed with '.$k.'.');
253
-                    }
254
-                }
255
-
256
-                throw new RuntimeException('Matching failed with unknown error code.');
257
-            }
258
-        } finally {
259
-            restore_error_handler();
260
-        }
261
-
262
-        return $matches;
263
-    }
264
-
265
-    public function padBoth(int $length, string $padStr = ' '): parent
266
-    {
267
-        $str = clone $this;
268
-        $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_BOTH);
269
-
270
-        return $str;
271
-    }
272
-
273
-    public function padEnd(int $length, string $padStr = ' '): parent
274
-    {
275
-        $str = clone $this;
276
-        $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_RIGHT);
277
-
278
-        return $str;
279
-    }
280
-
281
-    public function padStart(int $length, string $padStr = ' '): parent
282
-    {
283
-        $str = clone $this;
284
-        $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_LEFT);
285
-
286
-        return $str;
287
-    }
288
-
289
-    public function prepend(string ...$prefix): parent
290
-    {
291
-        $str = clone $this;
292
-        $str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$str->string;
293
-
294
-        return $str;
295
-    }
296
-
297
-    public function replace(string $from, string $to): parent
298
-    {
299
-        $str = clone $this;
300
-
301
-        if ('' !== $from) {
302
-            $str->string = $this->ignoreCase ? str_ireplace($from, $to, $this->string) : str_replace($from, $to, $this->string);
303
-        }
304
-
305
-        return $str;
306
-    }
307
-
308
-    public function replaceMatches(string $fromRegexp, $to): parent
309
-    {
310
-        if ($this->ignoreCase) {
311
-            $fromRegexp .= 'i';
312
-        }
313
-
314
-        if (\is_array($to)) {
315
-            if (!\is_callable($to)) {
316
-                throw new \TypeError(sprintf('Argument 2 passed to "%s::replaceMatches()" must be callable, array given.', static::class));
317
-            }
318
-
319
-            $replace = 'preg_replace_callback';
320
-        } else {
321
-            $replace = $to instanceof \Closure ? 'preg_replace_callback' : 'preg_replace';
322
-        }
323
-
324
-        set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); });
325
-
326
-        try {
327
-            if (null === $string = $replace($fromRegexp, $to, $this->string)) {
328
-                $lastError = preg_last_error();
329
-
330
-                foreach (get_defined_constants(true)['pcre'] as $k => $v) {
331
-                    if ($lastError === $v && '_ERROR' === substr($k, -6)) {
332
-                        throw new RuntimeException('Matching failed with '.$k.'.');
333
-                    }
334
-                }
335
-
336
-                throw new RuntimeException('Matching failed with unknown error code.');
337
-            }
338
-        } finally {
339
-            restore_error_handler();
340
-        }
341
-
342
-        $str = clone $this;
343
-        $str->string = $string;
344
-
345
-        return $str;
346
-    }
347
-
348
-    public function reverse(): parent
349
-    {
350
-        $str = clone $this;
351
-        $str->string = strrev($str->string);
352
-
353
-        return $str;
354
-    }
355
-
356
-    public function slice(int $start = 0, int $length = null): parent
357
-    {
358
-        $str = clone $this;
359
-        $str->string = (string) substr($this->string, $start, $length ?? \PHP_INT_MAX);
360
-
361
-        return $str;
362
-    }
363
-
364
-    public function snake(): parent
365
-    {
366
-        $str = $this->camel()->title();
367
-        $str->string = strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], '\1_\2', $str->string));
368
-
369
-        return $str;
370
-    }
371
-
372
-    public function splice(string $replacement, int $start = 0, int $length = null): parent
373
-    {
374
-        $str = clone $this;
375
-        $str->string = substr_replace($this->string, $replacement, $start, $length ?? \PHP_INT_MAX);
376
-
377
-        return $str;
378
-    }
379
-
380
-    public function split(string $delimiter, int $limit = null, int $flags = null): array
381
-    {
382
-        if (1 > $limit = $limit ?? \PHP_INT_MAX) {
383
-            throw new InvalidArgumentException('Split limit must be a positive integer.');
384
-        }
385
-
386
-        if ('' === $delimiter) {
387
-            throw new InvalidArgumentException('Split delimiter is empty.');
388
-        }
389
-
390
-        if (null !== $flags) {
391
-            return parent::split($delimiter, $limit, $flags);
392
-        }
393
-
394
-        $str = clone $this;
395
-        $chunks = $this->ignoreCase
396
-            ? preg_split('{'.preg_quote($delimiter).'}iD', $this->string, $limit)
397
-            : explode($delimiter, $this->string, $limit);
398
-
399
-        foreach ($chunks as &$chunk) {
400
-            $str->string = $chunk;
401
-            $chunk = clone $str;
402
-        }
403
-
404
-        return $chunks;
405
-    }
406
-
407
-    public function startsWith($prefix): bool
408
-    {
409
-        if ($prefix instanceof parent) {
410
-            $prefix = $prefix->string;
411
-        } elseif (!\is_string($prefix)) {
412
-            return parent::startsWith($prefix);
413
-        }
414
-
415
-        return '' !== $prefix && 0 === ($this->ignoreCase ? strncasecmp($this->string, $prefix, \strlen($prefix)) : strncmp($this->string, $prefix, \strlen($prefix)));
416
-    }
417
-
418
-    public function title(bool $allWords = false): parent
419
-    {
420
-        $str = clone $this;
421
-        $str->string = $allWords ? ucwords($str->string) : ucfirst($str->string);
422
-
423
-        return $str;
424
-    }
425
-
426
-    public function toUnicodeString(string $fromEncoding = null): UnicodeString
427
-    {
428
-        return new UnicodeString($this->toCodePointString($fromEncoding)->string);
429
-    }
430
-
431
-    public function toCodePointString(string $fromEncoding = null): CodePointString
432
-    {
433
-        $u = new CodePointString();
434
-
435
-        if (\in_array($fromEncoding, [null, 'utf8', 'utf-8', 'UTF8', 'UTF-8'], true) && preg_match('//u', $this->string)) {
436
-            $u->string = $this->string;
437
-
438
-            return $u;
439
-        }
45
+	public static function fromRandom(int $length = 16, string $alphabet = null): self
46
+	{
47
+		if ($length <= 0) {
48
+			throw new InvalidArgumentException(sprintf('A strictly positive length is expected, "%d" given.', $length));
49
+		}
50
+
51
+		$alphabet = $alphabet ?? self::ALPHABET_ALPHANUMERIC;
52
+		$alphabetSize = \strlen($alphabet);
53
+		$bits = (int) ceil(log($alphabetSize, 2.0));
54
+		if ($bits <= 0 || $bits > 56) {
55
+			throw new InvalidArgumentException('The length of the alphabet must in the [2^1, 2^56] range.');
56
+		}
57
+
58
+		$ret = '';
59
+		while ($length > 0) {
60
+			$urandomLength = (int) ceil(2 * $length * $bits / 8.0);
61
+			$data = random_bytes($urandomLength);
62
+			$unpackedData = 0;
63
+			$unpackedBits = 0;
64
+			for ($i = 0; $i < $urandomLength && $length > 0; ++$i) {
65
+				// Unpack 8 bits
66
+				$unpackedData = ($unpackedData << 8) | \ord($data[$i]);
67
+				$unpackedBits += 8;
68
+
69
+				// While we have enough bits to select a character from the alphabet, keep
70
+				// consuming the random data
71
+				for (; $unpackedBits >= $bits && $length > 0; $unpackedBits -= $bits) {
72
+					$index = ($unpackedData & ((1 << $bits) - 1));
73
+					$unpackedData >>= $bits;
74
+					// Unfortunately, the alphabet size is not necessarily a power of two.
75
+					// Worst case, it is 2^k + 1, which means we need (k+1) bits and we
76
+					// have around a 50% chance of missing as k gets larger
77
+					if ($index < $alphabetSize) {
78
+						$ret .= $alphabet[$index];
79
+						--$length;
80
+					}
81
+				}
82
+			}
83
+		}
84
+
85
+		return new static($ret);
86
+	}
87
+
88
+	public function bytesAt(int $offset): array
89
+	{
90
+		$str = $this->string[$offset] ?? '';
91
+
92
+		return '' === $str ? [] : [\ord($str)];
93
+	}
94
+
95
+	public function append(string ...$suffix): parent
96
+	{
97
+		$str = clone $this;
98
+		$str->string .= 1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix);
99
+
100
+		return $str;
101
+	}
102
+
103
+	public function camel(): parent
104
+	{
105
+		$str = clone $this;
106
+		$str->string = lcfirst(str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $this->string))));
107
+
108
+		return $str;
109
+	}
110
+
111
+	public function chunk(int $length = 1): array
112
+	{
113
+		if (1 > $length) {
114
+			throw new InvalidArgumentException('The chunk length must be greater than zero.');
115
+		}
116
+
117
+		if ('' === $this->string) {
118
+			return [];
119
+		}
120
+
121
+		$str = clone $this;
122
+		$chunks = [];
123
+
124
+		foreach (str_split($this->string, $length) as $chunk) {
125
+			$str->string = $chunk;
126
+			$chunks[] = clone $str;
127
+		}
128
+
129
+		return $chunks;
130
+	}
131
+
132
+	public function endsWith($suffix): bool
133
+	{
134
+		if ($suffix instanceof parent) {
135
+			$suffix = $suffix->string;
136
+		} elseif (\is_array($suffix) || $suffix instanceof \Traversable) {
137
+			return parent::endsWith($suffix);
138
+		} else {
139
+			$suffix = (string) $suffix;
140
+		}
141
+
142
+		return '' !== $suffix && \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix), null, $this->ignoreCase);
143
+	}
144
+
145
+	public function equalsTo($string): bool
146
+	{
147
+		if ($string instanceof parent) {
148
+			$string = $string->string;
149
+		} elseif (\is_array($string) || $string instanceof \Traversable) {
150
+			return parent::equalsTo($string);
151
+		} else {
152
+			$string = (string) $string;
153
+		}
154
+
155
+		if ('' !== $string && $this->ignoreCase) {
156
+			return 0 === strcasecmp($string, $this->string);
157
+		}
158
+
159
+		return $string === $this->string;
160
+	}
161
+
162
+	public function folded(): parent
163
+	{
164
+		$str = clone $this;
165
+		$str->string = strtolower($str->string);
166
+
167
+		return $str;
168
+	}
169
+
170
+	public function indexOf($needle, int $offset = 0): ?int
171
+	{
172
+		if ($needle instanceof parent) {
173
+			$needle = $needle->string;
174
+		} elseif (\is_array($needle) || $needle instanceof \Traversable) {
175
+			return parent::indexOf($needle, $offset);
176
+		} else {
177
+			$needle = (string) $needle;
178
+		}
179
+
180
+		if ('' === $needle) {
181
+			return null;
182
+		}
183
+
184
+		$i = $this->ignoreCase ? stripos($this->string, $needle, $offset) : strpos($this->string, $needle, $offset);
185
+
186
+		return false === $i ? null : $i;
187
+	}
188
+
189
+	public function indexOfLast($needle, int $offset = 0): ?int
190
+	{
191
+		if ($needle instanceof parent) {
192
+			$needle = $needle->string;
193
+		} elseif (\is_array($needle) || $needle instanceof \Traversable) {
194
+			return parent::indexOfLast($needle, $offset);
195
+		} else {
196
+			$needle = (string) $needle;
197
+		}
198
+
199
+		if ('' === $needle) {
200
+			return null;
201
+		}
202
+
203
+		$i = $this->ignoreCase ? strripos($this->string, $needle, $offset) : strrpos($this->string, $needle, $offset);
204
+
205
+		return false === $i ? null : $i;
206
+	}
207
+
208
+	public function isUtf8(): bool
209
+	{
210
+		return '' === $this->string || preg_match('//u', $this->string);
211
+	}
212
+
213
+	public function join(array $strings, string $lastGlue = null): parent
214
+	{
215
+		$str = clone $this;
216
+
217
+		$tail = null !== $lastGlue && 1 < \count($strings) ? $lastGlue.array_pop($strings) : '';
218
+		$str->string = implode($this->string, $strings).$tail;
219
+
220
+		return $str;
221
+	}
222
+
223
+	public function length(): int
224
+	{
225
+		return \strlen($this->string);
226
+	}
227
+
228
+	public function lower(): parent
229
+	{
230
+		$str = clone $this;
231
+		$str->string = strtolower($str->string);
232
+
233
+		return $str;
234
+	}
235
+
236
+	public function match(string $regexp, int $flags = 0, int $offset = 0): array
237
+	{
238
+		$match = ((\PREG_PATTERN_ORDER | \PREG_SET_ORDER) & $flags) ? 'preg_match_all' : 'preg_match';
239
+
240
+		if ($this->ignoreCase) {
241
+			$regexp .= 'i';
242
+		}
243
+
244
+		set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); });
245
+
246
+		try {
247
+			if (false === $match($regexp, $this->string, $matches, $flags | \PREG_UNMATCHED_AS_NULL, $offset)) {
248
+				$lastError = preg_last_error();
249
+
250
+				foreach (get_defined_constants(true)['pcre'] as $k => $v) {
251
+					if ($lastError === $v && '_ERROR' === substr($k, -6)) {
252
+						throw new RuntimeException('Matching failed with '.$k.'.');
253
+					}
254
+				}
255
+
256
+				throw new RuntimeException('Matching failed with unknown error code.');
257
+			}
258
+		} finally {
259
+			restore_error_handler();
260
+		}
261
+
262
+		return $matches;
263
+	}
264
+
265
+	public function padBoth(int $length, string $padStr = ' '): parent
266
+	{
267
+		$str = clone $this;
268
+		$str->string = str_pad($this->string, $length, $padStr, \STR_PAD_BOTH);
269
+
270
+		return $str;
271
+	}
272
+
273
+	public function padEnd(int $length, string $padStr = ' '): parent
274
+	{
275
+		$str = clone $this;
276
+		$str->string = str_pad($this->string, $length, $padStr, \STR_PAD_RIGHT);
277
+
278
+		return $str;
279
+	}
280
+
281
+	public function padStart(int $length, string $padStr = ' '): parent
282
+	{
283
+		$str = clone $this;
284
+		$str->string = str_pad($this->string, $length, $padStr, \STR_PAD_LEFT);
285
+
286
+		return $str;
287
+	}
288
+
289
+	public function prepend(string ...$prefix): parent
290
+	{
291
+		$str = clone $this;
292
+		$str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$str->string;
293
+
294
+		return $str;
295
+	}
296
+
297
+	public function replace(string $from, string $to): parent
298
+	{
299
+		$str = clone $this;
300
+
301
+		if ('' !== $from) {
302
+			$str->string = $this->ignoreCase ? str_ireplace($from, $to, $this->string) : str_replace($from, $to, $this->string);
303
+		}
304
+
305
+		return $str;
306
+	}
307
+
308
+	public function replaceMatches(string $fromRegexp, $to): parent
309
+	{
310
+		if ($this->ignoreCase) {
311
+			$fromRegexp .= 'i';
312
+		}
313
+
314
+		if (\is_array($to)) {
315
+			if (!\is_callable($to)) {
316
+				throw new \TypeError(sprintf('Argument 2 passed to "%s::replaceMatches()" must be callable, array given.', static::class));
317
+			}
318
+
319
+			$replace = 'preg_replace_callback';
320
+		} else {
321
+			$replace = $to instanceof \Closure ? 'preg_replace_callback' : 'preg_replace';
322
+		}
323
+
324
+		set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); });
325
+
326
+		try {
327
+			if (null === $string = $replace($fromRegexp, $to, $this->string)) {
328
+				$lastError = preg_last_error();
329
+
330
+				foreach (get_defined_constants(true)['pcre'] as $k => $v) {
331
+					if ($lastError === $v && '_ERROR' === substr($k, -6)) {
332
+						throw new RuntimeException('Matching failed with '.$k.'.');
333
+					}
334
+				}
335
+
336
+				throw new RuntimeException('Matching failed with unknown error code.');
337
+			}
338
+		} finally {
339
+			restore_error_handler();
340
+		}
341
+
342
+		$str = clone $this;
343
+		$str->string = $string;
344
+
345
+		return $str;
346
+	}
347
+
348
+	public function reverse(): parent
349
+	{
350
+		$str = clone $this;
351
+		$str->string = strrev($str->string);
352
+
353
+		return $str;
354
+	}
355
+
356
+	public function slice(int $start = 0, int $length = null): parent
357
+	{
358
+		$str = clone $this;
359
+		$str->string = (string) substr($this->string, $start, $length ?? \PHP_INT_MAX);
360
+
361
+		return $str;
362
+	}
363
+
364
+	public function snake(): parent
365
+	{
366
+		$str = $this->camel()->title();
367
+		$str->string = strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], '\1_\2', $str->string));
368
+
369
+		return $str;
370
+	}
371
+
372
+	public function splice(string $replacement, int $start = 0, int $length = null): parent
373
+	{
374
+		$str = clone $this;
375
+		$str->string = substr_replace($this->string, $replacement, $start, $length ?? \PHP_INT_MAX);
376
+
377
+		return $str;
378
+	}
379
+
380
+	public function split(string $delimiter, int $limit = null, int $flags = null): array
381
+	{
382
+		if (1 > $limit = $limit ?? \PHP_INT_MAX) {
383
+			throw new InvalidArgumentException('Split limit must be a positive integer.');
384
+		}
385
+
386
+		if ('' === $delimiter) {
387
+			throw new InvalidArgumentException('Split delimiter is empty.');
388
+		}
389
+
390
+		if (null !== $flags) {
391
+			return parent::split($delimiter, $limit, $flags);
392
+		}
393
+
394
+		$str = clone $this;
395
+		$chunks = $this->ignoreCase
396
+			? preg_split('{'.preg_quote($delimiter).'}iD', $this->string, $limit)
397
+			: explode($delimiter, $this->string, $limit);
398
+
399
+		foreach ($chunks as &$chunk) {
400
+			$str->string = $chunk;
401
+			$chunk = clone $str;
402
+		}
403
+
404
+		return $chunks;
405
+	}
406
+
407
+	public function startsWith($prefix): bool
408
+	{
409
+		if ($prefix instanceof parent) {
410
+			$prefix = $prefix->string;
411
+		} elseif (!\is_string($prefix)) {
412
+			return parent::startsWith($prefix);
413
+		}
414
+
415
+		return '' !== $prefix && 0 === ($this->ignoreCase ? strncasecmp($this->string, $prefix, \strlen($prefix)) : strncmp($this->string, $prefix, \strlen($prefix)));
416
+	}
417
+
418
+	public function title(bool $allWords = false): parent
419
+	{
420
+		$str = clone $this;
421
+		$str->string = $allWords ? ucwords($str->string) : ucfirst($str->string);
422
+
423
+		return $str;
424
+	}
425
+
426
+	public function toUnicodeString(string $fromEncoding = null): UnicodeString
427
+	{
428
+		return new UnicodeString($this->toCodePointString($fromEncoding)->string);
429
+	}
430
+
431
+	public function toCodePointString(string $fromEncoding = null): CodePointString
432
+	{
433
+		$u = new CodePointString();
434
+
435
+		if (\in_array($fromEncoding, [null, 'utf8', 'utf-8', 'UTF8', 'UTF-8'], true) && preg_match('//u', $this->string)) {
436
+			$u->string = $this->string;
437
+
438
+			return $u;
439
+		}
440 440
 
441
-        set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); });
442
-
443
-        try {
444
-            try {
445
-                $validEncoding = false !== mb_detect_encoding($this->string, $fromEncoding ?? 'Windows-1252', true);
446
-            } catch (InvalidArgumentException $e) {
447
-                if (!\function_exists('iconv')) {
448
-                    throw $e;
449
-                }
450
-
451
-                $u->string = iconv($fromEncoding ?? 'Windows-1252', 'UTF-8', $this->string);
441
+		set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); });
442
+
443
+		try {
444
+			try {
445
+				$validEncoding = false !== mb_detect_encoding($this->string, $fromEncoding ?? 'Windows-1252', true);
446
+			} catch (InvalidArgumentException $e) {
447
+				if (!\function_exists('iconv')) {
448
+					throw $e;
449
+				}
450
+
451
+				$u->string = iconv($fromEncoding ?? 'Windows-1252', 'UTF-8', $this->string);
452 452
 
453
-                return $u;
454
-            }
455
-        } finally {
456
-            restore_error_handler();
457
-        }
458
-
459
-        if (!$validEncoding) {
460
-            throw new InvalidArgumentException(sprintf('Invalid "%s" string.', $fromEncoding ?? 'Windows-1252'));
461
-        }
462
-
463
-        $u->string = mb_convert_encoding($this->string, 'UTF-8', $fromEncoding ?? 'Windows-1252');
464
-
465
-        return $u;
466
-    }
453
+				return $u;
454
+			}
455
+		} finally {
456
+			restore_error_handler();
457
+		}
458
+
459
+		if (!$validEncoding) {
460
+			throw new InvalidArgumentException(sprintf('Invalid "%s" string.', $fromEncoding ?? 'Windows-1252'));
461
+		}
462
+
463
+		$u->string = mb_convert_encoding($this->string, 'UTF-8', $fromEncoding ?? 'Windows-1252');
464
+
465
+		return $u;
466
+	}
467 467
 
468
-    public function trim(string $chars = " \t\n\r\0\x0B\x0C"): parent
469
-    {
470
-        $str = clone $this;
471
-        $str->string = trim($str->string, $chars);
472
-
473
-        return $str;
474
-    }
468
+	public function trim(string $chars = " \t\n\r\0\x0B\x0C"): parent
469
+	{
470
+		$str = clone $this;
471
+		$str->string = trim($str->string, $chars);
472
+
473
+		return $str;
474
+	}
475 475
 
476
-    public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C"): parent
477
-    {
478
-        $str = clone $this;
479
-        $str->string = rtrim($str->string, $chars);
480
-
481
-        return $str;
482
-    }
476
+	public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C"): parent
477
+	{
478
+		$str = clone $this;
479
+		$str->string = rtrim($str->string, $chars);
480
+
481
+		return $str;
482
+	}
483 483
 
484
-    public function trimStart(string $chars = " \t\n\r\0\x0B\x0C"): parent
485
-    {
486
-        $str = clone $this;
487
-        $str->string = ltrim($str->string, $chars);
484
+	public function trimStart(string $chars = " \t\n\r\0\x0B\x0C"): parent
485
+	{
486
+		$str = clone $this;
487
+		$str->string = ltrim($str->string, $chars);
488 488
 
489
-        return $str;
490
-    }
489
+		return $str;
490
+	}
491 491
 
492
-    public function upper(): parent
493
-    {
494
-        $str = clone $this;
495
-        $str->string = strtoupper($str->string);
492
+	public function upper(): parent
493
+	{
494
+		$str = clone $this;
495
+		$str->string = strtoupper($str->string);
496 496
 
497
-        return $str;
498
-    }
497
+		return $str;
498
+	}
499 499
 
500
-    public function width(bool $ignoreAnsiDecoration = true): int
501
-    {
502
-        $string = preg_match('//u', $this->string) ? $this->string : preg_replace('/[\x80-\xFF]/', '?', $this->string);
500
+	public function width(bool $ignoreAnsiDecoration = true): int
501
+	{
502
+		$string = preg_match('//u', $this->string) ? $this->string : preg_replace('/[\x80-\xFF]/', '?', $this->string);
503 503
 
504
-        return (new CodePointString($string))->width($ignoreAnsiDecoration);
505
-    }
504
+		return (new CodePointString($string))->width($ignoreAnsiDecoration);
505
+	}
506 506
 }
Please login to merge, or discard this patch.
vendor/symfony/string/Inflector/FrenchInflector.php 1 patch
Indentation   +136 added lines, -136 removed lines patch added patch discarded remove patch
@@ -18,140 +18,140 @@
 block discarded – undo
18 18
  */
19 19
 final class FrenchInflector implements InflectorInterface
20 20
 {
21
-    /**
22
-     * A list of all rules for pluralise.
23
-     *
24
-     * @see https://la-conjugaison.nouvelobs.com/regles/grammaire/le-pluriel-des-noms-121.php
25
-     */
26
-    private const PLURALIZE_REGEXP = [
27
-        // First entry: regexp
28
-        // Second entry: replacement
29
-
30
-        // Words finishing with "s", "x" or "z" are invariables
31
-        // Les mots finissant par "s", "x" ou "z" sont invariables
32
-        ['/(s|x|z)$/i', '\1'],
33
-
34
-        // Words finishing with "eau" are pluralized with a "x"
35
-        // Les mots finissant par "eau" prennent tous un "x" au pluriel
36
-        ['/(eau)$/i', '\1x'],
37
-
38
-        // Words finishing with "au" are pluralized with a "x" excepted "landau"
39
-        // Les mots finissant par "au" prennent un "x" au pluriel sauf "landau"
40
-        ['/^(landau)$/i', '\1s'],
41
-        ['/(au)$/i', '\1x'],
42
-
43
-        // Words finishing with "eu" are pluralized with a "x" excepted "pneu", "bleu", "émeu"
44
-        // Les mots finissant en "eu" prennent un "x" au pluriel sauf "pneu", "bleu", "émeu"
45
-        ['/^(pneu|bleu|émeu)$/i', '\1s'],
46
-        ['/(eu)$/i', '\1x'],
47
-
48
-        // Words finishing with "al" are pluralized with a "aux" excepted
49
-        // Les mots finissant en "al" se terminent en "aux" sauf
50
-        ['/^(bal|carnaval|caracal|chacal|choral|corral|étal|festival|récital|val)$/i', '\1s'],
51
-        ['/al$/i', '\1aux'],
52
-
53
-        // Aspirail, bail, corail, émail, fermail, soupirail, travail, vantail et vitrail font leur pluriel en -aux
54
-        ['/^(aspir|b|cor|ém|ferm|soupir|trav|vant|vitr)ail$/i', '\1aux'],
55
-
56
-        // Bijou, caillou, chou, genou, hibou, joujou et pou qui prennent un x au pluriel
57
-        ['/^(bij|caill|ch|gen|hib|jouj|p)ou$/i', '\1oux'],
58
-
59
-        // Invariable words
60
-        ['/^(cinquante|soixante|mille)$/i', '\1'],
61
-
62
-        // French titles
63
-        ['/^(mon|ma)(sieur|dame|demoiselle|seigneur)$/', 'mes\2s'],
64
-        ['/^(Mon|Ma)(sieur|dame|demoiselle|seigneur)$/', 'Mes\2s'],
65
-    ];
66
-
67
-    /**
68
-     * A list of all rules for singularize.
69
-     */
70
-    private const SINGULARIZE_REGEXP = [
71
-        // First entry: regexp
72
-        // Second entry: replacement
73
-
74
-        // Aspirail, bail, corail, émail, fermail, soupirail, travail, vantail et vitrail font leur pluriel en -aux
75
-        ['/((aspir|b|cor|ém|ferm|soupir|trav|vant|vitr))aux$/i', '\1ail'],
76
-
77
-        // Words finishing with "eau" are pluralized with a "x"
78
-        // Les mots finissant par "eau" prennent tous un "x" au pluriel
79
-        ['/(eau)x$/i', '\1'],
80
-
81
-        // Words finishing with "al" are pluralized with a "aux" expected
82
-        // Les mots finissant en "al" se terminent en "aux" sauf
83
-        ['/(amir|anim|arsen|boc|can|capit|capor|chev|crist|génér|hopit|hôpit|idé|journ|littor|loc|m|mét|minér|princip|radic|termin)aux$/i', '\1al'],
84
-
85
-        // Words finishing with "au" are pluralized with a "x" excepted "landau"
86
-        // Les mots finissant par "au" prennent un "x" au pluriel sauf "landau"
87
-        ['/(au)x$/i', '\1'],
88
-
89
-        // Words finishing with "eu" are pluralized with a "x" excepted "pneu", "bleu", "émeu"
90
-        // Les mots finissant en "eu" prennent un "x" au pluriel sauf "pneu", "bleu", "émeu"
91
-        ['/(eu)x$/i', '\1'],
92
-
93
-        //  Words finishing with "ou" are pluralized with a "s" excepted bijou, caillou, chou, genou, hibou, joujou, pou
94
-        // Les mots finissant par "ou" prennent un "s" sauf bijou, caillou, chou, genou, hibou, joujou, pou
95
-        ['/(bij|caill|ch|gen|hib|jouj|p)oux$/i', '\1ou'],
96
-
97
-        // French titles
98
-        ['/^mes(dame|demoiselle)s$/', 'ma\1'],
99
-        ['/^Mes(dame|demoiselle)s$/', 'Ma\1'],
100
-        ['/^mes(sieur|seigneur)s$/', 'mon\1'],
101
-        ['/^Mes(sieur|seigneur)s$/', 'Mon\1'],
102
-
103
-        //Default rule
104
-        ['/s$/i', ''],
105
-    ];
106
-
107
-    /**
108
-     * A list of words which should not be inflected.
109
-     * This list is only used by singularize.
110
-     */
111
-    private const UNINFLECTED = '/^(abcès|accès|abus|albatros|anchois|anglais|autobus|bois|brebis|carquois|cas|chas|colis|concours|corps|cours|cyprès|décès|devis|discours|dos|embarras|engrais|entrelacs|excès|fils|fois|gâchis|gars|glas|héros|intrus|jars|jus|kermès|lacis|legs|lilas|marais|mars|matelas|mépris|mets|mois|mors|obus|os|palais|paradis|parcours|pardessus|pays|plusieurs|poids|pois|pouls|printemps|processus|progrès|puits|pus|rabais|radis|recors|recours|refus|relais|remords|remous|rictus|rhinocéros|repas|rubis|sas|secours|sens|souris|succès|talus|tapis|tas|taudis|temps|tiers|univers|velours|verglas|vernis|virus)$/i';
112
-
113
-    /**
114
-     * {@inheritdoc}
115
-     */
116
-    public function singularize(string $plural): array
117
-    {
118
-        if ($this->isInflectedWord($plural)) {
119
-            return [$plural];
120
-        }
121
-
122
-        foreach (self::SINGULARIZE_REGEXP as $rule) {
123
-            [$regexp, $replace] = $rule;
124
-
125
-            if (1 === preg_match($regexp, $plural)) {
126
-                return [preg_replace($regexp, $replace, $plural)];
127
-            }
128
-        }
129
-
130
-        return [$plural];
131
-    }
132
-
133
-    /**
134
-     * {@inheritdoc}
135
-     */
136
-    public function pluralize(string $singular): array
137
-    {
138
-        if ($this->isInflectedWord($singular)) {
139
-            return [$singular];
140
-        }
141
-
142
-        foreach (self::PLURALIZE_REGEXP as $rule) {
143
-            [$regexp, $replace] = $rule;
144
-
145
-            if (1 === preg_match($regexp, $singular)) {
146
-                return [preg_replace($regexp, $replace, $singular)];
147
-            }
148
-        }
149
-
150
-        return [$singular.'s'];
151
-    }
152
-
153
-    private function isInflectedWord(string $word): bool
154
-    {
155
-        return 1 === preg_match(self::UNINFLECTED, $word);
156
-    }
21
+	/**
22
+	 * A list of all rules for pluralise.
23
+	 *
24
+	 * @see https://la-conjugaison.nouvelobs.com/regles/grammaire/le-pluriel-des-noms-121.php
25
+	 */
26
+	private const PLURALIZE_REGEXP = [
27
+		// First entry: regexp
28
+		// Second entry: replacement
29
+
30
+		// Words finishing with "s", "x" or "z" are invariables
31
+		// Les mots finissant par "s", "x" ou "z" sont invariables
32
+		['/(s|x|z)$/i', '\1'],
33
+
34
+		// Words finishing with "eau" are pluralized with a "x"
35
+		// Les mots finissant par "eau" prennent tous un "x" au pluriel
36
+		['/(eau)$/i', '\1x'],
37
+
38
+		// Words finishing with "au" are pluralized with a "x" excepted "landau"
39
+		// Les mots finissant par "au" prennent un "x" au pluriel sauf "landau"
40
+		['/^(landau)$/i', '\1s'],
41
+		['/(au)$/i', '\1x'],
42
+
43
+		// Words finishing with "eu" are pluralized with a "x" excepted "pneu", "bleu", "émeu"
44
+		// Les mots finissant en "eu" prennent un "x" au pluriel sauf "pneu", "bleu", "émeu"
45
+		['/^(pneu|bleu|émeu)$/i', '\1s'],
46
+		['/(eu)$/i', '\1x'],
47
+
48
+		// Words finishing with "al" are pluralized with a "aux" excepted
49
+		// Les mots finissant en "al" se terminent en "aux" sauf
50
+		['/^(bal|carnaval|caracal|chacal|choral|corral|étal|festival|récital|val)$/i', '\1s'],
51
+		['/al$/i', '\1aux'],
52
+
53
+		// Aspirail, bail, corail, émail, fermail, soupirail, travail, vantail et vitrail font leur pluriel en -aux
54
+		['/^(aspir|b|cor|ém|ferm|soupir|trav|vant|vitr)ail$/i', '\1aux'],
55
+
56
+		// Bijou, caillou, chou, genou, hibou, joujou et pou qui prennent un x au pluriel
57
+		['/^(bij|caill|ch|gen|hib|jouj|p)ou$/i', '\1oux'],
58
+
59
+		// Invariable words
60
+		['/^(cinquante|soixante|mille)$/i', '\1'],
61
+
62
+		// French titles
63
+		['/^(mon|ma)(sieur|dame|demoiselle|seigneur)$/', 'mes\2s'],
64
+		['/^(Mon|Ma)(sieur|dame|demoiselle|seigneur)$/', 'Mes\2s'],
65
+	];
66
+
67
+	/**
68
+	 * A list of all rules for singularize.
69
+	 */
70
+	private const SINGULARIZE_REGEXP = [
71
+		// First entry: regexp
72
+		// Second entry: replacement
73
+
74
+		// Aspirail, bail, corail, émail, fermail, soupirail, travail, vantail et vitrail font leur pluriel en -aux
75
+		['/((aspir|b|cor|ém|ferm|soupir|trav|vant|vitr))aux$/i', '\1ail'],
76
+
77
+		// Words finishing with "eau" are pluralized with a "x"
78
+		// Les mots finissant par "eau" prennent tous un "x" au pluriel
79
+		['/(eau)x$/i', '\1'],
80
+
81
+		// Words finishing with "al" are pluralized with a "aux" expected
82
+		// Les mots finissant en "al" se terminent en "aux" sauf
83
+		['/(amir|anim|arsen|boc|can|capit|capor|chev|crist|génér|hopit|hôpit|idé|journ|littor|loc|m|mét|minér|princip|radic|termin)aux$/i', '\1al'],
84
+
85
+		// Words finishing with "au" are pluralized with a "x" excepted "landau"
86
+		// Les mots finissant par "au" prennent un "x" au pluriel sauf "landau"
87
+		['/(au)x$/i', '\1'],
88
+
89
+		// Words finishing with "eu" are pluralized with a "x" excepted "pneu", "bleu", "émeu"
90
+		// Les mots finissant en "eu" prennent un "x" au pluriel sauf "pneu", "bleu", "émeu"
91
+		['/(eu)x$/i', '\1'],
92
+
93
+		//  Words finishing with "ou" are pluralized with a "s" excepted bijou, caillou, chou, genou, hibou, joujou, pou
94
+		// Les mots finissant par "ou" prennent un "s" sauf bijou, caillou, chou, genou, hibou, joujou, pou
95
+		['/(bij|caill|ch|gen|hib|jouj|p)oux$/i', '\1ou'],
96
+
97
+		// French titles
98
+		['/^mes(dame|demoiselle)s$/', 'ma\1'],
99
+		['/^Mes(dame|demoiselle)s$/', 'Ma\1'],
100
+		['/^mes(sieur|seigneur)s$/', 'mon\1'],
101
+		['/^Mes(sieur|seigneur)s$/', 'Mon\1'],
102
+
103
+		//Default rule
104
+		['/s$/i', ''],
105
+	];
106
+
107
+	/**
108
+	 * A list of words which should not be inflected.
109
+	 * This list is only used by singularize.
110
+	 */
111
+	private const UNINFLECTED = '/^(abcès|accès|abus|albatros|anchois|anglais|autobus|bois|brebis|carquois|cas|chas|colis|concours|corps|cours|cyprès|décès|devis|discours|dos|embarras|engrais|entrelacs|excès|fils|fois|gâchis|gars|glas|héros|intrus|jars|jus|kermès|lacis|legs|lilas|marais|mars|matelas|mépris|mets|mois|mors|obus|os|palais|paradis|parcours|pardessus|pays|plusieurs|poids|pois|pouls|printemps|processus|progrès|puits|pus|rabais|radis|recors|recours|refus|relais|remords|remous|rictus|rhinocéros|repas|rubis|sas|secours|sens|souris|succès|talus|tapis|tas|taudis|temps|tiers|univers|velours|verglas|vernis|virus)$/i';
112
+
113
+	/**
114
+	 * {@inheritdoc}
115
+	 */
116
+	public function singularize(string $plural): array
117
+	{
118
+		if ($this->isInflectedWord($plural)) {
119
+			return [$plural];
120
+		}
121
+
122
+		foreach (self::SINGULARIZE_REGEXP as $rule) {
123
+			[$regexp, $replace] = $rule;
124
+
125
+			if (1 === preg_match($regexp, $plural)) {
126
+				return [preg_replace($regexp, $replace, $plural)];
127
+			}
128
+		}
129
+
130
+		return [$plural];
131
+	}
132
+
133
+	/**
134
+	 * {@inheritdoc}
135
+	 */
136
+	public function pluralize(string $singular): array
137
+	{
138
+		if ($this->isInflectedWord($singular)) {
139
+			return [$singular];
140
+		}
141
+
142
+		foreach (self::PLURALIZE_REGEXP as $rule) {
143
+			[$regexp, $replace] = $rule;
144
+
145
+			if (1 === preg_match($regexp, $singular)) {
146
+				return [preg_replace($regexp, $replace, $singular)];
147
+			}
148
+		}
149
+
150
+		return [$singular.'s'];
151
+	}
152
+
153
+	private function isInflectedWord(string $word): bool
154
+	{
155
+		return 1 === preg_match(self::UNINFLECTED, $word);
156
+	}
157 157
 }
Please login to merge, or discard this patch.
vendor/symfony/string/Inflector/InflectorInterface.php 1 patch
Indentation   +16 added lines, -16 removed lines patch added patch discarded remove patch
@@ -13,21 +13,21 @@
 block discarded – undo
13 13
 
14 14
 interface InflectorInterface
15 15
 {
16
-    /**
17
-     * Returns the singular forms of a string.
18
-     *
19
-     * If the method can't determine the form with certainty, several possible singulars are returned.
20
-     *
21
-     * @return string[] An array of possible singular forms
22
-     */
23
-    public function singularize(string $plural): array;
16
+	/**
17
+	 * Returns the singular forms of a string.
18
+	 *
19
+	 * If the method can't determine the form with certainty, several possible singulars are returned.
20
+	 *
21
+	 * @return string[] An array of possible singular forms
22
+	 */
23
+	public function singularize(string $plural): array;
24 24
 
25
-    /**
26
-     * Returns the plural forms of a string.
27
-     *
28
-     * If the method can't determine the form with certainty, several possible plurals are returned.
29
-     *
30
-     * @return string[] An array of possible plural forms
31
-     */
32
-    public function pluralize(string $singular): array;
25
+	/**
26
+	 * Returns the plural forms of a string.
27
+	 *
28
+	 * If the method can't determine the form with certainty, several possible plurals are returned.
29
+	 *
30
+	 * @return string[] An array of possible plural forms
31
+	 */
32
+	public function pluralize(string $singular): array;
33 33
 }
Please login to merge, or discard this patch.
vendor/symfony/string/Inflector/EnglishInflector.php 1 patch
Indentation   +392 added lines, -392 removed lines patch added patch discarded remove patch
@@ -13,496 +13,496 @@
 block discarded – undo
13 13
 
14 14
 final class EnglishInflector implements InflectorInterface
15 15
 {
16
-    /**
17
-     * Map English plural to singular suffixes.
18
-     *
19
-     * @see http://english-zone.com/spelling/plurals.html
20
-     */
21
-    private const PLURAL_MAP = [
22
-        // First entry: plural suffix, reversed
23
-        // Second entry: length of plural suffix
24
-        // Third entry: Whether the suffix may succeed a vocal
25
-        // Fourth entry: Whether the suffix may succeed a consonant
26
-        // Fifth entry: singular suffix, normal
16
+	/**
17
+	 * Map English plural to singular suffixes.
18
+	 *
19
+	 * @see http://english-zone.com/spelling/plurals.html
20
+	 */
21
+	private const PLURAL_MAP = [
22
+		// First entry: plural suffix, reversed
23
+		// Second entry: length of plural suffix
24
+		// Third entry: Whether the suffix may succeed a vocal
25
+		// Fourth entry: Whether the suffix may succeed a consonant
26
+		// Fifth entry: singular suffix, normal
27 27
 
28
-        // bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
29
-        ['a', 1, true, true, ['on', 'um']],
28
+		// bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
29
+		['a', 1, true, true, ['on', 'um']],
30 30
 
31
-        // nebulae (nebula)
32
-        ['ea', 2, true, true, 'a'],
31
+		// nebulae (nebula)
32
+		['ea', 2, true, true, 'a'],
33 33
 
34
-        // services (service)
35
-        ['secivres', 8, true, true, 'service'],
34
+		// services (service)
35
+		['secivres', 8, true, true, 'service'],
36 36
 
37
-        // mice (mouse), lice (louse)
38
-        ['eci', 3, false, true, 'ouse'],
37
+		// mice (mouse), lice (louse)
38
+		['eci', 3, false, true, 'ouse'],
39 39
 
40
-        // geese (goose)
41
-        ['esee', 4, false, true, 'oose'],
40
+		// geese (goose)
41
+		['esee', 4, false, true, 'oose'],
42 42
 
43
-        // fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius)
44
-        ['i', 1, true, true, 'us'],
43
+		// fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius)
44
+		['i', 1, true, true, 'us'],
45 45
 
46
-        // men (man), women (woman)
47
-        ['nem', 3, true, true, 'man'],
46
+		// men (man), women (woman)
47
+		['nem', 3, true, true, 'man'],
48 48
 
49
-        // children (child)
50
-        ['nerdlihc', 8, true, true, 'child'],
49
+		// children (child)
50
+		['nerdlihc', 8, true, true, 'child'],
51 51
 
52
-        // oxen (ox)
53
-        ['nexo', 4, false, false, 'ox'],
52
+		// oxen (ox)
53
+		['nexo', 4, false, false, 'ox'],
54 54
 
55
-        // indices (index), appendices (appendix), prices (price)
56
-        ['seci', 4, false, true, ['ex', 'ix', 'ice']],
55
+		// indices (index), appendices (appendix), prices (price)
56
+		['seci', 4, false, true, ['ex', 'ix', 'ice']],
57 57
 
58
-        // selfies (selfie)
59
-        ['seifles', 7, true, true, 'selfie'],
58
+		// selfies (selfie)
59
+		['seifles', 7, true, true, 'selfie'],
60 60
 
61
-        // movies (movie)
62
-        ['seivom', 6, true, true, 'movie'],
61
+		// movies (movie)
62
+		['seivom', 6, true, true, 'movie'],
63 63
 
64
-        // conspectuses (conspectus), prospectuses (prospectus)
65
-        ['sesutcep', 8, true, true, 'pectus'],
64
+		// conspectuses (conspectus), prospectuses (prospectus)
65
+		['sesutcep', 8, true, true, 'pectus'],
66 66
 
67
-        // feet (foot)
68
-        ['teef', 4, true, true, 'foot'],
67
+		// feet (foot)
68
+		['teef', 4, true, true, 'foot'],
69 69
 
70
-        // geese (goose)
71
-        ['eseeg', 5, true, true, 'goose'],
70
+		// geese (goose)
71
+		['eseeg', 5, true, true, 'goose'],
72 72
 
73
-        // teeth (tooth)
74
-        ['hteet', 5, true, true, 'tooth'],
73
+		// teeth (tooth)
74
+		['hteet', 5, true, true, 'tooth'],
75 75
 
76
-        // news (news)
77
-        ['swen', 4, true, true, 'news'],
76
+		// news (news)
77
+		['swen', 4, true, true, 'news'],
78 78
 
79
-        // series (series)
80
-        ['seires', 6, true, true, 'series'],
79
+		// series (series)
80
+		['seires', 6, true, true, 'series'],
81 81
 
82
-        // babies (baby)
83
-        ['sei', 3, false, true, 'y'],
82
+		// babies (baby)
83
+		['sei', 3, false, true, 'y'],
84 84
 
85
-        // accesses (access), addresses (address), kisses (kiss)
86
-        ['sess', 4, true, false, 'ss'],
85
+		// accesses (access), addresses (address), kisses (kiss)
86
+		['sess', 4, true, false, 'ss'],
87 87
 
88
-        // analyses (analysis), ellipses (ellipsis), fungi (fungus),
89
-        // neuroses (neurosis), theses (thesis), emphases (emphasis),
90
-        // oases (oasis), crises (crisis), houses (house), bases (base),
91
-        // atlases (atlas)
92
-        ['ses', 3, true, true, ['s', 'se', 'sis']],
88
+		// analyses (analysis), ellipses (ellipsis), fungi (fungus),
89
+		// neuroses (neurosis), theses (thesis), emphases (emphasis),
90
+		// oases (oasis), crises (crisis), houses (house), bases (base),
91
+		// atlases (atlas)
92
+		['ses', 3, true, true, ['s', 'se', 'sis']],
93 93
 
94
-        // objectives (objective), alternative (alternatives)
95
-        ['sevit', 5, true, true, 'tive'],
94
+		// objectives (objective), alternative (alternatives)
95
+		['sevit', 5, true, true, 'tive'],
96 96
 
97
-        // drives (drive)
98
-        ['sevird', 6, false, true, 'drive'],
97
+		// drives (drive)
98
+		['sevird', 6, false, true, 'drive'],
99 99
 
100
-        // lives (life), wives (wife)
101
-        ['sevi', 4, false, true, 'ife'],
100
+		// lives (life), wives (wife)
101
+		['sevi', 4, false, true, 'ife'],
102 102
 
103
-        // moves (move)
104
-        ['sevom', 5, true, true, 'move'],
103
+		// moves (move)
104
+		['sevom', 5, true, true, 'move'],
105 105
 
106
-        // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf), caves (cave), staves (staff)
107
-        ['sev', 3, true, true, ['f', 've', 'ff']],
106
+		// hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf), caves (cave), staves (staff)
107
+		['sev', 3, true, true, ['f', 've', 'ff']],
108 108
 
109
-        // axes (axis), axes (ax), axes (axe)
110
-        ['sexa', 4, false, false, ['ax', 'axe', 'axis']],
109
+		// axes (axis), axes (ax), axes (axe)
110
+		['sexa', 4, false, false, ['ax', 'axe', 'axis']],
111 111
 
112
-        // indexes (index), matrixes (matrix)
113
-        ['sex', 3, true, false, 'x'],
112
+		// indexes (index), matrixes (matrix)
113
+		['sex', 3, true, false, 'x'],
114 114
 
115
-        // quizzes (quiz)
116
-        ['sezz', 4, true, false, 'z'],
115
+		// quizzes (quiz)
116
+		['sezz', 4, true, false, 'z'],
117 117
 
118
-        // bureaus (bureau)
119
-        ['suae', 4, false, true, 'eau'],
118
+		// bureaus (bureau)
119
+		['suae', 4, false, true, 'eau'],
120 120
 
121
-        // fees (fee), trees (tree), employees (employee)
122
-        ['see', 3, true, true, 'ee'],
121
+		// fees (fee), trees (tree), employees (employee)
122
+		['see', 3, true, true, 'ee'],
123 123
 
124
-        // edges (edge)
125
-        ['segd', 4, true, true, 'dge'],
124
+		// edges (edge)
125
+		['segd', 4, true, true, 'dge'],
126 126
 
127
-        // roses (rose), garages (garage), cassettes (cassette),
128
-        // waltzes (waltz), heroes (hero), bushes (bush), arches (arch),
129
-        // shoes (shoe)
130
-        ['se', 2, true, true, ['', 'e']],
127
+		// roses (rose), garages (garage), cassettes (cassette),
128
+		// waltzes (waltz), heroes (hero), bushes (bush), arches (arch),
129
+		// shoes (shoe)
130
+		['se', 2, true, true, ['', 'e']],
131 131
 
132
-        // tags (tag)
133
-        ['s', 1, true, true, ''],
132
+		// tags (tag)
133
+		['s', 1, true, true, ''],
134 134
 
135
-        // chateaux (chateau)
136
-        ['xuae', 4, false, true, 'eau'],
135
+		// chateaux (chateau)
136
+		['xuae', 4, false, true, 'eau'],
137 137
 
138
-        // people (person)
139
-        ['elpoep', 6, true, true, 'person'],
140
-    ];
138
+		// people (person)
139
+		['elpoep', 6, true, true, 'person'],
140
+	];
141 141
 
142
-    /**
143
-     * Map English singular to plural suffixes.
144
-     *
145
-     * @see http://english-zone.com/spelling/plurals.html
146
-     */
147
-    private const SINGULAR_MAP = [
148
-        // First entry: singular suffix, reversed
149
-        // Second entry: length of singular suffix
150
-        // Third entry: Whether the suffix may succeed a vocal
151
-        // Fourth entry: Whether the suffix may succeed a consonant
152
-        // Fifth entry: plural suffix, normal
142
+	/**
143
+	 * Map English singular to plural suffixes.
144
+	 *
145
+	 * @see http://english-zone.com/spelling/plurals.html
146
+	 */
147
+	private const SINGULAR_MAP = [
148
+		// First entry: singular suffix, reversed
149
+		// Second entry: length of singular suffix
150
+		// Third entry: Whether the suffix may succeed a vocal
151
+		// Fourth entry: Whether the suffix may succeed a consonant
152
+		// Fifth entry: plural suffix, normal
153 153
 
154
-        // criterion (criteria)
155
-        ['airetirc', 8, false, false, 'criterion'],
154
+		// criterion (criteria)
155
+		['airetirc', 8, false, false, 'criterion'],
156 156
 
157
-        // nebulae (nebula)
158
-        ['aluben', 6, false, false, 'nebulae'],
157
+		// nebulae (nebula)
158
+		['aluben', 6, false, false, 'nebulae'],
159 159
 
160
-        // children (child)
161
-        ['dlihc', 5, true, true, 'children'],
160
+		// children (child)
161
+		['dlihc', 5, true, true, 'children'],
162 162
 
163
-        // prices (price)
164
-        ['eci', 3, false, true, 'ices'],
163
+		// prices (price)
164
+		['eci', 3, false, true, 'ices'],
165 165
 
166
-        // services (service)
167
-        ['ecivres', 7, true, true, 'services'],
166
+		// services (service)
167
+		['ecivres', 7, true, true, 'services'],
168 168
 
169
-        // lives (life), wives (wife)
170
-        ['efi', 3, false, true, 'ives'],
169
+		// lives (life), wives (wife)
170
+		['efi', 3, false, true, 'ives'],
171 171
 
172
-        // selfies (selfie)
173
-        ['eifles', 6, true, true, 'selfies'],
172
+		// selfies (selfie)
173
+		['eifles', 6, true, true, 'selfies'],
174 174
 
175
-        // movies (movie)
176
-        ['eivom', 5, true, true, 'movies'],
175
+		// movies (movie)
176
+		['eivom', 5, true, true, 'movies'],
177 177
 
178
-        // lice (louse)
179
-        ['esuol', 5, false, true, 'lice'],
178
+		// lice (louse)
179
+		['esuol', 5, false, true, 'lice'],
180 180
 
181
-        // mice (mouse)
182
-        ['esuom', 5, false, true, 'mice'],
181
+		// mice (mouse)
182
+		['esuom', 5, false, true, 'mice'],
183 183
 
184
-        // geese (goose)
185
-        ['esoo', 4, false, true, 'eese'],
184
+		// geese (goose)
185
+		['esoo', 4, false, true, 'eese'],
186 186
 
187
-        // houses (house), bases (base)
188
-        ['es', 2, true, true, 'ses'],
187
+		// houses (house), bases (base)
188
+		['es', 2, true, true, 'ses'],
189 189
 
190
-        // geese (goose)
191
-        ['esoog', 5, true, true, 'geese'],
190
+		// geese (goose)
191
+		['esoog', 5, true, true, 'geese'],
192 192
 
193
-        // caves (cave)
194
-        ['ev', 2, true, true, 'ves'],
193
+		// caves (cave)
194
+		['ev', 2, true, true, 'ves'],
195 195
 
196
-        // drives (drive)
197
-        ['evird', 5, false, true, 'drives'],
196
+		// drives (drive)
197
+		['evird', 5, false, true, 'drives'],
198 198
 
199
-        // objectives (objective), alternative (alternatives)
200
-        ['evit', 4, true, true, 'tives'],
199
+		// objectives (objective), alternative (alternatives)
200
+		['evit', 4, true, true, 'tives'],
201 201
 
202
-        // moves (move)
203
-        ['evom', 4, true, true, 'moves'],
202
+		// moves (move)
203
+		['evom', 4, true, true, 'moves'],
204 204
 
205
-        // staves (staff)
206
-        ['ffats', 5, true, true, 'staves'],
205
+		// staves (staff)
206
+		['ffats', 5, true, true, 'staves'],
207 207
 
208
-        // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf)
209
-        ['ff', 2, true, true, 'ffs'],
208
+		// hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf)
209
+		['ff', 2, true, true, 'ffs'],
210 210
 
211
-        // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf)
212
-        ['f', 1, true, true, ['fs', 'ves']],
211
+		// hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf)
212
+		['f', 1, true, true, ['fs', 'ves']],
213 213
 
214
-        // arches (arch)
215
-        ['hc', 2, true, true, 'ches'],
214
+		// arches (arch)
215
+		['hc', 2, true, true, 'ches'],
216 216
 
217
-        // bushes (bush)
218
-        ['hs', 2, true, true, 'shes'],
217
+		// bushes (bush)
218
+		['hs', 2, true, true, 'shes'],
219 219
 
220
-        // teeth (tooth)
221
-        ['htoot', 5, true, true, 'teeth'],
220
+		// teeth (tooth)
221
+		['htoot', 5, true, true, 'teeth'],
222 222
 
223
-        // bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
224
-        ['mu', 2, true, true, 'a'],
223
+		// bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
224
+		['mu', 2, true, true, 'a'],
225 225
 
226
-        // men (man), women (woman)
227
-        ['nam', 3, true, true, 'men'],
226
+		// men (man), women (woman)
227
+		['nam', 3, true, true, 'men'],
228 228
 
229
-        // people (person)
230
-        ['nosrep', 6, true, true, ['persons', 'people']],
229
+		// people (person)
230
+		['nosrep', 6, true, true, ['persons', 'people']],
231 231
 
232
-        // bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
233
-        ['noi', 3, true, true, 'ions'],
232
+		// bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
233
+		['noi', 3, true, true, 'ions'],
234 234
 
235
-        // coupon (coupons)
236
-        ['nop', 3, true, true, 'pons'],
235
+		// coupon (coupons)
236
+		['nop', 3, true, true, 'pons'],
237 237
 
238
-        // seasons (season), treasons (treason), poisons (poison), lessons (lesson)
239
-        ['nos', 3, true, true, 'sons'],
238
+		// seasons (season), treasons (treason), poisons (poison), lessons (lesson)
239
+		['nos', 3, true, true, 'sons'],
240 240
 
241
-        // bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
242
-        ['no', 2, true, true, 'a'],
241
+		// bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
242
+		['no', 2, true, true, 'a'],
243 243
 
244
-        // echoes (echo)
245
-        ['ohce', 4, true, true, 'echoes'],
244
+		// echoes (echo)
245
+		['ohce', 4, true, true, 'echoes'],
246 246
 
247
-        // heroes (hero)
248
-        ['oreh', 4, true, true, 'heroes'],
247
+		// heroes (hero)
248
+		['oreh', 4, true, true, 'heroes'],
249 249
 
250
-        // atlases (atlas)
251
-        ['salta', 5, true, true, 'atlases'],
250
+		// atlases (atlas)
251
+		['salta', 5, true, true, 'atlases'],
252 252
 
253
-        // irises (iris)
254
-        ['siri', 4, true, true, 'irises'],
253
+		// irises (iris)
254
+		['siri', 4, true, true, 'irises'],
255 255
 
256
-        // analyses (analysis), ellipses (ellipsis), neuroses (neurosis)
257
-        // theses (thesis), emphases (emphasis), oases (oasis),
258
-        // crises (crisis)
259
-        ['sis', 3, true, true, 'ses'],
256
+		// analyses (analysis), ellipses (ellipsis), neuroses (neurosis)
257
+		// theses (thesis), emphases (emphasis), oases (oasis),
258
+		// crises (crisis)
259
+		['sis', 3, true, true, 'ses'],
260 260
 
261
-        // accesses (access), addresses (address), kisses (kiss)
262
-        ['ss', 2, true, false, 'sses'],
261
+		// accesses (access), addresses (address), kisses (kiss)
262
+		['ss', 2, true, false, 'sses'],
263 263
 
264
-        // syllabi (syllabus)
265
-        ['suballys', 8, true, true, 'syllabi'],
264
+		// syllabi (syllabus)
265
+		['suballys', 8, true, true, 'syllabi'],
266 266
 
267
-        // buses (bus)
268
-        ['sub', 3, true, true, 'buses'],
267
+		// buses (bus)
268
+		['sub', 3, true, true, 'buses'],
269 269
 
270
-        // circuses (circus)
271
-        ['suc', 3, true, true, 'cuses'],
270
+		// circuses (circus)
271
+		['suc', 3, true, true, 'cuses'],
272 272
 
273
-        // conspectuses (conspectus), prospectuses (prospectus)
274
-        ['sutcep', 6, true, true, 'pectuses'],
273
+		// conspectuses (conspectus), prospectuses (prospectus)
274
+		['sutcep', 6, true, true, 'pectuses'],
275 275
 
276
-        // fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius)
277
-        ['su', 2, true, true, 'i'],
276
+		// fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius)
277
+		['su', 2, true, true, 'i'],
278 278
 
279
-        // news (news)
280
-        ['swen', 4, true, true, 'news'],
279
+		// news (news)
280
+		['swen', 4, true, true, 'news'],
281 281
 
282
-        // feet (foot)
283
-        ['toof', 4, true, true, 'feet'],
282
+		// feet (foot)
283
+		['toof', 4, true, true, 'feet'],
284 284
 
285
-        // chateaux (chateau), bureaus (bureau)
286
-        ['uae', 3, false, true, ['eaus', 'eaux']],
285
+		// chateaux (chateau), bureaus (bureau)
286
+		['uae', 3, false, true, ['eaus', 'eaux']],
287 287
 
288
-        // oxen (ox)
289
-        ['xo', 2, false, false, 'oxen'],
288
+		// oxen (ox)
289
+		['xo', 2, false, false, 'oxen'],
290 290
 
291
-        // hoaxes (hoax)
292
-        ['xaoh', 4, true, false, 'hoaxes'],
291
+		// hoaxes (hoax)
292
+		['xaoh', 4, true, false, 'hoaxes'],
293 293
 
294
-        // indices (index)
295
-        ['xedni', 5, false, true, ['indicies', 'indexes']],
294
+		// indices (index)
295
+		['xedni', 5, false, true, ['indicies', 'indexes']],
296 296
 
297
-        // boxes (box)
298
-        ['xo', 2, false, true, 'oxes'],
297
+		// boxes (box)
298
+		['xo', 2, false, true, 'oxes'],
299 299
 
300
-        // indexes (index), matrixes (matrix)
301
-        ['x', 1, true, false, ['cies', 'xes']],
300
+		// indexes (index), matrixes (matrix)
301
+		['x', 1, true, false, ['cies', 'xes']],
302 302
 
303
-        // appendices (appendix)
304
-        ['xi', 2, false, true, 'ices'],
303
+		// appendices (appendix)
304
+		['xi', 2, false, true, 'ices'],
305 305
 
306
-        // babies (baby)
307
-        ['y', 1, false, true, 'ies'],
306
+		// babies (baby)
307
+		['y', 1, false, true, 'ies'],
308 308
 
309
-        // quizzes (quiz)
310
-        ['ziuq', 4, true, false, 'quizzes'],
309
+		// quizzes (quiz)
310
+		['ziuq', 4, true, false, 'quizzes'],
311 311
 
312
-        // waltzes (waltz)
313
-        ['z', 1, true, true, 'zes'],
314
-    ];
312
+		// waltzes (waltz)
313
+		['z', 1, true, true, 'zes'],
314
+	];
315 315
 
316
-    /**
317
-     * A list of words which should not be inflected, reversed.
318
-     */
319
-    private const UNINFLECTED = [
320
-        '',
316
+	/**
317
+	 * A list of words which should not be inflected, reversed.
318
+	 */
319
+	private const UNINFLECTED = [
320
+		'',
321 321
 
322
-        // data
323
-        'atad',
322
+		// data
323
+		'atad',
324 324
 
325
-        // deer
326
-        'reed',
325
+		// deer
326
+		'reed',
327 327
 
328
-        // feedback
329
-        'kcabdeef',
328
+		// feedback
329
+		'kcabdeef',
330 330
 
331
-        // fish
332
-        'hsif',
331
+		// fish
332
+		'hsif',
333 333
 
334
-        // info
335
-        'ofni',
334
+		// info
335
+		'ofni',
336 336
 
337
-        // moose
338
-        'esoom',
337
+		// moose
338
+		'esoom',
339 339
 
340
-        // series
341
-        'seires',
340
+		// series
341
+		'seires',
342 342
 
343
-        // sheep
344
-        'peehs',
343
+		// sheep
344
+		'peehs',
345 345
 
346
-        // species
347
-        'seiceps',
348
-    ];
349
-
350
-    /**
351
-     * {@inheritdoc}
352
-     */
353
-    public function singularize(string $plural): array
354
-    {
355
-        $pluralRev = strrev($plural);
356
-        $lowerPluralRev = strtolower($pluralRev);
357
-        $pluralLength = \strlen($lowerPluralRev);
358
-
359
-        // Check if the word is one which is not inflected, return early if so
360
-        if (\in_array($lowerPluralRev, self::UNINFLECTED, true)) {
361
-            return [$plural];
362
-        }
363
-
364
-        // The outer loop iterates over the entries of the plural table
365
-        // The inner loop $j iterates over the characters of the plural suffix
366
-        // in the plural table to compare them with the characters of the actual
367
-        // given plural suffix
368
-        foreach (self::PLURAL_MAP as $map) {
369
-            $suffix = $map[0];
370
-            $suffixLength = $map[1];
371
-            $j = 0;
372
-
373
-            // Compare characters in the plural table and of the suffix of the
374
-            // given plural one by one
375
-            while ($suffix[$j] === $lowerPluralRev[$j]) {
376
-                // Let $j point to the next character
377
-                ++$j;
378
-
379
-                // Successfully compared the last character
380
-                // Add an entry with the singular suffix to the singular array
381
-                if ($j === $suffixLength) {
382
-                    // Is there any character preceding the suffix in the plural string?
383
-                    if ($j < $pluralLength) {
384
-                        $nextIsVocal = false !== strpos('aeiou', $lowerPluralRev[$j]);
385
-
386
-                        if (!$map[2] && $nextIsVocal) {
387
-                            // suffix may not succeed a vocal but next char is one
388
-                            break;
389
-                        }
390
-
391
-                        if (!$map[3] && !$nextIsVocal) {
392
-                            // suffix may not succeed a consonant but next char is one
393
-                            break;
394
-                        }
395
-                    }
396
-
397
-                    $newBase = substr($plural, 0, $pluralLength - $suffixLength);
398
-                    $newSuffix = $map[4];
399
-
400
-                    // Check whether the first character in the plural suffix
401
-                    // is uppercased. If yes, uppercase the first character in
402
-                    // the singular suffix too
403
-                    $firstUpper = ctype_upper($pluralRev[$j - 1]);
404
-
405
-                    if (\is_array($newSuffix)) {
406
-                        $singulars = [];
407
-
408
-                        foreach ($newSuffix as $newSuffixEntry) {
409
-                            $singulars[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry);
410
-                        }
411
-
412
-                        return $singulars;
413
-                    }
414
-
415
-                    return [$newBase.($firstUpper ? ucfirst($newSuffix) : $newSuffix)];
416
-                }
417
-
418
-                // Suffix is longer than word
419
-                if ($j === $pluralLength) {
420
-                    break;
421
-                }
422
-            }
423
-        }
424
-
425
-        // Assume that plural and singular is identical
426
-        return [$plural];
427
-    }
428
-
429
-    /**
430
-     * {@inheritdoc}
431
-     */
432
-    public function pluralize(string $singular): array
433
-    {
434
-        $singularRev = strrev($singular);
435
-        $lowerSingularRev = strtolower($singularRev);
436
-        $singularLength = \strlen($lowerSingularRev);
437
-
438
-        // Check if the word is one which is not inflected, return early if so
439
-        if (\in_array($lowerSingularRev, self::UNINFLECTED, true)) {
440
-            return [$singular];
441
-        }
442
-
443
-        // The outer loop iterates over the entries of the singular table
444
-        // The inner loop $j iterates over the characters of the singular suffix
445
-        // in the singular table to compare them with the characters of the actual
446
-        // given singular suffix
447
-        foreach (self::SINGULAR_MAP as $map) {
448
-            $suffix = $map[0];
449
-            $suffixLength = $map[1];
450
-            $j = 0;
451
-
452
-            // Compare characters in the singular table and of the suffix of the
453
-            // given plural one by one
454
-
455
-            while ($suffix[$j] === $lowerSingularRev[$j]) {
456
-                // Let $j point to the next character
457
-                ++$j;
458
-
459
-                // Successfully compared the last character
460
-                // Add an entry with the plural suffix to the plural array
461
-                if ($j === $suffixLength) {
462
-                    // Is there any character preceding the suffix in the plural string?
463
-                    if ($j < $singularLength) {
464
-                        $nextIsVocal = false !== strpos('aeiou', $lowerSingularRev[$j]);
465
-
466
-                        if (!$map[2] && $nextIsVocal) {
467
-                            // suffix may not succeed a vocal but next char is one
468
-                            break;
469
-                        }
470
-
471
-                        if (!$map[3] && !$nextIsVocal) {
472
-                            // suffix may not succeed a consonant but next char is one
473
-                            break;
474
-                        }
475
-                    }
476
-
477
-                    $newBase = substr($singular, 0, $singularLength - $suffixLength);
478
-                    $newSuffix = $map[4];
479
-
480
-                    // Check whether the first character in the singular suffix
481
-                    // is uppercased. If yes, uppercase the first character in
482
-                    // the singular suffix too
483
-                    $firstUpper = ctype_upper($singularRev[$j - 1]);
484
-
485
-                    if (\is_array($newSuffix)) {
486
-                        $plurals = [];
487
-
488
-                        foreach ($newSuffix as $newSuffixEntry) {
489
-                            $plurals[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry);
490
-                        }
491
-
492
-                        return $plurals;
493
-                    }
494
-
495
-                    return [$newBase.($firstUpper ? ucfirst($newSuffix) : $newSuffix)];
496
-                }
497
-
498
-                // Suffix is longer than word
499
-                if ($j === $singularLength) {
500
-                    break;
501
-                }
502
-            }
503
-        }
504
-
505
-        // Assume that plural is singular with a trailing `s`
506
-        return [$singular.'s'];
507
-    }
346
+		// species
347
+		'seiceps',
348
+	];
349
+
350
+	/**
351
+	 * {@inheritdoc}
352
+	 */
353
+	public function singularize(string $plural): array
354
+	{
355
+		$pluralRev = strrev($plural);
356
+		$lowerPluralRev = strtolower($pluralRev);
357
+		$pluralLength = \strlen($lowerPluralRev);
358
+
359
+		// Check if the word is one which is not inflected, return early if so
360
+		if (\in_array($lowerPluralRev, self::UNINFLECTED, true)) {
361
+			return [$plural];
362
+		}
363
+
364
+		// The outer loop iterates over the entries of the plural table
365
+		// The inner loop $j iterates over the characters of the plural suffix
366
+		// in the plural table to compare them with the characters of the actual
367
+		// given plural suffix
368
+		foreach (self::PLURAL_MAP as $map) {
369
+			$suffix = $map[0];
370
+			$suffixLength = $map[1];
371
+			$j = 0;
372
+
373
+			// Compare characters in the plural table and of the suffix of the
374
+			// given plural one by one
375
+			while ($suffix[$j] === $lowerPluralRev[$j]) {
376
+				// Let $j point to the next character
377
+				++$j;
378
+
379
+				// Successfully compared the last character
380
+				// Add an entry with the singular suffix to the singular array
381
+				if ($j === $suffixLength) {
382
+					// Is there any character preceding the suffix in the plural string?
383
+					if ($j < $pluralLength) {
384
+						$nextIsVocal = false !== strpos('aeiou', $lowerPluralRev[$j]);
385
+
386
+						if (!$map[2] && $nextIsVocal) {
387
+							// suffix may not succeed a vocal but next char is one
388
+							break;
389
+						}
390
+
391
+						if (!$map[3] && !$nextIsVocal) {
392
+							// suffix may not succeed a consonant but next char is one
393
+							break;
394
+						}
395
+					}
396
+
397
+					$newBase = substr($plural, 0, $pluralLength - $suffixLength);
398
+					$newSuffix = $map[4];
399
+
400
+					// Check whether the first character in the plural suffix
401
+					// is uppercased. If yes, uppercase the first character in
402
+					// the singular suffix too
403
+					$firstUpper = ctype_upper($pluralRev[$j - 1]);
404
+
405
+					if (\is_array($newSuffix)) {
406
+						$singulars = [];
407
+
408
+						foreach ($newSuffix as $newSuffixEntry) {
409
+							$singulars[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry);
410
+						}
411
+
412
+						return $singulars;
413
+					}
414
+
415
+					return [$newBase.($firstUpper ? ucfirst($newSuffix) : $newSuffix)];
416
+				}
417
+
418
+				// Suffix is longer than word
419
+				if ($j === $pluralLength) {
420
+					break;
421
+				}
422
+			}
423
+		}
424
+
425
+		// Assume that plural and singular is identical
426
+		return [$plural];
427
+	}
428
+
429
+	/**
430
+	 * {@inheritdoc}
431
+	 */
432
+	public function pluralize(string $singular): array
433
+	{
434
+		$singularRev = strrev($singular);
435
+		$lowerSingularRev = strtolower($singularRev);
436
+		$singularLength = \strlen($lowerSingularRev);
437
+
438
+		// Check if the word is one which is not inflected, return early if so
439
+		if (\in_array($lowerSingularRev, self::UNINFLECTED, true)) {
440
+			return [$singular];
441
+		}
442
+
443
+		// The outer loop iterates over the entries of the singular table
444
+		// The inner loop $j iterates over the characters of the singular suffix
445
+		// in the singular table to compare them with the characters of the actual
446
+		// given singular suffix
447
+		foreach (self::SINGULAR_MAP as $map) {
448
+			$suffix = $map[0];
449
+			$suffixLength = $map[1];
450
+			$j = 0;
451
+
452
+			// Compare characters in the singular table and of the suffix of the
453
+			// given plural one by one
454
+
455
+			while ($suffix[$j] === $lowerSingularRev[$j]) {
456
+				// Let $j point to the next character
457
+				++$j;
458
+
459
+				// Successfully compared the last character
460
+				// Add an entry with the plural suffix to the plural array
461
+				if ($j === $suffixLength) {
462
+					// Is there any character preceding the suffix in the plural string?
463
+					if ($j < $singularLength) {
464
+						$nextIsVocal = false !== strpos('aeiou', $lowerSingularRev[$j]);
465
+
466
+						if (!$map[2] && $nextIsVocal) {
467
+							// suffix may not succeed a vocal but next char is one
468
+							break;
469
+						}
470
+
471
+						if (!$map[3] && !$nextIsVocal) {
472
+							// suffix may not succeed a consonant but next char is one
473
+							break;
474
+						}
475
+					}
476
+
477
+					$newBase = substr($singular, 0, $singularLength - $suffixLength);
478
+					$newSuffix = $map[4];
479
+
480
+					// Check whether the first character in the singular suffix
481
+					// is uppercased. If yes, uppercase the first character in
482
+					// the singular suffix too
483
+					$firstUpper = ctype_upper($singularRev[$j - 1]);
484
+
485
+					if (\is_array($newSuffix)) {
486
+						$plurals = [];
487
+
488
+						foreach ($newSuffix as $newSuffixEntry) {
489
+							$plurals[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry);
490
+						}
491
+
492
+						return $plurals;
493
+					}
494
+
495
+					return [$newBase.($firstUpper ? ucfirst($newSuffix) : $newSuffix)];
496
+				}
497
+
498
+				// Suffix is longer than word
499
+				if ($j === $singularLength) {
500
+					break;
501
+				}
502
+			}
503
+		}
504
+
505
+		// Assume that plural is singular with a trailing `s`
506
+		return [$singular.'s'];
507
+	}
508 508
 }
Please login to merge, or discard this patch.
vendor/symfony/filesystem/Exception/FileNotFoundException.php 1 patch
Indentation   +11 added lines, -11 removed lines patch added patch discarded remove patch
@@ -19,16 +19,16 @@
 block discarded – undo
19 19
  */
20 20
 class FileNotFoundException extends IOException
21 21
 {
22
-    public function __construct(string $message = null, int $code = 0, \Throwable $previous = null, string $path = null)
23
-    {
24
-        if (null === $message) {
25
-            if (null === $path) {
26
-                $message = 'File could not be found.';
27
-            } else {
28
-                $message = sprintf('File "%s" could not be found.', $path);
29
-            }
30
-        }
22
+	public function __construct(string $message = null, int $code = 0, \Throwable $previous = null, string $path = null)
23
+	{
24
+		if (null === $message) {
25
+			if (null === $path) {
26
+				$message = 'File could not be found.';
27
+			} else {
28
+				$message = sprintf('File "%s" could not be found.', $path);
29
+			}
30
+		}
31 31
 
32
-        parent::__construct($message, $code, $previous, $path);
33
-    }
32
+		parent::__construct($message, $code, $previous, $path);
33
+	}
34 34
 }
Please login to merge, or discard this patch.
vendor/symfony/filesystem/Exception/IOException.php 1 patch
Indentation   +13 added lines, -13 removed lines patch added patch discarded remove patch
@@ -20,20 +20,20 @@
 block discarded – undo
20 20
  */
21 21
 class IOException extends \RuntimeException implements IOExceptionInterface
22 22
 {
23
-    private $path;
23
+	private $path;
24 24
 
25
-    public function __construct(string $message, int $code = 0, \Throwable $previous = null, string $path = null)
26
-    {
27
-        $this->path = $path;
25
+	public function __construct(string $message, int $code = 0, \Throwable $previous = null, string $path = null)
26
+	{
27
+		$this->path = $path;
28 28
 
29
-        parent::__construct($message, $code, $previous);
30
-    }
29
+		parent::__construct($message, $code, $previous);
30
+	}
31 31
 
32
-    /**
33
-     * {@inheritdoc}
34
-     */
35
-    public function getPath()
36
-    {
37
-        return $this->path;
38
-    }
32
+	/**
33
+	 * {@inheritdoc}
34
+	 */
35
+	public function getPath()
36
+	{
37
+		return $this->path;
38
+	}
39 39
 }
Please login to merge, or discard this patch.
vendor/symfony/filesystem/Exception/IOExceptionInterface.php 1 patch
Indentation   +6 added lines, -6 removed lines patch added patch discarded remove patch
@@ -18,10 +18,10 @@
 block discarded – undo
18 18
  */
19 19
 interface IOExceptionInterface extends ExceptionInterface
20 20
 {
21
-    /**
22
-     * Returns the associated path for the exception.
23
-     *
24
-     * @return string|null The path
25
-     */
26
-    public function getPath();
21
+	/**
22
+	 * Returns the associated path for the exception.
23
+	 *
24
+	 * @return string|null The path
25
+	 */
26
+	public function getPath();
27 27
 }
Please login to merge, or discard this patch.
vendor/symfony/filesystem/Filesystem.php 1 patch
Indentation   +733 added lines, -733 removed lines patch added patch discarded remove patch
@@ -22,737 +22,737 @@
 block discarded – undo
22 22
  */
23 23
 class Filesystem
24 24
 {
25
-    private static $lastError;
26
-
27
-    /**
28
-     * Copies a file.
29
-     *
30
-     * If the target file is older than the origin file, it's always overwritten.
31
-     * If the target file is newer, it is overwritten only when the
32
-     * $overwriteNewerFiles option is set to true.
33
-     *
34
-     * @throws FileNotFoundException When originFile doesn't exist
35
-     * @throws IOException           When copy fails
36
-     */
37
-    public function copy(string $originFile, string $targetFile, bool $overwriteNewerFiles = false)
38
-    {
39
-        $originIsLocal = stream_is_local($originFile) || 0 === stripos($originFile, 'file://');
40
-        if ($originIsLocal && !is_file($originFile)) {
41
-            throw new FileNotFoundException(sprintf('Failed to copy "%s" because file does not exist.', $originFile), 0, null, $originFile);
42
-        }
43
-
44
-        $this->mkdir(\dirname($targetFile));
45
-
46
-        $doCopy = true;
47
-        if (!$overwriteNewerFiles && null === parse_url($originFile, \PHP_URL_HOST) && is_file($targetFile)) {
48
-            $doCopy = filemtime($originFile) > filemtime($targetFile);
49
-        }
50
-
51
-        if ($doCopy) {
52
-            // https://bugs.php.net/64634
53
-            if (!$source = self::box('fopen', $originFile, 'r')) {
54
-                throw new IOException(sprintf('Failed to copy "%s" to "%s" because source file could not be opened for reading: ', $originFile, $targetFile).self::$lastError, 0, null, $originFile);
55
-            }
56
-
57
-            // Stream context created to allow files overwrite when using FTP stream wrapper - disabled by default
58
-            if (!$target = self::box('fopen', $targetFile, 'w', false, stream_context_create(['ftp' => ['overwrite' => true]]))) {
59
-                throw new IOException(sprintf('Failed to copy "%s" to "%s" because target file could not be opened for writing: ', $originFile, $targetFile).self::$lastError, 0, null, $originFile);
60
-            }
61
-
62
-            $bytesCopied = stream_copy_to_stream($source, $target);
63
-            fclose($source);
64
-            fclose($target);
65
-            unset($source, $target);
66
-
67
-            if (!is_file($targetFile)) {
68
-                throw new IOException(sprintf('Failed to copy "%s" to "%s".', $originFile, $targetFile), 0, null, $originFile);
69
-            }
70
-
71
-            if ($originIsLocal) {
72
-                // Like `cp`, preserve executable permission bits
73
-                self::box('chmod', $targetFile, fileperms($targetFile) | (fileperms($originFile) & 0111));
74
-
75
-                if ($bytesCopied !== $bytesOrigin = filesize($originFile)) {
76
-                    throw new IOException(sprintf('Failed to copy the whole content of "%s" to "%s" (%g of %g bytes copied).', $originFile, $targetFile, $bytesCopied, $bytesOrigin), 0, null, $originFile);
77
-                }
78
-            }
79
-        }
80
-    }
81
-
82
-    /**
83
-     * Creates a directory recursively.
84
-     *
85
-     * @param string|iterable $dirs The directory path
86
-     *
87
-     * @throws IOException On any directory creation failure
88
-     */
89
-    public function mkdir($dirs, int $mode = 0777)
90
-    {
91
-        foreach ($this->toIterable($dirs) as $dir) {
92
-            if (is_dir($dir)) {
93
-                continue;
94
-            }
95
-
96
-            if (!self::box('mkdir', $dir, $mode, true) && !is_dir($dir)) {
97
-                throw new IOException(sprintf('Failed to create "%s": ', $dir).self::$lastError, 0, null, $dir);
98
-            }
99
-        }
100
-    }
101
-
102
-    /**
103
-     * Checks the existence of files or directories.
104
-     *
105
-     * @param string|iterable $files A filename, an array of files, or a \Traversable instance to check
106
-     *
107
-     * @return bool true if the file exists, false otherwise
108
-     */
109
-    public function exists($files)
110
-    {
111
-        $maxPathLength = \PHP_MAXPATHLEN - 2;
112
-
113
-        foreach ($this->toIterable($files) as $file) {
114
-            if (\strlen($file) > $maxPathLength) {
115
-                throw new IOException(sprintf('Could not check if file exist because path length exceeds %d characters.', $maxPathLength), 0, null, $file);
116
-            }
117
-
118
-            if (!file_exists($file)) {
119
-                return false;
120
-            }
121
-        }
122
-
123
-        return true;
124
-    }
125
-
126
-    /**
127
-     * Sets access and modification time of file.
128
-     *
129
-     * @param string|iterable $files A filename, an array of files, or a \Traversable instance to create
130
-     * @param int|null        $time  The touch time as a Unix timestamp, if not supplied the current system time is used
131
-     * @param int|null        $atime The access time as a Unix timestamp, if not supplied the current system time is used
132
-     *
133
-     * @throws IOException When touch fails
134
-     */
135
-    public function touch($files, int $time = null, int $atime = null)
136
-    {
137
-        foreach ($this->toIterable($files) as $file) {
138
-            if (!($time ? self::box('touch', $file, $time, $atime) : self::box('touch', $file))) {
139
-                throw new IOException(sprintf('Failed to touch "%s": ', $file).self::$lastError, 0, null, $file);
140
-            }
141
-        }
142
-    }
143
-
144
-    /**
145
-     * Removes files or directories.
146
-     *
147
-     * @param string|iterable $files A filename, an array of files, or a \Traversable instance to remove
148
-     *
149
-     * @throws IOException When removal fails
150
-     */
151
-    public function remove($files)
152
-    {
153
-        if ($files instanceof \Traversable) {
154
-            $files = iterator_to_array($files, false);
155
-        } elseif (!\is_array($files)) {
156
-            $files = [$files];
157
-        }
158
-
159
-        self::doRemove($files, false);
160
-    }
161
-
162
-    private static function doRemove(array $files, bool $isRecursive): void
163
-    {
164
-        $files = array_reverse($files);
165
-        foreach ($files as $file) {
166
-            if (is_link($file)) {
167
-                // See https://bugs.php.net/52176
168
-                if (!(self::box('unlink', $file) || '\\' !== \DIRECTORY_SEPARATOR || self::box('rmdir', $file)) && file_exists($file)) {
169
-                    throw new IOException(sprintf('Failed to remove symlink "%s": ', $file).self::$lastError);
170
-                }
171
-            } elseif (is_dir($file)) {
172
-                if (!$isRecursive) {
173
-                    $tmpName = \dirname(realpath($file)).'/.'.strrev(strtr(base64_encode(random_bytes(2)), '/=', '-.'));
174
-
175
-                    if (file_exists($tmpName)) {
176
-                        try {
177
-                            self::doRemove([$tmpName], true);
178
-                        } catch (IOException $e) {
179
-                        }
180
-                    }
181
-
182
-                    if (!file_exists($tmpName) && self::box('rename', $file, $tmpName)) {
183
-                        $origFile = $file;
184
-                        $file = $tmpName;
185
-                    } else {
186
-                        $origFile = null;
187
-                    }
188
-                }
189
-
190
-                $files = new \FilesystemIterator($file, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS);
191
-                self::doRemove(iterator_to_array($files, true), true);
192
-
193
-                if (!self::box('rmdir', $file) && file_exists($file) && !$isRecursive) {
194
-                    $lastError = self::$lastError;
195
-
196
-                    if (null !== $origFile && self::box('rename', $file, $origFile)) {
197
-                        $file = $origFile;
198
-                    }
199
-
200
-                    throw new IOException(sprintf('Failed to remove directory "%s": ', $file).$lastError);
201
-                }
202
-            } elseif (!self::box('unlink', $file) && (str_contains(self::$lastError, 'Permission denied') || file_exists($file))) {
203
-                throw new IOException(sprintf('Failed to remove file "%s": ', $file).self::$lastError);
204
-            }
205
-        }
206
-    }
207
-
208
-    /**
209
-     * Change mode for an array of files or directories.
210
-     *
211
-     * @param string|iterable $files     A filename, an array of files, or a \Traversable instance to change mode
212
-     * @param int             $mode      The new mode (octal)
213
-     * @param int             $umask     The mode mask (octal)
214
-     * @param bool            $recursive Whether change the mod recursively or not
215
-     *
216
-     * @throws IOException When the change fails
217
-     */
218
-    public function chmod($files, int $mode, int $umask = 0000, bool $recursive = false)
219
-    {
220
-        foreach ($this->toIterable($files) as $file) {
221
-            if ((\PHP_VERSION_ID < 80000 || \is_int($mode)) && !self::box('chmod', $file, $mode & ~$umask)) {
222
-                throw new IOException(sprintf('Failed to chmod file "%s": ', $file).self::$lastError, 0, null, $file);
223
-            }
224
-            if ($recursive && is_dir($file) && !is_link($file)) {
225
-                $this->chmod(new \FilesystemIterator($file), $mode, $umask, true);
226
-            }
227
-        }
228
-    }
229
-
230
-    /**
231
-     * Change the owner of an array of files or directories.
232
-     *
233
-     * @param string|iterable $files     A filename, an array of files, or a \Traversable instance to change owner
234
-     * @param string|int      $user      A user name or number
235
-     * @param bool            $recursive Whether change the owner recursively or not
236
-     *
237
-     * @throws IOException When the change fails
238
-     */
239
-    public function chown($files, $user, bool $recursive = false)
240
-    {
241
-        foreach ($this->toIterable($files) as $file) {
242
-            if ($recursive && is_dir($file) && !is_link($file)) {
243
-                $this->chown(new \FilesystemIterator($file), $user, true);
244
-            }
245
-            if (is_link($file) && \function_exists('lchown')) {
246
-                if (!self::box('lchown', $file, $user)) {
247
-                    throw new IOException(sprintf('Failed to chown file "%s": ', $file).self::$lastError, 0, null, $file);
248
-                }
249
-            } else {
250
-                if (!self::box('chown', $file, $user)) {
251
-                    throw new IOException(sprintf('Failed to chown file "%s": ', $file).self::$lastError, 0, null, $file);
252
-                }
253
-            }
254
-        }
255
-    }
256
-
257
-    /**
258
-     * Change the group of an array of files or directories.
259
-     *
260
-     * @param string|iterable $files     A filename, an array of files, or a \Traversable instance to change group
261
-     * @param string|int      $group     A group name or number
262
-     * @param bool            $recursive Whether change the group recursively or not
263
-     *
264
-     * @throws IOException When the change fails
265
-     */
266
-    public function chgrp($files, $group, bool $recursive = false)
267
-    {
268
-        foreach ($this->toIterable($files) as $file) {
269
-            if ($recursive && is_dir($file) && !is_link($file)) {
270
-                $this->chgrp(new \FilesystemIterator($file), $group, true);
271
-            }
272
-            if (is_link($file) && \function_exists('lchgrp')) {
273
-                if (!self::box('lchgrp', $file, $group)) {
274
-                    throw new IOException(sprintf('Failed to chgrp file "%s": ', $file).self::$lastError, 0, null, $file);
275
-                }
276
-            } else {
277
-                if (!self::box('chgrp', $file, $group)) {
278
-                    throw new IOException(sprintf('Failed to chgrp file "%s": ', $file).self::$lastError, 0, null, $file);
279
-                }
280
-            }
281
-        }
282
-    }
283
-
284
-    /**
285
-     * Renames a file or a directory.
286
-     *
287
-     * @throws IOException When target file or directory already exists
288
-     * @throws IOException When origin cannot be renamed
289
-     */
290
-    public function rename(string $origin, string $target, bool $overwrite = false)
291
-    {
292
-        // we check that target does not exist
293
-        if (!$overwrite && $this->isReadable($target)) {
294
-            throw new IOException(sprintf('Cannot rename because the target "%s" already exists.', $target), 0, null, $target);
295
-        }
296
-
297
-        if (!self::box('rename', $origin, $target)) {
298
-            if (is_dir($origin)) {
299
-                // See https://bugs.php.net/54097 & https://php.net/rename#113943
300
-                $this->mirror($origin, $target, null, ['override' => $overwrite, 'delete' => $overwrite]);
301
-                $this->remove($origin);
302
-
303
-                return;
304
-            }
305
-            throw new IOException(sprintf('Cannot rename "%s" to "%s": ', $origin, $target).self::$lastError, 0, null, $target);
306
-        }
307
-    }
308
-
309
-    /**
310
-     * Tells whether a file exists and is readable.
311
-     *
312
-     * @throws IOException When windows path is longer than 258 characters
313
-     */
314
-    private function isReadable(string $filename): bool
315
-    {
316
-        $maxPathLength = \PHP_MAXPATHLEN - 2;
317
-
318
-        if (\strlen($filename) > $maxPathLength) {
319
-            throw new IOException(sprintf('Could not check if file is readable because path length exceeds %d characters.', $maxPathLength), 0, null, $filename);
320
-        }
321
-
322
-        return is_readable($filename);
323
-    }
324
-
325
-    /**
326
-     * Creates a symbolic link or copy a directory.
327
-     *
328
-     * @throws IOException When symlink fails
329
-     */
330
-    public function symlink(string $originDir, string $targetDir, bool $copyOnWindows = false)
331
-    {
332
-        if ('\\' === \DIRECTORY_SEPARATOR) {
333
-            $originDir = strtr($originDir, '/', '\\');
334
-            $targetDir = strtr($targetDir, '/', '\\');
335
-
336
-            if ($copyOnWindows) {
337
-                $this->mirror($originDir, $targetDir);
338
-
339
-                return;
340
-            }
341
-        }
342
-
343
-        $this->mkdir(\dirname($targetDir));
344
-
345
-        if (is_link($targetDir)) {
346
-            if (readlink($targetDir) === $originDir) {
347
-                return;
348
-            }
349
-            $this->remove($targetDir);
350
-        }
351
-
352
-        if (!self::box('symlink', $originDir, $targetDir)) {
353
-            $this->linkException($originDir, $targetDir, 'symbolic');
354
-        }
355
-    }
356
-
357
-    /**
358
-     * Creates a hard link, or several hard links to a file.
359
-     *
360
-     * @param string|string[] $targetFiles The target file(s)
361
-     *
362
-     * @throws FileNotFoundException When original file is missing or not a file
363
-     * @throws IOException           When link fails, including if link already exists
364
-     */
365
-    public function hardlink(string $originFile, $targetFiles)
366
-    {
367
-        if (!$this->exists($originFile)) {
368
-            throw new FileNotFoundException(null, 0, null, $originFile);
369
-        }
370
-
371
-        if (!is_file($originFile)) {
372
-            throw new FileNotFoundException(sprintf('Origin file "%s" is not a file.', $originFile));
373
-        }
374
-
375
-        foreach ($this->toIterable($targetFiles) as $targetFile) {
376
-            if (is_file($targetFile)) {
377
-                if (fileinode($originFile) === fileinode($targetFile)) {
378
-                    continue;
379
-                }
380
-                $this->remove($targetFile);
381
-            }
382
-
383
-            if (!self::box('link', $originFile, $targetFile)) {
384
-                $this->linkException($originFile, $targetFile, 'hard');
385
-            }
386
-        }
387
-    }
388
-
389
-    /**
390
-     * @param string $linkType Name of the link type, typically 'symbolic' or 'hard'
391
-     */
392
-    private function linkException(string $origin, string $target, string $linkType)
393
-    {
394
-        if (self::$lastError) {
395
-            if ('\\' === \DIRECTORY_SEPARATOR && str_contains(self::$lastError, 'error code(1314)')) {
396
-                throw new IOException(sprintf('Unable to create "%s" link due to error code 1314: \'A required privilege is not held by the client\'. Do you have the required Administrator-rights?', $linkType), 0, null, $target);
397
-            }
398
-        }
399
-        throw new IOException(sprintf('Failed to create "%s" link from "%s" to "%s": ', $linkType, $origin, $target).self::$lastError, 0, null, $target);
400
-    }
401
-
402
-    /**
403
-     * Resolves links in paths.
404
-     *
405
-     * With $canonicalize = false (default)
406
-     *      - if $path does not exist or is not a link, returns null
407
-     *      - if $path is a link, returns the next direct target of the link without considering the existence of the target
408
-     *
409
-     * With $canonicalize = true
410
-     *      - if $path does not exist, returns null
411
-     *      - if $path exists, returns its absolute fully resolved final version
412
-     *
413
-     * @return string|null
414
-     */
415
-    public function readlink(string $path, bool $canonicalize = false)
416
-    {
417
-        if (!$canonicalize && !is_link($path)) {
418
-            return null;
419
-        }
420
-
421
-        if ($canonicalize) {
422
-            if (!$this->exists($path)) {
423
-                return null;
424
-            }
425
-
426
-            if ('\\' === \DIRECTORY_SEPARATOR && \PHP_VERSION_ID < 70410) {
427
-                $path = readlink($path);
428
-            }
429
-
430
-            return realpath($path);
431
-        }
432
-
433
-        if ('\\' === \DIRECTORY_SEPARATOR && \PHP_VERSION_ID < 70400) {
434
-            return realpath($path);
435
-        }
436
-
437
-        return readlink($path);
438
-    }
439
-
440
-    /**
441
-     * Given an existing path, convert it to a path relative to a given starting path.
442
-     *
443
-     * @return string Path of target relative to starting path
444
-     */
445
-    public function makePathRelative(string $endPath, string $startPath)
446
-    {
447
-        if (!$this->isAbsolutePath($startPath)) {
448
-            throw new InvalidArgumentException(sprintf('The start path "%s" is not absolute.', $startPath));
449
-        }
450
-
451
-        if (!$this->isAbsolutePath($endPath)) {
452
-            throw new InvalidArgumentException(sprintf('The end path "%s" is not absolute.', $endPath));
453
-        }
454
-
455
-        // Normalize separators on Windows
456
-        if ('\\' === \DIRECTORY_SEPARATOR) {
457
-            $endPath = str_replace('\\', '/', $endPath);
458
-            $startPath = str_replace('\\', '/', $startPath);
459
-        }
460
-
461
-        $splitDriveLetter = function ($path) {
462
-            return (\strlen($path) > 2 && ':' === $path[1] && '/' === $path[2] && ctype_alpha($path[0]))
463
-                ? [substr($path, 2), strtoupper($path[0])]
464
-                : [$path, null];
465
-        };
466
-
467
-        $splitPath = function ($path) {
468
-            $result = [];
469
-
470
-            foreach (explode('/', trim($path, '/')) as $segment) {
471
-                if ('..' === $segment) {
472
-                    array_pop($result);
473
-                } elseif ('.' !== $segment && '' !== $segment) {
474
-                    $result[] = $segment;
475
-                }
476
-            }
477
-
478
-            return $result;
479
-        };
480
-
481
-        [$endPath, $endDriveLetter] = $splitDriveLetter($endPath);
482
-        [$startPath, $startDriveLetter] = $splitDriveLetter($startPath);
483
-
484
-        $startPathArr = $splitPath($startPath);
485
-        $endPathArr = $splitPath($endPath);
486
-
487
-        if ($endDriveLetter && $startDriveLetter && $endDriveLetter != $startDriveLetter) {
488
-            // End path is on another drive, so no relative path exists
489
-            return $endDriveLetter.':/'.($endPathArr ? implode('/', $endPathArr).'/' : '');
490
-        }
491
-
492
-        // Find for which directory the common path stops
493
-        $index = 0;
494
-        while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) {
495
-            ++$index;
496
-        }
497
-
498
-        // Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels)
499
-        if (1 === \count($startPathArr) && '' === $startPathArr[0]) {
500
-            $depth = 0;
501
-        } else {
502
-            $depth = \count($startPathArr) - $index;
503
-        }
504
-
505
-        // Repeated "../" for each level need to reach the common path
506
-        $traverser = str_repeat('../', $depth);
507
-
508
-        $endPathRemainder = implode('/', \array_slice($endPathArr, $index));
509
-
510
-        // Construct $endPath from traversing to the common path, then to the remaining $endPath
511
-        $relativePath = $traverser.('' !== $endPathRemainder ? $endPathRemainder.'/' : '');
512
-
513
-        return '' === $relativePath ? './' : $relativePath;
514
-    }
515
-
516
-    /**
517
-     * Mirrors a directory to another.
518
-     *
519
-     * Copies files and directories from the origin directory into the target directory. By default:
520
-     *
521
-     *  - existing files in the target directory will be overwritten, except if they are newer (see the `override` option)
522
-     *  - files in the target directory that do not exist in the source directory will not be deleted (see the `delete` option)
523
-     *
524
-     * @param \Traversable|null $iterator Iterator that filters which files and directories to copy, if null a recursive iterator is created
525
-     * @param array             $options  An array of boolean options
526
-     *                                    Valid options are:
527
-     *                                    - $options['override'] If true, target files newer than origin files are overwritten (see copy(), defaults to false)
528
-     *                                    - $options['copy_on_windows'] Whether to copy files instead of links on Windows (see symlink(), defaults to false)
529
-     *                                    - $options['delete'] Whether to delete files that are not in the source directory (defaults to false)
530
-     *
531
-     * @throws IOException When file type is unknown
532
-     */
533
-    public function mirror(string $originDir, string $targetDir, \Traversable $iterator = null, array $options = [])
534
-    {
535
-        $targetDir = rtrim($targetDir, '/\\');
536
-        $originDir = rtrim($originDir, '/\\');
537
-        $originDirLen = \strlen($originDir);
538
-
539
-        if (!$this->exists($originDir)) {
540
-            throw new IOException(sprintf('The origin directory specified "%s" was not found.', $originDir), 0, null, $originDir);
541
-        }
542
-
543
-        // Iterate in destination folder to remove obsolete entries
544
-        if ($this->exists($targetDir) && isset($options['delete']) && $options['delete']) {
545
-            $deleteIterator = $iterator;
546
-            if (null === $deleteIterator) {
547
-                $flags = \FilesystemIterator::SKIP_DOTS;
548
-                $deleteIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($targetDir, $flags), \RecursiveIteratorIterator::CHILD_FIRST);
549
-            }
550
-            $targetDirLen = \strlen($targetDir);
551
-            foreach ($deleteIterator as $file) {
552
-                $origin = $originDir.substr($file->getPathname(), $targetDirLen);
553
-                if (!$this->exists($origin)) {
554
-                    $this->remove($file);
555
-                }
556
-            }
557
-        }
558
-
559
-        $copyOnWindows = $options['copy_on_windows'] ?? false;
560
-
561
-        if (null === $iterator) {
562
-            $flags = $copyOnWindows ? \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS : \FilesystemIterator::SKIP_DOTS;
563
-            $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($originDir, $flags), \RecursiveIteratorIterator::SELF_FIRST);
564
-        }
565
-
566
-        $this->mkdir($targetDir);
567
-        $filesCreatedWhileMirroring = [];
568
-
569
-        foreach ($iterator as $file) {
570
-            if ($file->getPathname() === $targetDir || $file->getRealPath() === $targetDir || isset($filesCreatedWhileMirroring[$file->getRealPath()])) {
571
-                continue;
572
-            }
573
-
574
-            $target = $targetDir.substr($file->getPathname(), $originDirLen);
575
-            $filesCreatedWhileMirroring[$target] = true;
576
-
577
-            if (!$copyOnWindows && is_link($file)) {
578
-                $this->symlink($file->getLinkTarget(), $target);
579
-            } elseif (is_dir($file)) {
580
-                $this->mkdir($target);
581
-            } elseif (is_file($file)) {
582
-                $this->copy($file, $target, $options['override'] ?? false);
583
-            } else {
584
-                throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file);
585
-            }
586
-        }
587
-    }
588
-
589
-    /**
590
-     * Returns whether the file path is an absolute path.
591
-     *
592
-     * @return bool
593
-     */
594
-    public function isAbsolutePath(string $file)
595
-    {
596
-        return '' !== $file && (strspn($file, '/\\', 0, 1)
597
-            || (\strlen($file) > 3 && ctype_alpha($file[0])
598
-                && ':' === $file[1]
599
-                && strspn($file, '/\\', 2, 1)
600
-            )
601
-            || null !== parse_url($file, \PHP_URL_SCHEME)
602
-        );
603
-    }
604
-
605
-    /**
606
-     * Creates a temporary file with support for custom stream wrappers.
607
-     *
608
-     * @param string $prefix The prefix of the generated temporary filename
609
-     *                       Note: Windows uses only the first three characters of prefix
610
-     * @param string $suffix The suffix of the generated temporary filename
611
-     *
612
-     * @return string The new temporary filename (with path), or throw an exception on failure
613
-     */
614
-    public function tempnam(string $dir, string $prefix/*, string $suffix = ''*/)
615
-    {
616
-        $suffix = \func_num_args() > 2 ? func_get_arg(2) : '';
617
-        [$scheme, $hierarchy] = $this->getSchemeAndHierarchy($dir);
618
-
619
-        // If no scheme or scheme is "file" or "gs" (Google Cloud) create temp file in local filesystem
620
-        if ((null === $scheme || 'file' === $scheme || 'gs' === $scheme) && '' === $suffix) {
621
-            // If tempnam failed or no scheme return the filename otherwise prepend the scheme
622
-            if ($tmpFile = self::box('tempnam', $hierarchy, $prefix)) {
623
-                if (null !== $scheme && 'gs' !== $scheme) {
624
-                    return $scheme.'://'.$tmpFile;
625
-                }
626
-
627
-                return $tmpFile;
628
-            }
629
-
630
-            throw new IOException('A temporary file could not be created: '.self::$lastError);
631
-        }
632
-
633
-        // Loop until we create a valid temp file or have reached 10 attempts
634
-        for ($i = 0; $i < 10; ++$i) {
635
-            // Create a unique filename
636
-            $tmpFile = $dir.'/'.$prefix.uniqid(mt_rand(), true).$suffix;
637
-
638
-            // Use fopen instead of file_exists as some streams do not support stat
639
-            // Use mode 'x+' to atomically check existence and create to avoid a TOCTOU vulnerability
640
-            if (!$handle = self::box('fopen', $tmpFile, 'x+')) {
641
-                continue;
642
-            }
643
-
644
-            // Close the file if it was successfully opened
645
-            self::box('fclose', $handle);
646
-
647
-            return $tmpFile;
648
-        }
649
-
650
-        throw new IOException('A temporary file could not be created: '.self::$lastError);
651
-    }
652
-
653
-    /**
654
-     * Atomically dumps content into a file.
655
-     *
656
-     * @param string|resource $content The data to write into the file
657
-     *
658
-     * @throws IOException if the file cannot be written to
659
-     */
660
-    public function dumpFile(string $filename, $content)
661
-    {
662
-        if (\is_array($content)) {
663
-            throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be string or resource, array given.', __METHOD__));
664
-        }
665
-
666
-        $dir = \dirname($filename);
667
-
668
-        if (!is_dir($dir)) {
669
-            $this->mkdir($dir);
670
-        }
671
-
672
-        // Will create a temp file with 0600 access rights
673
-        // when the filesystem supports chmod.
674
-        $tmpFile = $this->tempnam($dir, basename($filename));
675
-
676
-        try {
677
-            if (false === self::box('file_put_contents', $tmpFile, $content)) {
678
-                throw new IOException(sprintf('Failed to write file "%s": ', $filename).self::$lastError, 0, null, $filename);
679
-            }
680
-
681
-            self::box('chmod', $tmpFile, file_exists($filename) ? fileperms($filename) : 0666 & ~umask());
682
-
683
-            $this->rename($tmpFile, $filename, true);
684
-        } finally {
685
-            if (file_exists($tmpFile)) {
686
-                self::box('unlink', $tmpFile);
687
-            }
688
-        }
689
-    }
690
-
691
-    /**
692
-     * Appends content to an existing file.
693
-     *
694
-     * @param string|resource $content The content to append
695
-     *
696
-     * @throws IOException If the file is not writable
697
-     */
698
-    public function appendToFile(string $filename, $content)
699
-    {
700
-        if (\is_array($content)) {
701
-            throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be string or resource, array given.', __METHOD__));
702
-        }
703
-
704
-        $dir = \dirname($filename);
705
-
706
-        if (!is_dir($dir)) {
707
-            $this->mkdir($dir);
708
-        }
709
-
710
-        if (false === self::box('file_put_contents', $filename, $content, \FILE_APPEND)) {
711
-            throw new IOException(sprintf('Failed to write file "%s": ', $filename).self::$lastError, 0, null, $filename);
712
-        }
713
-    }
714
-
715
-    private function toIterable($files): iterable
716
-    {
717
-        return is_iterable($files) ? $files : [$files];
718
-    }
719
-
720
-    /**
721
-     * Gets a 2-tuple of scheme (may be null) and hierarchical part of a filename (e.g. file:///tmp -> [file, tmp]).
722
-     */
723
-    private function getSchemeAndHierarchy(string $filename): array
724
-    {
725
-        $components = explode('://', $filename, 2);
726
-
727
-        return 2 === \count($components) ? [$components[0], $components[1]] : [null, $components[0]];
728
-    }
729
-
730
-    /**
731
-     * @param mixed ...$args
732
-     *
733
-     * @return mixed
734
-     */
735
-    private static function box(callable $func, ...$args)
736
-    {
737
-        self::$lastError = null;
738
-        set_error_handler(__CLASS__.'::handleError');
739
-        try {
740
-            $result = $func(...$args);
741
-            restore_error_handler();
742
-
743
-            return $result;
744
-        } catch (\Throwable $e) {
745
-        }
746
-        restore_error_handler();
747
-
748
-        throw $e;
749
-    }
750
-
751
-    /**
752
-     * @internal
753
-     */
754
-    public static function handleError(int $type, string $msg)
755
-    {
756
-        self::$lastError = $msg;
757
-    }
25
+	private static $lastError;
26
+
27
+	/**
28
+	 * Copies a file.
29
+	 *
30
+	 * If the target file is older than the origin file, it's always overwritten.
31
+	 * If the target file is newer, it is overwritten only when the
32
+	 * $overwriteNewerFiles option is set to true.
33
+	 *
34
+	 * @throws FileNotFoundException When originFile doesn't exist
35
+	 * @throws IOException           When copy fails
36
+	 */
37
+	public function copy(string $originFile, string $targetFile, bool $overwriteNewerFiles = false)
38
+	{
39
+		$originIsLocal = stream_is_local($originFile) || 0 === stripos($originFile, 'file://');
40
+		if ($originIsLocal && !is_file($originFile)) {
41
+			throw new FileNotFoundException(sprintf('Failed to copy "%s" because file does not exist.', $originFile), 0, null, $originFile);
42
+		}
43
+
44
+		$this->mkdir(\dirname($targetFile));
45
+
46
+		$doCopy = true;
47
+		if (!$overwriteNewerFiles && null === parse_url($originFile, \PHP_URL_HOST) && is_file($targetFile)) {
48
+			$doCopy = filemtime($originFile) > filemtime($targetFile);
49
+		}
50
+
51
+		if ($doCopy) {
52
+			// https://bugs.php.net/64634
53
+			if (!$source = self::box('fopen', $originFile, 'r')) {
54
+				throw new IOException(sprintf('Failed to copy "%s" to "%s" because source file could not be opened for reading: ', $originFile, $targetFile).self::$lastError, 0, null, $originFile);
55
+			}
56
+
57
+			// Stream context created to allow files overwrite when using FTP stream wrapper - disabled by default
58
+			if (!$target = self::box('fopen', $targetFile, 'w', false, stream_context_create(['ftp' => ['overwrite' => true]]))) {
59
+				throw new IOException(sprintf('Failed to copy "%s" to "%s" because target file could not be opened for writing: ', $originFile, $targetFile).self::$lastError, 0, null, $originFile);
60
+			}
61
+
62
+			$bytesCopied = stream_copy_to_stream($source, $target);
63
+			fclose($source);
64
+			fclose($target);
65
+			unset($source, $target);
66
+
67
+			if (!is_file($targetFile)) {
68
+				throw new IOException(sprintf('Failed to copy "%s" to "%s".', $originFile, $targetFile), 0, null, $originFile);
69
+			}
70
+
71
+			if ($originIsLocal) {
72
+				// Like `cp`, preserve executable permission bits
73
+				self::box('chmod', $targetFile, fileperms($targetFile) | (fileperms($originFile) & 0111));
74
+
75
+				if ($bytesCopied !== $bytesOrigin = filesize($originFile)) {
76
+					throw new IOException(sprintf('Failed to copy the whole content of "%s" to "%s" (%g of %g bytes copied).', $originFile, $targetFile, $bytesCopied, $bytesOrigin), 0, null, $originFile);
77
+				}
78
+			}
79
+		}
80
+	}
81
+
82
+	/**
83
+	 * Creates a directory recursively.
84
+	 *
85
+	 * @param string|iterable $dirs The directory path
86
+	 *
87
+	 * @throws IOException On any directory creation failure
88
+	 */
89
+	public function mkdir($dirs, int $mode = 0777)
90
+	{
91
+		foreach ($this->toIterable($dirs) as $dir) {
92
+			if (is_dir($dir)) {
93
+				continue;
94
+			}
95
+
96
+			if (!self::box('mkdir', $dir, $mode, true) && !is_dir($dir)) {
97
+				throw new IOException(sprintf('Failed to create "%s": ', $dir).self::$lastError, 0, null, $dir);
98
+			}
99
+		}
100
+	}
101
+
102
+	/**
103
+	 * Checks the existence of files or directories.
104
+	 *
105
+	 * @param string|iterable $files A filename, an array of files, or a \Traversable instance to check
106
+	 *
107
+	 * @return bool true if the file exists, false otherwise
108
+	 */
109
+	public function exists($files)
110
+	{
111
+		$maxPathLength = \PHP_MAXPATHLEN - 2;
112
+
113
+		foreach ($this->toIterable($files) as $file) {
114
+			if (\strlen($file) > $maxPathLength) {
115
+				throw new IOException(sprintf('Could not check if file exist because path length exceeds %d characters.', $maxPathLength), 0, null, $file);
116
+			}
117
+
118
+			if (!file_exists($file)) {
119
+				return false;
120
+			}
121
+		}
122
+
123
+		return true;
124
+	}
125
+
126
+	/**
127
+	 * Sets access and modification time of file.
128
+	 *
129
+	 * @param string|iterable $files A filename, an array of files, or a \Traversable instance to create
130
+	 * @param int|null        $time  The touch time as a Unix timestamp, if not supplied the current system time is used
131
+	 * @param int|null        $atime The access time as a Unix timestamp, if not supplied the current system time is used
132
+	 *
133
+	 * @throws IOException When touch fails
134
+	 */
135
+	public function touch($files, int $time = null, int $atime = null)
136
+	{
137
+		foreach ($this->toIterable($files) as $file) {
138
+			if (!($time ? self::box('touch', $file, $time, $atime) : self::box('touch', $file))) {
139
+				throw new IOException(sprintf('Failed to touch "%s": ', $file).self::$lastError, 0, null, $file);
140
+			}
141
+		}
142
+	}
143
+
144
+	/**
145
+	 * Removes files or directories.
146
+	 *
147
+	 * @param string|iterable $files A filename, an array of files, or a \Traversable instance to remove
148
+	 *
149
+	 * @throws IOException When removal fails
150
+	 */
151
+	public function remove($files)
152
+	{
153
+		if ($files instanceof \Traversable) {
154
+			$files = iterator_to_array($files, false);
155
+		} elseif (!\is_array($files)) {
156
+			$files = [$files];
157
+		}
158
+
159
+		self::doRemove($files, false);
160
+	}
161
+
162
+	private static function doRemove(array $files, bool $isRecursive): void
163
+	{
164
+		$files = array_reverse($files);
165
+		foreach ($files as $file) {
166
+			if (is_link($file)) {
167
+				// See https://bugs.php.net/52176
168
+				if (!(self::box('unlink', $file) || '\\' !== \DIRECTORY_SEPARATOR || self::box('rmdir', $file)) && file_exists($file)) {
169
+					throw new IOException(sprintf('Failed to remove symlink "%s": ', $file).self::$lastError);
170
+				}
171
+			} elseif (is_dir($file)) {
172
+				if (!$isRecursive) {
173
+					$tmpName = \dirname(realpath($file)).'/.'.strrev(strtr(base64_encode(random_bytes(2)), '/=', '-.'));
174
+
175
+					if (file_exists($tmpName)) {
176
+						try {
177
+							self::doRemove([$tmpName], true);
178
+						} catch (IOException $e) {
179
+						}
180
+					}
181
+
182
+					if (!file_exists($tmpName) && self::box('rename', $file, $tmpName)) {
183
+						$origFile = $file;
184
+						$file = $tmpName;
185
+					} else {
186
+						$origFile = null;
187
+					}
188
+				}
189
+
190
+				$files = new \FilesystemIterator($file, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS);
191
+				self::doRemove(iterator_to_array($files, true), true);
192
+
193
+				if (!self::box('rmdir', $file) && file_exists($file) && !$isRecursive) {
194
+					$lastError = self::$lastError;
195
+
196
+					if (null !== $origFile && self::box('rename', $file, $origFile)) {
197
+						$file = $origFile;
198
+					}
199
+
200
+					throw new IOException(sprintf('Failed to remove directory "%s": ', $file).$lastError);
201
+				}
202
+			} elseif (!self::box('unlink', $file) && (str_contains(self::$lastError, 'Permission denied') || file_exists($file))) {
203
+				throw new IOException(sprintf('Failed to remove file "%s": ', $file).self::$lastError);
204
+			}
205
+		}
206
+	}
207
+
208
+	/**
209
+	 * Change mode for an array of files or directories.
210
+	 *
211
+	 * @param string|iterable $files     A filename, an array of files, or a \Traversable instance to change mode
212
+	 * @param int             $mode      The new mode (octal)
213
+	 * @param int             $umask     The mode mask (octal)
214
+	 * @param bool            $recursive Whether change the mod recursively or not
215
+	 *
216
+	 * @throws IOException When the change fails
217
+	 */
218
+	public function chmod($files, int $mode, int $umask = 0000, bool $recursive = false)
219
+	{
220
+		foreach ($this->toIterable($files) as $file) {
221
+			if ((\PHP_VERSION_ID < 80000 || \is_int($mode)) && !self::box('chmod', $file, $mode & ~$umask)) {
222
+				throw new IOException(sprintf('Failed to chmod file "%s": ', $file).self::$lastError, 0, null, $file);
223
+			}
224
+			if ($recursive && is_dir($file) && !is_link($file)) {
225
+				$this->chmod(new \FilesystemIterator($file), $mode, $umask, true);
226
+			}
227
+		}
228
+	}
229
+
230
+	/**
231
+	 * Change the owner of an array of files or directories.
232
+	 *
233
+	 * @param string|iterable $files     A filename, an array of files, or a \Traversable instance to change owner
234
+	 * @param string|int      $user      A user name or number
235
+	 * @param bool            $recursive Whether change the owner recursively or not
236
+	 *
237
+	 * @throws IOException When the change fails
238
+	 */
239
+	public function chown($files, $user, bool $recursive = false)
240
+	{
241
+		foreach ($this->toIterable($files) as $file) {
242
+			if ($recursive && is_dir($file) && !is_link($file)) {
243
+				$this->chown(new \FilesystemIterator($file), $user, true);
244
+			}
245
+			if (is_link($file) && \function_exists('lchown')) {
246
+				if (!self::box('lchown', $file, $user)) {
247
+					throw new IOException(sprintf('Failed to chown file "%s": ', $file).self::$lastError, 0, null, $file);
248
+				}
249
+			} else {
250
+				if (!self::box('chown', $file, $user)) {
251
+					throw new IOException(sprintf('Failed to chown file "%s": ', $file).self::$lastError, 0, null, $file);
252
+				}
253
+			}
254
+		}
255
+	}
256
+
257
+	/**
258
+	 * Change the group of an array of files or directories.
259
+	 *
260
+	 * @param string|iterable $files     A filename, an array of files, or a \Traversable instance to change group
261
+	 * @param string|int      $group     A group name or number
262
+	 * @param bool            $recursive Whether change the group recursively or not
263
+	 *
264
+	 * @throws IOException When the change fails
265
+	 */
266
+	public function chgrp($files, $group, bool $recursive = false)
267
+	{
268
+		foreach ($this->toIterable($files) as $file) {
269
+			if ($recursive && is_dir($file) && !is_link($file)) {
270
+				$this->chgrp(new \FilesystemIterator($file), $group, true);
271
+			}
272
+			if (is_link($file) && \function_exists('lchgrp')) {
273
+				if (!self::box('lchgrp', $file, $group)) {
274
+					throw new IOException(sprintf('Failed to chgrp file "%s": ', $file).self::$lastError, 0, null, $file);
275
+				}
276
+			} else {
277
+				if (!self::box('chgrp', $file, $group)) {
278
+					throw new IOException(sprintf('Failed to chgrp file "%s": ', $file).self::$lastError, 0, null, $file);
279
+				}
280
+			}
281
+		}
282
+	}
283
+
284
+	/**
285
+	 * Renames a file or a directory.
286
+	 *
287
+	 * @throws IOException When target file or directory already exists
288
+	 * @throws IOException When origin cannot be renamed
289
+	 */
290
+	public function rename(string $origin, string $target, bool $overwrite = false)
291
+	{
292
+		// we check that target does not exist
293
+		if (!$overwrite && $this->isReadable($target)) {
294
+			throw new IOException(sprintf('Cannot rename because the target "%s" already exists.', $target), 0, null, $target);
295
+		}
296
+
297
+		if (!self::box('rename', $origin, $target)) {
298
+			if (is_dir($origin)) {
299
+				// See https://bugs.php.net/54097 & https://php.net/rename#113943
300
+				$this->mirror($origin, $target, null, ['override' => $overwrite, 'delete' => $overwrite]);
301
+				$this->remove($origin);
302
+
303
+				return;
304
+			}
305
+			throw new IOException(sprintf('Cannot rename "%s" to "%s": ', $origin, $target).self::$lastError, 0, null, $target);
306
+		}
307
+	}
308
+
309
+	/**
310
+	 * Tells whether a file exists and is readable.
311
+	 *
312
+	 * @throws IOException When windows path is longer than 258 characters
313
+	 */
314
+	private function isReadable(string $filename): bool
315
+	{
316
+		$maxPathLength = \PHP_MAXPATHLEN - 2;
317
+
318
+		if (\strlen($filename) > $maxPathLength) {
319
+			throw new IOException(sprintf('Could not check if file is readable because path length exceeds %d characters.', $maxPathLength), 0, null, $filename);
320
+		}
321
+
322
+		return is_readable($filename);
323
+	}
324
+
325
+	/**
326
+	 * Creates a symbolic link or copy a directory.
327
+	 *
328
+	 * @throws IOException When symlink fails
329
+	 */
330
+	public function symlink(string $originDir, string $targetDir, bool $copyOnWindows = false)
331
+	{
332
+		if ('\\' === \DIRECTORY_SEPARATOR) {
333
+			$originDir = strtr($originDir, '/', '\\');
334
+			$targetDir = strtr($targetDir, '/', '\\');
335
+
336
+			if ($copyOnWindows) {
337
+				$this->mirror($originDir, $targetDir);
338
+
339
+				return;
340
+			}
341
+		}
342
+
343
+		$this->mkdir(\dirname($targetDir));
344
+
345
+		if (is_link($targetDir)) {
346
+			if (readlink($targetDir) === $originDir) {
347
+				return;
348
+			}
349
+			$this->remove($targetDir);
350
+		}
351
+
352
+		if (!self::box('symlink', $originDir, $targetDir)) {
353
+			$this->linkException($originDir, $targetDir, 'symbolic');
354
+		}
355
+	}
356
+
357
+	/**
358
+	 * Creates a hard link, or several hard links to a file.
359
+	 *
360
+	 * @param string|string[] $targetFiles The target file(s)
361
+	 *
362
+	 * @throws FileNotFoundException When original file is missing or not a file
363
+	 * @throws IOException           When link fails, including if link already exists
364
+	 */
365
+	public function hardlink(string $originFile, $targetFiles)
366
+	{
367
+		if (!$this->exists($originFile)) {
368
+			throw new FileNotFoundException(null, 0, null, $originFile);
369
+		}
370
+
371
+		if (!is_file($originFile)) {
372
+			throw new FileNotFoundException(sprintf('Origin file "%s" is not a file.', $originFile));
373
+		}
374
+
375
+		foreach ($this->toIterable($targetFiles) as $targetFile) {
376
+			if (is_file($targetFile)) {
377
+				if (fileinode($originFile) === fileinode($targetFile)) {
378
+					continue;
379
+				}
380
+				$this->remove($targetFile);
381
+			}
382
+
383
+			if (!self::box('link', $originFile, $targetFile)) {
384
+				$this->linkException($originFile, $targetFile, 'hard');
385
+			}
386
+		}
387
+	}
388
+
389
+	/**
390
+	 * @param string $linkType Name of the link type, typically 'symbolic' or 'hard'
391
+	 */
392
+	private function linkException(string $origin, string $target, string $linkType)
393
+	{
394
+		if (self::$lastError) {
395
+			if ('\\' === \DIRECTORY_SEPARATOR && str_contains(self::$lastError, 'error code(1314)')) {
396
+				throw new IOException(sprintf('Unable to create "%s" link due to error code 1314: \'A required privilege is not held by the client\'. Do you have the required Administrator-rights?', $linkType), 0, null, $target);
397
+			}
398
+		}
399
+		throw new IOException(sprintf('Failed to create "%s" link from "%s" to "%s": ', $linkType, $origin, $target).self::$lastError, 0, null, $target);
400
+	}
401
+
402
+	/**
403
+	 * Resolves links in paths.
404
+	 *
405
+	 * With $canonicalize = false (default)
406
+	 *      - if $path does not exist or is not a link, returns null
407
+	 *      - if $path is a link, returns the next direct target of the link without considering the existence of the target
408
+	 *
409
+	 * With $canonicalize = true
410
+	 *      - if $path does not exist, returns null
411
+	 *      - if $path exists, returns its absolute fully resolved final version
412
+	 *
413
+	 * @return string|null
414
+	 */
415
+	public function readlink(string $path, bool $canonicalize = false)
416
+	{
417
+		if (!$canonicalize && !is_link($path)) {
418
+			return null;
419
+		}
420
+
421
+		if ($canonicalize) {
422
+			if (!$this->exists($path)) {
423
+				return null;
424
+			}
425
+
426
+			if ('\\' === \DIRECTORY_SEPARATOR && \PHP_VERSION_ID < 70410) {
427
+				$path = readlink($path);
428
+			}
429
+
430
+			return realpath($path);
431
+		}
432
+
433
+		if ('\\' === \DIRECTORY_SEPARATOR && \PHP_VERSION_ID < 70400) {
434
+			return realpath($path);
435
+		}
436
+
437
+		return readlink($path);
438
+	}
439
+
440
+	/**
441
+	 * Given an existing path, convert it to a path relative to a given starting path.
442
+	 *
443
+	 * @return string Path of target relative to starting path
444
+	 */
445
+	public function makePathRelative(string $endPath, string $startPath)
446
+	{
447
+		if (!$this->isAbsolutePath($startPath)) {
448
+			throw new InvalidArgumentException(sprintf('The start path "%s" is not absolute.', $startPath));
449
+		}
450
+
451
+		if (!$this->isAbsolutePath($endPath)) {
452
+			throw new InvalidArgumentException(sprintf('The end path "%s" is not absolute.', $endPath));
453
+		}
454
+
455
+		// Normalize separators on Windows
456
+		if ('\\' === \DIRECTORY_SEPARATOR) {
457
+			$endPath = str_replace('\\', '/', $endPath);
458
+			$startPath = str_replace('\\', '/', $startPath);
459
+		}
460
+
461
+		$splitDriveLetter = function ($path) {
462
+			return (\strlen($path) > 2 && ':' === $path[1] && '/' === $path[2] && ctype_alpha($path[0]))
463
+				? [substr($path, 2), strtoupper($path[0])]
464
+				: [$path, null];
465
+		};
466
+
467
+		$splitPath = function ($path) {
468
+			$result = [];
469
+
470
+			foreach (explode('/', trim($path, '/')) as $segment) {
471
+				if ('..' === $segment) {
472
+					array_pop($result);
473
+				} elseif ('.' !== $segment && '' !== $segment) {
474
+					$result[] = $segment;
475
+				}
476
+			}
477
+
478
+			return $result;
479
+		};
480
+
481
+		[$endPath, $endDriveLetter] = $splitDriveLetter($endPath);
482
+		[$startPath, $startDriveLetter] = $splitDriveLetter($startPath);
483
+
484
+		$startPathArr = $splitPath($startPath);
485
+		$endPathArr = $splitPath($endPath);
486
+
487
+		if ($endDriveLetter && $startDriveLetter && $endDriveLetter != $startDriveLetter) {
488
+			// End path is on another drive, so no relative path exists
489
+			return $endDriveLetter.':/'.($endPathArr ? implode('/', $endPathArr).'/' : '');
490
+		}
491
+
492
+		// Find for which directory the common path stops
493
+		$index = 0;
494
+		while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) {
495
+			++$index;
496
+		}
497
+
498
+		// Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels)
499
+		if (1 === \count($startPathArr) && '' === $startPathArr[0]) {
500
+			$depth = 0;
501
+		} else {
502
+			$depth = \count($startPathArr) - $index;
503
+		}
504
+
505
+		// Repeated "../" for each level need to reach the common path
506
+		$traverser = str_repeat('../', $depth);
507
+
508
+		$endPathRemainder = implode('/', \array_slice($endPathArr, $index));
509
+
510
+		// Construct $endPath from traversing to the common path, then to the remaining $endPath
511
+		$relativePath = $traverser.('' !== $endPathRemainder ? $endPathRemainder.'/' : '');
512
+
513
+		return '' === $relativePath ? './' : $relativePath;
514
+	}
515
+
516
+	/**
517
+	 * Mirrors a directory to another.
518
+	 *
519
+	 * Copies files and directories from the origin directory into the target directory. By default:
520
+	 *
521
+	 *  - existing files in the target directory will be overwritten, except if they are newer (see the `override` option)
522
+	 *  - files in the target directory that do not exist in the source directory will not be deleted (see the `delete` option)
523
+	 *
524
+	 * @param \Traversable|null $iterator Iterator that filters which files and directories to copy, if null a recursive iterator is created
525
+	 * @param array             $options  An array of boolean options
526
+	 *                                    Valid options are:
527
+	 *                                    - $options['override'] If true, target files newer than origin files are overwritten (see copy(), defaults to false)
528
+	 *                                    - $options['copy_on_windows'] Whether to copy files instead of links on Windows (see symlink(), defaults to false)
529
+	 *                                    - $options['delete'] Whether to delete files that are not in the source directory (defaults to false)
530
+	 *
531
+	 * @throws IOException When file type is unknown
532
+	 */
533
+	public function mirror(string $originDir, string $targetDir, \Traversable $iterator = null, array $options = [])
534
+	{
535
+		$targetDir = rtrim($targetDir, '/\\');
536
+		$originDir = rtrim($originDir, '/\\');
537
+		$originDirLen = \strlen($originDir);
538
+
539
+		if (!$this->exists($originDir)) {
540
+			throw new IOException(sprintf('The origin directory specified "%s" was not found.', $originDir), 0, null, $originDir);
541
+		}
542
+
543
+		// Iterate in destination folder to remove obsolete entries
544
+		if ($this->exists($targetDir) && isset($options['delete']) && $options['delete']) {
545
+			$deleteIterator = $iterator;
546
+			if (null === $deleteIterator) {
547
+				$flags = \FilesystemIterator::SKIP_DOTS;
548
+				$deleteIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($targetDir, $flags), \RecursiveIteratorIterator::CHILD_FIRST);
549
+			}
550
+			$targetDirLen = \strlen($targetDir);
551
+			foreach ($deleteIterator as $file) {
552
+				$origin = $originDir.substr($file->getPathname(), $targetDirLen);
553
+				if (!$this->exists($origin)) {
554
+					$this->remove($file);
555
+				}
556
+			}
557
+		}
558
+
559
+		$copyOnWindows = $options['copy_on_windows'] ?? false;
560
+
561
+		if (null === $iterator) {
562
+			$flags = $copyOnWindows ? \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS : \FilesystemIterator::SKIP_DOTS;
563
+			$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($originDir, $flags), \RecursiveIteratorIterator::SELF_FIRST);
564
+		}
565
+
566
+		$this->mkdir($targetDir);
567
+		$filesCreatedWhileMirroring = [];
568
+
569
+		foreach ($iterator as $file) {
570
+			if ($file->getPathname() === $targetDir || $file->getRealPath() === $targetDir || isset($filesCreatedWhileMirroring[$file->getRealPath()])) {
571
+				continue;
572
+			}
573
+
574
+			$target = $targetDir.substr($file->getPathname(), $originDirLen);
575
+			$filesCreatedWhileMirroring[$target] = true;
576
+
577
+			if (!$copyOnWindows && is_link($file)) {
578
+				$this->symlink($file->getLinkTarget(), $target);
579
+			} elseif (is_dir($file)) {
580
+				$this->mkdir($target);
581
+			} elseif (is_file($file)) {
582
+				$this->copy($file, $target, $options['override'] ?? false);
583
+			} else {
584
+				throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file);
585
+			}
586
+		}
587
+	}
588
+
589
+	/**
590
+	 * Returns whether the file path is an absolute path.
591
+	 *
592
+	 * @return bool
593
+	 */
594
+	public function isAbsolutePath(string $file)
595
+	{
596
+		return '' !== $file && (strspn($file, '/\\', 0, 1)
597
+			|| (\strlen($file) > 3 && ctype_alpha($file[0])
598
+				&& ':' === $file[1]
599
+				&& strspn($file, '/\\', 2, 1)
600
+			)
601
+			|| null !== parse_url($file, \PHP_URL_SCHEME)
602
+		);
603
+	}
604
+
605
+	/**
606
+	 * Creates a temporary file with support for custom stream wrappers.
607
+	 *
608
+	 * @param string $prefix The prefix of the generated temporary filename
609
+	 *                       Note: Windows uses only the first three characters of prefix
610
+	 * @param string $suffix The suffix of the generated temporary filename
611
+	 *
612
+	 * @return string The new temporary filename (with path), or throw an exception on failure
613
+	 */
614
+	public function tempnam(string $dir, string $prefix/*, string $suffix = ''*/)
615
+	{
616
+		$suffix = \func_num_args() > 2 ? func_get_arg(2) : '';
617
+		[$scheme, $hierarchy] = $this->getSchemeAndHierarchy($dir);
618
+
619
+		// If no scheme or scheme is "file" or "gs" (Google Cloud) create temp file in local filesystem
620
+		if ((null === $scheme || 'file' === $scheme || 'gs' === $scheme) && '' === $suffix) {
621
+			// If tempnam failed or no scheme return the filename otherwise prepend the scheme
622
+			if ($tmpFile = self::box('tempnam', $hierarchy, $prefix)) {
623
+				if (null !== $scheme && 'gs' !== $scheme) {
624
+					return $scheme.'://'.$tmpFile;
625
+				}
626
+
627
+				return $tmpFile;
628
+			}
629
+
630
+			throw new IOException('A temporary file could not be created: '.self::$lastError);
631
+		}
632
+
633
+		// Loop until we create a valid temp file or have reached 10 attempts
634
+		for ($i = 0; $i < 10; ++$i) {
635
+			// Create a unique filename
636
+			$tmpFile = $dir.'/'.$prefix.uniqid(mt_rand(), true).$suffix;
637
+
638
+			// Use fopen instead of file_exists as some streams do not support stat
639
+			// Use mode 'x+' to atomically check existence and create to avoid a TOCTOU vulnerability
640
+			if (!$handle = self::box('fopen', $tmpFile, 'x+')) {
641
+				continue;
642
+			}
643
+
644
+			// Close the file if it was successfully opened
645
+			self::box('fclose', $handle);
646
+
647
+			return $tmpFile;
648
+		}
649
+
650
+		throw new IOException('A temporary file could not be created: '.self::$lastError);
651
+	}
652
+
653
+	/**
654
+	 * Atomically dumps content into a file.
655
+	 *
656
+	 * @param string|resource $content The data to write into the file
657
+	 *
658
+	 * @throws IOException if the file cannot be written to
659
+	 */
660
+	public function dumpFile(string $filename, $content)
661
+	{
662
+		if (\is_array($content)) {
663
+			throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be string or resource, array given.', __METHOD__));
664
+		}
665
+
666
+		$dir = \dirname($filename);
667
+
668
+		if (!is_dir($dir)) {
669
+			$this->mkdir($dir);
670
+		}
671
+
672
+		// Will create a temp file with 0600 access rights
673
+		// when the filesystem supports chmod.
674
+		$tmpFile = $this->tempnam($dir, basename($filename));
675
+
676
+		try {
677
+			if (false === self::box('file_put_contents', $tmpFile, $content)) {
678
+				throw new IOException(sprintf('Failed to write file "%s": ', $filename).self::$lastError, 0, null, $filename);
679
+			}
680
+
681
+			self::box('chmod', $tmpFile, file_exists($filename) ? fileperms($filename) : 0666 & ~umask());
682
+
683
+			$this->rename($tmpFile, $filename, true);
684
+		} finally {
685
+			if (file_exists($tmpFile)) {
686
+				self::box('unlink', $tmpFile);
687
+			}
688
+		}
689
+	}
690
+
691
+	/**
692
+	 * Appends content to an existing file.
693
+	 *
694
+	 * @param string|resource $content The content to append
695
+	 *
696
+	 * @throws IOException If the file is not writable
697
+	 */
698
+	public function appendToFile(string $filename, $content)
699
+	{
700
+		if (\is_array($content)) {
701
+			throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be string or resource, array given.', __METHOD__));
702
+		}
703
+
704
+		$dir = \dirname($filename);
705
+
706
+		if (!is_dir($dir)) {
707
+			$this->mkdir($dir);
708
+		}
709
+
710
+		if (false === self::box('file_put_contents', $filename, $content, \FILE_APPEND)) {
711
+			throw new IOException(sprintf('Failed to write file "%s": ', $filename).self::$lastError, 0, null, $filename);
712
+		}
713
+	}
714
+
715
+	private function toIterable($files): iterable
716
+	{
717
+		return is_iterable($files) ? $files : [$files];
718
+	}
719
+
720
+	/**
721
+	 * Gets a 2-tuple of scheme (may be null) and hierarchical part of a filename (e.g. file:///tmp -> [file, tmp]).
722
+	 */
723
+	private function getSchemeAndHierarchy(string $filename): array
724
+	{
725
+		$components = explode('://', $filename, 2);
726
+
727
+		return 2 === \count($components) ? [$components[0], $components[1]] : [null, $components[0]];
728
+	}
729
+
730
+	/**
731
+	 * @param mixed ...$args
732
+	 *
733
+	 * @return mixed
734
+	 */
735
+	private static function box(callable $func, ...$args)
736
+	{
737
+		self::$lastError = null;
738
+		set_error_handler(__CLASS__.'::handleError');
739
+		try {
740
+			$result = $func(...$args);
741
+			restore_error_handler();
742
+
743
+			return $result;
744
+		} catch (\Throwable $e) {
745
+		}
746
+		restore_error_handler();
747
+
748
+		throw $e;
749
+	}
750
+
751
+	/**
752
+	 * @internal
753
+	 */
754
+	public static function handleError(int $type, string $msg)
755
+	{
756
+		self::$lastError = $msg;
757
+	}
758 758
 }
Please login to merge, or discard this patch.