8000 Merge branch '4.4' into 5.0 · symfony/symfony@efb5c49 · GitHub
[go: up one dir, main page]

Skip to content

Commit efb5c49

Browse files
Merge branch '4.4' into 5.0
* 4.4: [Http Foundation] Fix clear cookie samesite [Security] Check if firewall is stateless before checking for session/previous session [Form] Support customized intl php.ini settings [Security] Remember me: allow to set the samesite cookie flag [Debug] fix for PHP 7.3.16+/7.4.4+ [Validator] Backport translations [Mailer] Use %d instead of %s for error code in error messages [HttpKernel] fix locking for PHP 7.4+ [Security] Fixed hardcoded value of SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE Prevent warning in proc_open() [FrameworkBundle] Fix Router Cache Fix deprecation messages
2 parents ea0eb11 + 7f5d017 commit efb5c49

26 files changed

+155
-69
lines changed

src/Symfony/Bundle/FrameworkBundle/Routing/Router.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
use Psr\Container\ContainerInterface;
1515
use Psr\Log\LoggerInterface;
1616
use Symfony\Component\Config\Loader\LoaderInterface;
17+
use Symfony\Component\Config\Resource\FileExistenceResource;
18+
use Symfony\Component\Config\Resource\FileResource;
1719
use Symfony\Component\DependencyInjection\Config\ContainerParametersResource;
1820
use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface;
1921
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
@@ -71,6 +73,16 @@ public function getRouteCollection()
7173
$this->collection = $this->container->get('routing.loader')->load($this->resource, $this->options['resource_type']);
7274
$this->resolveParameters($this->collection);
7375
$this->collection->addResource(new ContainerParametersResource($this->collectedParameters));
76+
77+
try {
78+
$containerFile = ($this->paramFetcher)('kernel.cache_dir').'/'.($this->paramFetcher)('kernel.container_class').'.php';
79+
if (file_exists($containerFile)) {
80+
$this->collection->addResource(new FileResource($containerFile));
81+
} else {
82+
$this->collection->addResource(new FileExistenceResource($containerFile));
83+
}
84+
} catch (ParameterNotFoundException $exception) {
85+
}
7486
}
7587

7688
return $this->collection;

src/Symfony/Component/ErrorHandler/ErrorHandler.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,7 @@ public function handleError(int $type, string $message, string $file, int $line)
519519
if ($this->isRecursive) {
520520
$log = 0;
521521
} else {
522-
if (!\defined('HHVM_VERSION')) {
522+
if (\PHP_VERSION_ID < (\PHP_VERSION_ID < 70400 ? 70316 : 70404)) {
523523
$currentErrorHandler = set_error_handler('var_dump');
524524
restore_error_handler();
525525
}
@@ -531,7 +531,7 @@ public function handleError(int $type, string $message, string $file, int $line)
531531
} finally {
532532
$this->isRecursive = false;
533533

534-
if (!\defined('HHVM_VERSION')) {
534+
if (\PHP_VERSION_ID < (\PHP_VERSION_ID < 70400 ? 70316 : 70404)) {
535535
set_error_handler($currentErrorHandler);
536536
}
537537
}

src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,8 +363,6 @@ public function testHandleDeprecation()
363363
$handler = new ErrorHandler();
364364
$handler->setDefaultLogger($logger);
365365
@$handler->handleError(E_USER_DEPRECATED, 'Foo deprecation', __FILE__, __LINE__, []);
366-
367-
restore_error_handler();
368366
}
369367

370368
/**
@@ -618,6 +616,10 @@ public function errorHandlerWhenLoggingProvider(): iterable
618616

619617
public function testAssertQuietEval()
620618
{
619+
if ('-1' === ini_get('zend.assertions')) {
620+
$this->markTestSkipped('zend.assertions is forcibly disabled');
621+
}
622+
621623
$ini = [
622624
ini_set('zend.assertions', 1),
623625
ini_set('assert.active', 1),

src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformer.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,16 @@ public function reverseTransform($value)
117117
// date-only patterns require parsing to be done in UTC, as midnight might not exist in the local timezone due
118118
// to DST changes
119119
$dateOnly = $this->isPatternDateOnly();
120+
$dateFormatter = $this->getIntlDateFormatter($dateOnly);
120121

121-
$timestamp = $this->getIntlDateFormatter($dateOnly)->parse($value);
122+
try {
123+
$timestamp = @$dateFormatter->parse($value);
124+
} catch (\IntlException $e) {
125+
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
126+
}
122127

123128
if (0 != intl_get_error_code()) {
124-
throw new TransformationFailedException(intl_get_error_message());
129+
throw new TransformationFailedException(intl_get_error_message(), intl_get_error_code());
125130
} elseif ($timestamp > 253402214400) {
126131
// This timestamp represents UTC midnight of 9999-12-31 to prevent 5+ digit years
127132
throw new TransformationFailedException('Years beyond 9999 are not supported.');

src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformerTest.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ protected function setUp(): void
2727
{
2828
parent::setUp();
2929

30+
// Normalize intl. configuration settings.
31+
if (\extension_loaded('intl')) {
32+
$this->iniSet('intl.use_exceptions', 0);
33+
$this->iniSet('intl.error_level', 0);
34+
}
35+
3036
// Since we test against "de_AT", we need the full implementation
3137
IntlTestHelper::requireFullIntl($this, '57.1');
3238

@@ -322,4 +328,44 @@ public function testReverseTransformFiveDigitYearsWithTimestamp()
322328
$transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC', null, null, \IntlDateFormatter::GREGORIAN, 'yyyy-MM-dd HH:mm:ss');
323329
$transformer->reverseTransform('20107-03-21 12:34:56');
324330
}
331+
332+
public function testReverseTransformWrapsIntlErrorsWithErrorLevel()
333+
{
334+
if (!\extension_loaded('intl')) {
335+
$this->markTestSkipped('intl extension is not loaded');
336+
}
337+
338+
$this->iniSet('intl.error_level', E_WARNING);
339+
340+
$this->expectException('Symfony\Component\Form\Exception\TransformationFailedException');
341+
$transformer = new DateTimeToLocalizedStringTransformer();
342+
$transformer->reverseTransform('12345');
343+
}
344+
345+
public function testReverseTransformWrapsIntlErrorsWithExceptions()
346+
{
347+
if (!\extension_loaded('intl')) {
348+
$this->markTestSkipped('intl extension is not loaded');
349+
}
350+
351+
$this->iniSet('intl.use_exceptions', 1);
352+
353+
$this->expectException('Symfony\Component\Form\Exception\TransformationFailedException');
354+
$transformer = new DateTimeToLocalizedStringTransformer();
355+
$transformer->reverseTransform('12345');
356+
}
357+
358+
public function testReverseTransformWrapsIntlErrorsWithExceptionsAndErrorLevel()
359+
{
360+
if (!\extension_loaded('intl')) {
361+
$this->markTestSkipped('intl extension is not loaded');
362+
}
363+
364+
$this->iniSet('intl.use_exceptions', 1);
365+
$this->iniSet('intl.error_level', E_WARNING);
366+
367+
$this->expectException('Symfony\Component\Form\Exception\TransformationFailedException');
368+
$transformer = new DateTimeToLocalizedStringTransformer();
369+
$transformer->reverseTransform('12345');
370+
}
325371
}

src/Symfony/Component/HttpFoundation/ResponseHeaderBag.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,9 +239,9 @@ public function getCookies(string $format = self::COOKIES_FLAT)
239239
/**
240240
* Clears a cookie in the browser.
241241
*/
242-
public function clearCookie(string $name, ?string $path = '/', string $domain = null, bool $secure = false, bool $httpOnly = true)
242+
public function clearCookie(string $name, ?string $path = '/', string $domain = null, bool $secure = false, bool $httpOnly = true, string $sameSite = null)
243243
{
244-
$this->setCookie(new Cookie($name, null, 1, $path, $domain, $secure, $httpOnly, false, null));
244+
$this->setCookie(new Cookie($name, null, 1, $path, $domain, $secure, $httpOnly, false, $sameSite));
245245
}
246246

247247
/**

src/Symfony/Component/HttpFoundation/Tests/ResponseHeaderBagTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ public function testClearCookieSecureNotHttpOnly()
128128
$this->assertSetCookieHeader('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0; path=/; secure', $bag);
129129
}
130130

131+
public function testClearCookieSamesite()
132+
{
133+
$bag = new ResponseHeaderBag([]);
134+
135+
$bag->clearCookie('foo', '/', null, true, false, 'none');
136+
$this->assertSetCookieHeader('foo=deleted; expires='.gmdate('D, d-M-Y H:i:s T', time() - 31536001).'; Max-Age=0; path=/; secure; samesite=none', $bag);
137+
}
138+
131139
public function testReplace()
132140
{
133141
$bag = new ResponseHeaderBag([]);

src/Symfony/Component/HttpKernel/Kernel.php

Lines changed: 14 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,6 @@
3838
use Symfony\Component\HttpKernel\DependencyInjection\AddAnnotatedClassesToCachePass;
3939
use Symfony\Component\HttpKernel\DependencyInjection\MergeExtensionConfigurationPass;
4040

41-
// Help opcache.preload discover always-needed symbols
42-
class_exists(ConfigCache::class);
43-
4441
/**
4542
* The Kernel is the heart of the Symfony system.
4643
*
@@ -452,47 +449,20 @@ protected function initializeContainer()
452449
try {
453450
is_dir($cacheDir) ?: mkdir($cacheDir, 0777, true);
454451

455-
if ($lock = fopen($cachePath, 'w')) {
456-
chmod($cachePath, 0666 & ~umask());
452+
if ($lock = fopen($cachePath.'.lock', 'w')) {
457453
flock($lock, LOCK_EX | LOCK_NB, $wouldBlock);
458454

459455
if (!flock($lock, $wouldBlock ? LOCK_SH : LOCK_EX)) {
460456
fclose($lock);
461-
} else {
462-
$cache = new class($cachePath, $this->debug) extends ConfigCache {
463-
public $lock;
464-
465-
public function write(string $content, array $metadata = null)
466-
{
467-
rewind($this->lock);
468-
ftruncate($this->lock, 0);
469-
fwrite($this->lock, $content);
470-
471-
if (null !== $metadata) {
472-
file_put_contents($this->getPath().'.meta', serialize($metadata));
473-
@chmod($this->getPath().'.meta', 0666 & ~umask());
474-
}
475-
476-
if (\function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), FILTER_VALIDATE_BOOLEAN)) {
477-
@opcache_invalidate($this->getPath(), true);
478-
}
479-
}
480-
481-
public function release()
482-
{
483-
flock($this->lock, LOCK_UN);
484-
fclose($this->lock);
485-
}
486-
};
487-
$cache->lock = $lock;
488-
489-
if (!\is_object($this->container = include $cachePath)) {
490-
$this->container = null;
491-
} elseif (!$oldContainer || \get_class($this->container) !== $oldContainer->name) {
492-
$this->container->set('kernel', $this);
493-
494-
return;
495-
}
457+
$lock = null;
458+
} elseif (!\is_object($this->container = include $cachePath)) {
459+
$this->container = null;
460+
} elseif (!$oldContainer || \get_class($this->container) !== $oldContainer->name) {
461+
flock($lock, LOCK_UN);
462+
fclose($lock);
463+
$this->container->set('kernel', $this);
464+
465+
return;
496466
}
497467
}
498468
} catch (\Throwable $e) {
@@ -556,8 +526,10 @@ public function release()
556526
}
557527

558528
$this->dumpContainer($cache, $container, $class, $this->getContainerBaseClass());
559-
if (method_exists($cache, 'release')) {
560-
$cache->release();
529+
530+
if ($lock) {
531+
flock($lock, LOCK_UN);
532+
fclose($lock);
561533
}
562534

563535
$this->container = require $cachePath;

src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesApiTransport.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $e
6565

6666
$result = new \SimpleXMLElement($response->getContent(false));
6767
if (200 !== $response->getStatusCode()) {
68-
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $result->Error->Message, $result->Error->Code), $response);
68+
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %d).', $result->Error->Message, $result->Error->Code), $response);
6969
}
7070

7171
$property = $payload['Action'].'Result';

src/Symfony/Component/Mailer/Bridge/Amazon/Transport/SesHttpTransport.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ protected function doSendHttp(SentMessage $message): ResponseInterface
6565

6666
$result = new \SimpleXMLElement($response->getContent(false));
6767
if (200 !== $response->getStatusCode()) {
68-
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $result->Error->Message, $result->Error->Code), $response);
68+
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %d).', $result->Error->Message, $result->Error->Code), $response);
6969
}
7070

7171
$message->setMessageId($result->SendRawEmailResult->MessageId);

src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillApiTransport.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $e
5151
$result = $response->toArray(false);
5252
if (200 !== $response->getStatusCode()) {
5353
if ('error' === ($result['status'] ?? false)) {
54-
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $result['message'], $result['code']), $response);
54+
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %d).', $result['message'], $result['code']), $response);
5555
}
5656

57-
throw new HttpTransportException(sprintf('Unable to send an email (code %s).', $result['code']), $response);
57+
throw new HttpTransportException(sprintf('Unable to send an email (code %d).', $result['code']), $response);
5858
}
5959

6060
$firstRecipient = reset($result);

src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillHttpTransport.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,10 @@ protected function doSendHttp(SentMessage $message): ResponseInterface
5858
$result = $response->toArray(false);
5959
if (200 !== $response->getStatusCode()) {
6060
if ('error' === ($result['status'] ?? false)) {
61-
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $result['message'], $result['code']), $response);
61+
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %d).', $result['message'], $result['code']), $response);
6262
}
6363

64-
throw new HttpTransportException(sprintf('Unable to send an email (code %s).', $result['code']), $response);
64+
throw new HttpTransportException(sprintf('Unable to send an email (code %d).', $result['code']), $response);
6565
}
6666

6767
$message->setMessageId($result[0]['_id']);

src/Symfony/Component/Mailer/Bridge/Mailgun/Transport/MailgunApiTransport.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,10 @@ protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $e
6565
$result = $response->toArray(false);
6666
if (200 !== $response->getStatusCode()) {
6767
if ('application/json' === $response->getHeaders(false)['content-type'][0]) {
68-
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $result['message'], $response->getStatusCode()), $response);
68+
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %d).', $result['message'], $response->getStatusCode()), $response);
6969
}
7070

71-
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $response->getContent(false), $response->getStatus 10000 Code()), $response);
71+
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %d).', $response->getContent(false), $response->getStatusCode()), $response);
7272
}
7373

7474
$sentMessage->setMessageId($result['id']);

src/Symfony/Component/Mailer/Bridge/Mailgun/Transport/MailgunHttpTransport.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,10 @@ protected function doSendHttp(SentMessage $message): ResponseInterface
6767
$result = $response->toArray(false);
6868
if (200 !== $response->getStatusCode()) {
6969
if ('application/json' === $response->getHeaders(false)['content-type'][0]) {
70-
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $result['message'], $response->getStatusCode()), $response);
70+
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %d).', $result['message'], $response->getStatusCode()), $response);
7171
}
7272

73-
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $response->getContent(false), $response->getStatusCode()), $response);
73+
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %d).', $response->getContent(false), $response->getStatusCode()), $response);
7474
}
7575

7676
$message->setMessageId($result['id']);

src/Symfony/Component/Mailer/Bridge/Postmark/Transport/PostmarkApiTransport.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $e
5454

5555
$result = $response->toArray(false);
5656
if (200 !== $response->getStatusCode()) {
57-
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', $result['Message'], $result['ErrorCode']), $response);
57+
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %d).', $result['Message'], $result['ErrorCode']), $response);
5858
}
5959

6060
$sentMessage->setMessageId($result['MessageID']);

src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridApiTransport.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $e
5353
if (202 !== $response->getStatusCode()) {
5454
$errors = $response->toArray(false);
5555

56-
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %s).', implode('; ', array_column($errors['errors'], 'message')), $response->getStatusCode()), $response);
56+
throw new HttpTransportException(sprintf('Unable to send an email: %s (code %d).', implode('; ', array_column($errors['errors'], 'message')), $response->getStatusCode()), $response);
5757
}
5858

5959
$sentMessage->setMessageId($response->getHeaders(false)['x-message-id'][0]);

src/Symfony/Component/Process/Process.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ public function start(callable $callback = null, array $env = [])
332332
throw new RuntimeException(sprintf('The provided cwd "%s" does not exist.', $this->cwd));
333333
}
334334

335-
$this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $options);
335+
$this->process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $options);
336336

337337
if (!\is_resource($this->process)) {
338338
throw new RuntimeException('Unable to launch a new process.');

src/Symfony/Component/Security/Core/Encoder/SodiumPasswordEncoder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public function __construct(int $opsLimit = null, int $memLimit = null)
3535
}
3636

3737
$this->opsLimit = $opsLimit ?? max(4, \defined('SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE') ? SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE : 4);
38-
$this->memLimit = $memLimit ?? max(64 * 1024 * 1024, \defined('SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE') ? SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE : 64 * 1024 * 2014);
38+
$this->memLimit = $memLimit ?? max(64 * 1024 * 1024, \defined('SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE') ? SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE : 64 * 1024 * 1024);
3939

4040
if (3 > $this->opsLimit) {
4141
throw new \InvalidArgumentException('$opsLimit must be 3 or greater.');

src/Symfony/Component/Security/Guard/GuardAuthenticatorHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public function setSessionAuthenticationStrategy(SessionAuthenticationStrategyIn
120120

121121
private function migrateSession(Request $request, TokenInterface $token, ?string $providerKey)
122122
{
123-
if (!$this->sessionStrategy || !$request->hasSession() || !$request->hasPreviousSession() || \in_array($providerKey, $this->statelessProviderKeys, true)) {
123+
if (\in_array($providerKey, $this->statelessProviderKeys, true) || !$this->sessionStrategy || !$request->hasSession() || !$request->hasPreviousSession()) {
124124
return;
125125
}
126126

src/Symfony/Component/Security/Guard/Tests/GuardAuthenticatorHandlerTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,22 @@ public function testSessionStrategyIsNotCalledWhenStateless()
153153
$handler->authenticateWithToken($this->token, $this->request, 'some_provider_key');
154154
}
155155

156+
public function testSessionIsNotInstantiatedOnStatelessFirewall()
157+
{
158+
$sessionFactory = $this->getMockBuilder(\stdClass::class)
159+
->setMethods(['__invoke'])
160+
->getMock();
161+
162+
$sessionFactory->expects($this->never())
163+
->method('__invoke');
164+
165+
$this->request->setSessionFactory($sessionFactory);
166+
167+
$handler = new GuardAuthenticatorHandler($this->tokenStorage, $this->dispatcher, ['stateless_provider_key']);
168+
$handler->setSessionAuthenticationStrategy($this->sessionStrategy);
169+
$handler->authenticateWithToken($this->token, $this->request, 'stateless_provider_key');
170+
}
171+
156172
protected function setUp(): void
157173
{
158174
$this->tokenStorage = $this->getMockBuilder(TokenStorageInterface::class)->getMock();

0 commit comments

Comments
 (0)
0