Merge branch '5.2' into 5.x

* 5.2: (23 commits)
  [Console] Fix Windows code page support
  [SecurityBundle] Allow ips parameter in access_control accept comma-separated string
  [Form] Add TranslatableMessage support to choice_label option of ChoiceType
  Remove code that deals with legacy behavior of PHP_Incomplete_Class
  [Config][DependencyInjection] Uniformize trailing slash handling
  [PropertyInfo] Make ReflectionExtractor correctly extract nullability
  [PropertyInfo] fix attribute namespace with recursive traits
  [PhpUnitBridge] Fix tests with `@doesNotPerformAssertions` annotations
  Check redis extension version
  [Security] Update Russian translations
  [Notifier] Fix return SentMessage then Messenger not used
  [VarExporter] Add support of PHP enumerations
  [Security] Added missing Japanese translations
  [Security] Added missing Polish translations
  [Security] Add missing Italian translations #41051
  [Security] Missing translations pt_BR
  getProtocolVersion may return null
  Fix return type on isAllowedProperty method
  Make FailoverTransport always pick the first transport
  [TwigBridge] Fix HTML for translatable custom-file label in Bootstrap 4 theme
  ...
This commit is contained in:
Nicolas Grekas 2021-05-07 16:34:05 +02:00
commit 0c1261ebe1
52 changed files with 817 additions and 88 deletions

View File

@ -239,7 +239,7 @@ class SymfonyTestsListenerTrait
} }
if ($this->checkNumAssertions) { if ($this->checkNumAssertions) {
$this->checkNumAssertions = $test->getTestResultObject()->isStrictAboutTestsThatDoNotTestAnything() && !$test->doesNotPerformAssertions(); $this->checkNumAssertions = $test->getTestResultObject()->isStrictAboutTestsThatDoNotTestAnything();
} }
$test->getTestResultObject()->beStrictAboutTestsThatDoNotTestAnything(false); $test->getTestResultObject()->beStrictAboutTestsThatDoNotTestAnything(false);
@ -268,7 +268,10 @@ class SymfonyTestsListenerTrait
$groups = Test::getGroups($className, $test->getName(false)); $groups = Test::getGroups($className, $test->getName(false));
if ($this->checkNumAssertions) { if ($this->checkNumAssertions) {
if (!self::$expectedDeprecations && !$test->getNumAssertions() && $test->getTestResultObject()->noneSkipped()) { $assertions = \count(self::$expectedDeprecations) + $test->getNumAssertions();
if ($test->doesNotPerformAssertions() && $assertions > 0) {
$test->getTestResultObject()->addFailure($test, new RiskyTestError(sprintf('This test is annotated with "@doesNotPerformAssertions", but performed %s assertions', $assertions)), $time);
} elseif ($assertions === 0 && $test->getTestResultObject()->noneSkipped()) {
$test->getTestResultObject()->addFailure($test, new RiskyTestError('This test did not perform any assertions'), $time); $test->getTestResultObject()->addFailure($test, new RiskyTestError('This test did not perform any assertions'), $time);
} }

View File

@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\PhpUnit\Tests\FailTests;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
/**
* This class is deliberately suffixed with *TestRisky.php so that it is ignored
* by PHPUnit. This test is designed to fail. See ../expectrisky.phpt.
*/
final class NoAssertionsTestRisky extends TestCase
{
use ExpectDeprecationTrait;
/**
* Do not remove this test in the next major version.
*
* @group legacy
*/
public function testOne()
{
$this->expectNotToPerformAssertions();
$this->expectDeprecation('foo');
@trigger_error('foo', \E_USER_DEPRECATED);
}
/**
* Do not remove this test in the next major version.
*/
public function testTwo()
{
$this->expectNotToPerformAssertions();
}
}

View File

@ -0,0 +1,22 @@
--TEST--
Test NoAssertionsTestRisky risky test
--FILE--
<?php
$test = realpath(__DIR__.'/FailTests/NoAssertionsTestRisky.php');
passthru('php '.getenv('SYMFONY_SIMPLE_PHPUNIT_BIN_DIR').'/simple-phpunit.php --fail-on-risky --colors=never '.$test);
?>
--EXPECTF--
PHPUnit %s by Sebastian Bergmann and contributors.
%ATesting Symfony\Bridge\PhpUnit\Tests\FailTests\NoAssertionsTestRisky
R. 2 / 2 (100%)
Time: %s, Memory: %s
There was 1 risky test:
1) Symfony\Bridge\PhpUnit\Tests\FailTests\NoAssertionsTestRisky::testOne
This test is annotated with "@doesNotPerformAssertions", but performed 1 assertions
OK, but incomplete, skipped, or risky tests!
Tests: 2, Assertions: 1, Risky: 1.

View File

@ -121,11 +121,12 @@
{% block file_widget -%} {% block file_widget -%}
<{{ element|default('div') }} class="custom-file"> <{{ element|default('div') }} class="custom-file">
{%- set type = type|default('file') -%} {%- set type = type|default('file') -%}
{{- block('form_widget_simple') -}}
{%- set label_attr = label_attr|merge({ class: (label_attr.class|default('') ~ ' custom-file-label')|trim }) -%}
{%- set input_lang = 'en' -%} {%- set input_lang = 'en' -%}
{% if app is defined and app.request is defined %}{%- set input_lang = app.request.locale -%}{%- endif -%} {% if app is defined and app.request is defined %}{%- set input_lang = app.request.locale -%}{%- endif -%}
<label for="{{ form.vars.id }}" lang="{{ input_lang }}" {% with { attr: label_attr } %}{{ block('attributes') }}{% endwith %}> {%- set attr = {lang: input_lang} | merge(attr) -%}
{{- block('form_widget_simple') -}}
{%- set label_attr = label_attr|merge({ class: (label_attr.class|default('') ~ ' custom-file-label')|trim }) -%}
<label for="{{ form.vars.id }}" {% with { attr: label_attr } %}{{ block('attributes') }}{% endwith %}>
{%- if attr.placeholder is defined and attr.placeholder is not none -%} {%- if attr.placeholder is defined and attr.placeholder is not none -%}
{{- translation_domain is same as(false) ? attr.placeholder : attr.placeholder|trans({}, translation_domain) -}} {{- translation_domain is same as(false) ? attr.placeholder : attr.placeholder|trans({}, translation_domain) -}}
{%- endif -%} {%- endif -%}

View File

@ -1026,7 +1026,7 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
foreach ($ips as $ip) { foreach ($ips as $ip) {
$container->resolveEnvPlaceholders($ip, null, $usedEnvs); $container->resolveEnvPlaceholders($ip, null, $usedEnvs);
if (!$usedEnvs && !$this->isValidIp($ip)) { if (!$usedEnvs && !$this->isValidIps($ip)) {
throw new \LogicException(sprintf('The given value "%s" in the "security.access_control" config option is not a valid IP address.', $ip)); throw new \LogicException(sprintf('The given value "%s" in the "security.access_control" config option is not a valid IP address.', $ip));
} }
@ -1084,6 +1084,25 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
return new MainConfiguration($this->factories, $this->userProviderFactories); return new MainConfiguration($this->factories, $this->userProviderFactories);
} }
private function isValidIps($ips): bool
{
$ipsList = array_reduce((array) $ips, static function (array $ips, string $ip) {
return array_merge($ips, preg_split('/\s*,\s*/', $ip));
}, []);
if (!$ipsList) {
return false;
}
foreach ($ipsList as $cidr) {
if (!$this->isValidIp($cidr)) {
return false;
}
}
return true;
}
private function isValidIp(string $cidr): bool private function isValidIp(string $cidr): bool
{ {
$cidrParts = explode('/', $cidr); $cidrParts = explode('/', $cidr);

View File

@ -388,6 +388,33 @@ class SecurityExtensionTest extends TestCase
$this->assertEquals($secure, $definition->getArgument(3)['secure']); $this->assertEquals($secure, $definition->getArgument(3)['secure']);
} }
/**
* @dataProvider acceptableIpsProvider
*/
public function testAcceptableAccessControlIps($ips)
{
$container = $this->getRawContainer();
$container->loadFromExtension('security', [
'providers' => [
'default' => ['id' => 'foo'],
],
'firewalls' => [
'some_firewall' => [
'pattern' => '/.*',
'http_basic' => [],
],
],
'access_control' => [
['ips' => $ips, 'path' => '/somewhere', 'roles' => 'IS_AUTHENTICATED_FULLY'],
],
]);
$container->compile();
$this->assertTrue(true, 'Ip addresses is successfully consumed: '.(\is_string($ips) ? $ips : json_encode($ips)));
}
public function testCustomRememberMeHandler() public function testCustomRememberMeHandler()
{ {
$container = $this->getRawContainer(); $container = $this->getRawContainer();
@ -430,6 +457,16 @@ class SecurityExtensionTest extends TestCase
]; ];
} }
public function acceptableIpsProvider(): iterable
{
yield [['127.0.0.1']];
yield ['127.0.0.1'];
yield ['127.0.0.1, 127.0.0.2'];
yield ['127.0.0.1/8, 127.0.0.2/16'];
yield [['127.0.0.1/8, 127.0.0.2/16']];
yield [['127.0.0.1/8', '127.0.0.2/16']];
}
public function testSwitchUserWithSeveralDefinedProvidersButNoFirewallRootProviderConfigured() public function testSwitchUserWithSeveralDefinedProvidersButNoFirewallRootProviderConfigured()
{ {
$container = $this->getRawContainer(); $container = $this->getRawContainer();

View File

@ -203,7 +203,7 @@ trait RedisTrait
} }
try { try {
@$redis->{$connect}($host, $port, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval'], $params['read_timeout'], ['stream' => $params['ssl'] ?? null]); @$redis->{$connect}($host, $port, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval'], $params['read_timeout'], ...\defined('Redis::SCAN_PREFIX') ? [['stream' => $params['ssl'] ?? null]] : []);
set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
$isConnected = $redis->isConnected(); $isConnected = $redis->isConnected();
@ -266,7 +266,7 @@ trait RedisTrait
} }
try { try {
$redis = new $class(null, $hosts, $params['timeout'], $params['read_timeout'], (bool) $params['persistent'], $params['auth'] ?? '', $params['ssl'] ?? null); $redis = new $class(null, $hosts, $params['timeout'], $params['read_timeout'], (bool) $params['persistent'], $params['auth'] ?? '', ...\defined('Redis::SCAN_PREFIX') ? [$params['ssl'] ?? null] : []);
} catch (\RedisClusterException $e) { } catch (\RedisClusterException $e) {
throw new InvalidArgumentException(sprintf('Redis connection "%s" failed: ', $dsn).$e->getMessage()); throw new InvalidArgumentException(sprintf('Redis connection "%s" failed: ', $dsn).$e->getMessage());
} }

View File

@ -76,8 +76,8 @@ abstract class FileLoader extends Loader
$excluded = []; $excluded = [];
foreach ((array) $exclude as $pattern) { foreach ((array) $exclude as $pattern) {
foreach ($this->glob($pattern, true, $_, false, true) as $path => $info) { foreach ($this->glob($pattern, true, $_, false, true) as $path => $info) {
// normalize Windows slashes // normalize Windows slashes and remove trailing slashes
$excluded[str_replace('\\', '/', $path)] = true; $excluded[rtrim(str_replace('\\', '/', $path), '/')] = true;
} }
} }

View File

@ -127,6 +127,28 @@ class FileLoaderTest extends TestCase
$this->assertCount(2, $loadedFiles); $this->assertCount(2, $loadedFiles);
$this->assertNotContains('ExcludeFile.txt', $loadedFiles); $this->assertNotContains('ExcludeFile.txt', $loadedFiles);
} }
/**
* @dataProvider excludeTrailingSlashConsistencyProvider
*/
public function testExcludeTrailingSlashConsistency(string $exclude)
{
$loader = new TestFileLoader(new FileLocator(__DIR__.'/../Fixtures'));
$loadedFiles = $loader->import('ExcludeTrailingSlash/*', null, false, null, $exclude);
$this->assertCount(2, $loadedFiles);
$this->assertNotContains('baz.txt', $loadedFiles);
}
public function excludeTrailingSlashConsistencyProvider(): iterable
{
yield [__DIR__.'/../Fixtures/Exclude/ExcludeToo/'];
yield [__DIR__.'/../Fixtures/Exclude/ExcludeToo'];
yield [__DIR__.'/../Fixtures/Exclude/ExcludeToo/*'];
yield [__DIR__.'/../Fixtures/*/ExcludeToo'];
yield [__DIR__.'/../Fixtures/*/ExcludeToo/'];
yield [__DIR__.'/../Fixtures/Exclude/ExcludeToo/*'];
yield [__DIR__.'/../Fixtures/Exclude/ExcludeToo/AnotheExcludedFile.txt'];
}
} }
class TestFileLoader extends FileLoader class TestFileLoader extends FileLoader

View File

@ -177,6 +177,7 @@ class GlobResourceTest extends TestCase
$expected = [ $expected = [
$dir.'/Exclude/ExcludeToo/AnotheExcludedFile.txt', $dir.'/Exclude/ExcludeToo/AnotheExcludedFile.txt',
$dir.'/ExcludeTrailingSlash/exclude/baz.txt',
$dir.'/foo.xml', $dir.'/foo.xml',
]; ];

View File

@ -110,11 +110,6 @@ class QuestionHelper extends Helper
$inputStream = $this->inputStream ?: \STDIN; $inputStream = $this->inputStream ?: \STDIN;
$autocomplete = $question->getAutocompleterCallback(); $autocomplete = $question->getAutocompleterCallback();
if (\function_exists('sapi_windows_cp_set')) {
// Codepage used by cmd.exe on Windows to allow special characters (éàüñ).
@sapi_windows_cp_set(1252);
}
if (null === $autocomplete || !self::$stty || !Terminal::hasSttyAvailable()) { if (null === $autocomplete || !self::$stty || !Terminal::hasSttyAvailable()) {
$ret = false; $ret = false;
if ($question->isHidden()) { if ($question->isHidden()) {
@ -514,7 +509,10 @@ class QuestionHelper extends Helper
private function readInput($inputStream, Question $question) private function readInput($inputStream, Question $question)
{ {
if (!$question->isMultiline()) { if (!$question->isMultiline()) {
return fgets($inputStream, 4096); $cp = $this->setIOCodepage();
$ret = fgets($inputStream, 4096);
return $this->resetIOCodepage($cp, $ret);
} }
$multiLineStreamReader = $this->cloneInputStream($inputStream); $multiLineStreamReader = $this->cloneInputStream($inputStream);
@ -523,6 +521,7 @@ class QuestionHelper extends Helper
} }
$ret = ''; $ret = '';
$cp = $this->setIOCodepage();
while (false !== ($char = fgetc($multiLineStreamReader))) { while (false !== ($char = fgetc($multiLineStreamReader))) {
if (\PHP_EOL === "{$ret}{$char}") { if (\PHP_EOL === "{$ret}{$char}") {
break; break;
@ -530,7 +529,37 @@ class QuestionHelper extends Helper
$ret .= $char; $ret .= $char;
} }
return $ret; return $this->resetIOCodepage($cp, $ret);
}
/**
* Set console I/O to the host code page.
*
* @return int Previous code page in IBM/EBCDIC format
*/
private function setIOCodepage(): int
{
if (\function_exists('sapi_windows_cp_set')) {
$cp = sapi_windows_cp_get();
sapi_windows_cp_set(sapi_windows_cp_get('oem'));
return $cp;
}
return 0;
}
/**
* Set console I/O to the specified code page and convert the user input.
*/
private function resetIOCodepage(int $cp, string $input): string
{
if (\function_exists('sapi_windows_cp_set') && 0 < $cp) {
sapi_windows_cp_set($cp);
$input = sapi_windows_cp_conv(sapi_windows_cp_get('oem'), $cp, $input);
}
return $input;
} }
/** /**

View File

@ -180,8 +180,8 @@ abstract class FileLoader extends BaseFileLoader
$excludePrefix = $resource->getPrefix(); $excludePrefix = $resource->getPrefix();
} }
// normalize Windows slashes // normalize Windows slashes and remove trailing slashes
$excludePaths[str_replace('\\', '/', $path)] = true; $excludePaths[rtrim(str_replace('\\', '/', $path), '/')] = true;
} }
} }

View File

@ -244,6 +244,35 @@ class FileLoaderTest extends TestCase
); );
} }
/**
* @dataProvider excludeTrailingSlashConsistencyProvider
*/
public function testExcludeTrailingSlashConsistency(string $exclude)
{
$container = new ContainerBuilder();
$loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures'));
$loader->registerClasses(
new Definition(),
'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\\',
'Prototype/*',
$exclude
);
$this->assertTrue($container->has(Foo::class));
$this->assertFalse($container->has(DeeperBaz::class));
}
public function excludeTrailingSlashConsistencyProvider(): iterable
{
yield ['Prototype/OtherDir/AnotherSub/'];
yield ['Prototype/OtherDir/AnotherSub'];
yield ['Prototype/OtherDir/AnotherSub/*'];
yield ['Prototype/*/AnotherSub'];
yield ['Prototype/*/AnotherSub/'];
yield ['Prototype/*/AnotherSub/*'];
yield ['Prototype/OtherDir/AnotherSub/DeeperBaz.php'];
}
/** /**
* @requires PHP 8 * @requires PHP 8
* *

View File

@ -359,7 +359,6 @@ class FlattenException
return ['array', '*SKIPPED over 10000 entries*']; return ['array', '*SKIPPED over 10000 entries*'];
} }
if ($value instanceof \__PHP_Incomplete_Class) { if ($value instanceof \__PHP_Incomplete_Class) {
// is_object() returns false on PHP<=7.1
$result[$key] = ['incomplete-object', $this->getClassNameFromIncomplete($value)]; $result[$key] = ['incomplete-object', $this->getClassNameFromIncomplete($value)];
} elseif (\is_object($value)) { } elseif (\is_object($value)) {
$result[$key] = ['object', \get_class($value)]; $result[$key] = ['object', \get_class($value)];

View File

@ -20,6 +20,7 @@ use Symfony\Component\Form\ChoiceList\Loader\FilterChoiceLoaderDecorator;
use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
use Symfony\Component\Form\ChoiceList\View\ChoiceListView; use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Translation\TranslatableMessage;
/** /**
* Default implementation of {@link ChoiceListFactoryInterface}. * Default implementation of {@link ChoiceListFactoryInterface}.
@ -182,7 +183,14 @@ class DefaultChoiceListFactory implements ChoiceListFactoryInterface
// If "choice_label" is set to false and "expanded" is true, the value false // If "choice_label" is set to false and "expanded" is true, the value false
// should be passed on to the "label" option of the checkboxes/radio buttons // should be passed on to the "label" option of the checkboxes/radio buttons
$dynamicLabel = $label($choice, $key, $value); $dynamicLabel = $label($choice, $key, $value);
$label = false === $dynamicLabel ? false : (string) $dynamicLabel;
if (false === $dynamicLabel) {
$label = false;
} elseif ($dynamicLabel instanceof TranslatableMessage) {
$label = $dynamicLabel;
} else {
$label = (string) $dynamicLabel;
}
} }
$view = new ChoiceView( $view = new ChoiceView(

View File

@ -11,6 +11,8 @@
namespace Symfony\Component\Form\ChoiceList\View; namespace Symfony\Component\Form\ChoiceList\View;
use Symfony\Component\Translation\TranslatableMessage;
/** /**
* Represents a choice in templates. * Represents a choice in templates.
* *
@ -35,11 +37,11 @@ class ChoiceView
/** /**
* Creates a new choice view. * Creates a new choice view.
* *
* @param mixed $data The original choice * @param mixed $data The original choice
* @param string $value The view representation of the choice * @param string $value The view representation of the choice
* @param string|false $label The label displayed to humans; pass false to discard the label * @param string|TranslatableMessage|false $label The label displayed to humans; pass false to discard the label
* @param array $attr Additional attributes for the HTML tag * @param array $attr Additional attributes for the HTML tag
* @param array $labelTranslationParameters Additional parameters used to translate the label * @param array $labelTranslationParameters Additional parameters used to translate the label
*/ */
public function __construct($data, string $value, $label, array $attr = [], array $labelTranslationParameters = []) public function __construct($data, string $value, $label, array $attr = [], array $labelTranslationParameters = [])
{ {

View File

@ -21,6 +21,7 @@ use Symfony\Component\Form\ChoiceList\Loader\FilterChoiceLoaderDecorator;
use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
use Symfony\Component\Form\ChoiceList\View\ChoiceListView; use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Translation\TranslatableMessage;
class DefaultChoiceListFactoryTest extends TestCase class DefaultChoiceListFactoryTest extends TestCase
{ {
@ -759,6 +760,24 @@ class DefaultChoiceListFactoryTest extends TestCase
$this->assertFlatViewWithAttr($view); $this->assertFlatViewWithAttr($view);
} }
/**
* @requires function Symfony\Component\Translation\TranslatableMessage::__construct
*/
public function testPassTranslatableMessageAsLabelDoesntCastItToString()
{
$view = $this->factory->createView(
$this->list,
[$this->obj1],
static function ($choice, $key, $value) {
return new TranslatableMessage('my_message', ['param1' => 'value1']);
}
);
$this->assertInstanceOf(TranslatableMessage::class, $view->choices[0]->label);
$this->assertEquals('my_message', $view->choices[0]->label->getMessage());
$this->assertArrayHasKey('param1', $view->choices[0]->label->getParameters());
}
public function testCreateViewFlatLabelTranslationParametersAsArray() public function testCreateViewFlatLabelTranslationParametersAsArray()
{ {
$view = $this->factory->createView( $view = $this->factory->createView(

View File

@ -1502,7 +1502,7 @@ class Request
* if the proxy is trusted (see "setTrustedProxies()"), otherwise it returns * if the proxy is trusted (see "setTrustedProxies()"), otherwise it returns
* the latter (from the "SERVER_PROTOCOL" server parameter). * the latter (from the "SERVER_PROTOCOL" server parameter).
* *
* @return string * @return string|null
*/ */
public function getProtocolVersion() public function getProtocolVersion()
{ {

View File

@ -95,6 +95,9 @@ final class SlackTransport extends AbstractTransport
throw new TransportException(sprintf('Unable to post the Slack message: "%s".', $result['error']), $response); throw new TransportException(sprintf('Unable to post the Slack message: "%s".', $result['error']), $response);
} }
return new SentMessage($message, (string) $this); $sentMessage = new SentMessage($message, (string) $this);
$sentMessage->setMessageId($result['ts']);
return $sentMessage;
} }
} }

View File

@ -119,7 +119,7 @@ final class SlackTransportTest extends TransportTestCase
$response->expects($this->once()) $response->expects($this->once())
->method('getContent') ->method('getContent')
->willReturn(json_encode(['ok' => true])); ->willReturn(json_encode(['ok' => true, 'ts' => '1503435956.000247']));
$expectedBody = json_encode(['channel' => $channel, 'text' => $message]); $expectedBody = json_encode(['channel' => $channel, 'text' => $message]);
@ -131,7 +131,9 @@ final class SlackTransportTest extends TransportTestCase
$transport = $this->createTransport($client, $channel); $transport = $this->createTransport($client, $channel);
$transport->send(new ChatMessage('testMessage')); $sentMessage = $transport->send(new ChatMessage('testMessage'));
$this->assertSame('1503435956.000247', $sentMessage->getMessageId());
} }
public function testSendWithNotification() public function testSendWithNotification()
@ -147,7 +149,7 @@ final class SlackTransportTest extends TransportTestCase
$response->expects($this->once()) $response->expects($this->once())
->method('getContent') ->method('getContent')
->willReturn(json_encode(['ok' => true])); ->willReturn(json_encode(['ok' => true, 'ts' => '1503435956.000247']));
$notification = new Notification($message); $notification = new Notification($message);
$chatMessage = ChatMessage::fromNotification($notification); $chatMessage = ChatMessage::fromNotification($notification);
@ -167,7 +169,9 @@ final class SlackTransportTest extends TransportTestCase
$transport = $this->createTransport($client, $channel); $transport = $this->createTransport($client, $channel);
$transport->send($chatMessage); $sentMessage = $transport->send($chatMessage);
$this->assertSame('1503435956.000247', $sentMessage->getMessageId());
} }
public function testSendWithInvalidOptions() public function testSendWithInvalidOptions()

View File

@ -49,9 +49,7 @@ final class Chatter implements ChatterInterface
public function send(MessageInterface $message): ?SentMessage public function send(MessageInterface $message): ?SentMessage
{ {
if (null === $this->bus) { if (null === $this->bus) {
$this->transport->send($message); return $this->transport->send($message);
return null;
} }
if (null !== $this->dispatcher) { if (null !== $this->dispatcher) {

View File

@ -0,0 +1,63 @@
<?php
namespace Symfony\Component\Notifier\Tests;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Notifier\Chatter;
use Symfony\Component\Notifier\Message\SentMessage;
use Symfony\Component\Notifier\Tests\Transport\DummyMessage;
use Symfony\Component\Notifier\Transport\TransportInterface;
class ChatterTest extends TestCase
{
/** @var MockObject&TransportInterface */
private $transport;
/** @var MockObject&MessageBusInterface */
private $bus;
protected function setUp(): void
{
$this->transport = $this->createMock(TransportInterface::class);
$this->bus = $this->createMock(MessageBusInterface::class);
}
public function testSendWithoutBus()
{
$message = new DummyMessage();
$sentMessage = new SentMessage($message, 'any');
$this->transport
->expects($this->once())
->method('send')
->with($message)
->willReturn($sentMessage);
$chatter = new Chatter($this->transport);
$this->assertSame($sentMessage, $chatter->send($message));
$this->assertSame($message, $sentMessage->getOriginalMessage());
}
public function testSendWithBus()
{
$message = new DummyMessage();
$this->transport
->expects($this->never())
->method('send')
->with($message);
$this->bus
->expects($this->once())
->method('dispatch')
->with($message)
->willReturn(new Envelope(new \stdClass()));
$chatter = new Chatter($this->transport, $this->bus);
$this->assertNull($chatter->send($message));
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace Symfony\Component\Notifier\Tests;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Notifier\Message\SentMessage;
use Symfony\Component\Notifier\Tests\Transport\DummyMessage;
use Symfony\Component\Notifier\Texter;
use Symfony\Component\Notifier\Transport\TransportInterface;
class TexterTest extends TestCase
{
/** @var MockObject&TransportInterface */
private $transport;
/** @var MockObject&MessageBusInterface */
private $bus;
protected function setUp(): void
{
$this->transport = $this->createMock(TransportInterface::class);
$this->bus = $this->createMock(MessageBusInterface::class);
}
public function testSendWithoutBus()
{
$message = new DummyMessage();
$sentMessage = new SentMessage($message, 'any');
$this->transport
->expects($this->once())
->method('send')
->with($message)
->willReturn($sentMessage);
$texter = new Texter($this->transport);
$this->assertSame($sentMessage, $texter->send($message));
$this->assertSame($message, $sentMessage->getOriginalMessage());
}
public function testSendWithBus()
{
$message = new DummyMessage();
$this->transport
->expects($this->never())
->method('send')
->with($message);
$this->bus
->expects($this->once())
->method('dispatch')
->with($message)
->willReturn(new Envelope(new \stdClass()));
$texter = new Texter($this->transport, $this->bus);
$this->assertNull($texter->send($message));
}
}

View File

@ -0,0 +1,166 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Notifier\Tests\Transport;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Notifier\Exception\LogicException;
use Symfony\Component\Notifier\Exception\RuntimeException;
use Symfony\Component\Notifier\Exception\TransportExceptionInterface;
use Symfony\Component\Notifier\Message\SentMessage;
use Symfony\Component\Notifier\Transport\FailoverTransport;
use Symfony\Component\Notifier\Transport\TransportInterface;
/**
* @group time-sensitive
*/
class FailoverTransportTest extends TestCase
{
public function testSendNoTransports()
{
$this->expectException(LogicException::class);
new FailoverTransport([]);
}
public function testToString()
{
$t1 = $this->createMock(TransportInterface::class);
$t1->expects($this->once())->method('__toString')->willReturn('t1://local');
$t2 = $this->createMock(TransportInterface::class);
$t2->expects($this->once())->method('__toString')->willReturn('t2://local');
$t = new FailoverTransport([$t1, $t2]);
$this->assertEquals('t1://local || t2://local', (string) $t);
}
public function testSendMessageNotSupportedByAnyTransport()
{
$t1 = $this->createMock(TransportInterface::class);
$t2 = $this->createMock(TransportInterface::class);
$t = new FailoverTransport([$t1, $t2]);
$this->expectException(LogicException::class);
$t->send(new DummyMessage());
}
public function testSendFirstWork()
{
$message = new DummyMessage();
$t1 = $this->createMock(TransportInterface::class);
$t1->method('supports')->with($message)->willReturn(true);
$t1->expects($this->exactly(3))->method('send')->with($message)->willReturn(new SentMessage($message, 'test'));
$t2 = $this->createMock(TransportInterface::class);
$t2->expects($this->never())->method('send');
$t = new FailoverTransport([$t1, $t2]);
$t->send($message);
$t->send($message);
$t->send($message);
}
public function testSendAllDead()
{
$message = new DummyMessage();
$t1 = $this->createMock(TransportInterface::class);
$t1->method('supports')->with($message)->willReturn(true);
$t1->expects($this->once())->method('send')->with($message)->will($this->throwException($this->createMock(TransportExceptionInterface::class)));
$t2 = $this->createMock(TransportInterface::class);
$t2->method('supports')->with($message)->willReturn(true);
$t2->expects($this->once())->method('send')->with($message)->will($this->throwException($this->createMock(TransportExceptionInterface::class)));
$t = new FailoverTransport([$t1, $t2]);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('All transports failed.');
$t->send($message);
}
public function testSendOneDead()
{
$message = new DummyMessage();
$t1 = $this->createMock(TransportInterface::class);
$t1->method('supports')->with($message)->willReturn(true);
$t1->expects($this->once())->method('send')->will($this->throwException($this->createMock(TransportExceptionInterface::class)));
$t2 = $this->createMock(TransportInterface::class);
$t2->method('supports')->with($message)->willReturn(true);
$t2->expects($this->exactly(1))->method('send')->with($message)->willReturn(new SentMessage($message, 'test'));
$t = new FailoverTransport([$t1, $t2]);
$t->send($message);
}
public function testSendAllDeadWithinRetryPeriod()
{
$message = new DummyMessage();
$t1 = $this->createMock(TransportInterface::class);
$t1->method('supports')->with($message)->willReturn(true);
$t1->method('send')->will($this->throwException($this->createMock(TransportExceptionInterface::class)));
$t1->expects($this->once())->method('send');
$t2 = $this->createMock(TransportInterface::class);
$t2->method('supports')->with($message)->willReturn(true);
$t2->expects($this->exactly(3))
->method('send')
->willReturnOnConsecutiveCalls(
new SentMessage($message, 't2'),
new SentMessage($message, 't2'),
$this->throwException($this->createMock(TransportExceptionInterface::class))
);
$t = new FailoverTransport([$t1, $t2], 40);
$t->send($message);
sleep(4);
$t->send($message);
sleep(4);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('All transports failed.');
$t->send($message);
}
public function testSendOneDeadButRecover()
{
$message = new DummyMessage();
$t1 = $this->createMock(TransportInterface::class);
$t1->method('supports')->with($message)->willReturn(true);
$t1->expects($this->exactly(2))->method('send')->willReturnOnConsecutiveCalls(
$this->throwException($this->createMock(TransportExceptionInterface::class)),
new SentMessage($message, 't1')
);
$t2 = $this->createMock(TransportInterface::class);
$t2->method('supports')->with($message)->willReturn(true);
$t2->expects($this->exactly(2))->method('send')->willReturnOnConsecutiveCalls(
new SentMessage($message, 't2'),
$this->throwException($this->createMock(TransportExceptionInterface::class))
);
$t = new FailoverTransport([$t1, $t2], 1);
$t->send($message);
sleep(2);
$t->send($message);
}
}

View File

@ -49,9 +49,7 @@ final class Texter implements TexterInterface
public function send(MessageInterface $message): ?SentMessage public function send(MessageInterface $message): ?SentMessage
{ {
if (null === $this->bus) { if (null === $this->bus) {
$this->transport->send($message); return $this->transport->send($message);
return null;
} }
if (null !== $this->dispatcher) { if (null !== $this->dispatcher) {

View File

@ -31,6 +31,11 @@ class FailoverTransport extends RoundRobinTransport
return $this->currentTransport; return $this->currentTransport;
} }
protected function getInitialCursor(): int
{
return 0;
}
protected function getNameSymbol(): string protected function getNameSymbol(): string
{ {
return '||'; return '||';

View File

@ -27,7 +27,7 @@ class RoundRobinTransport implements TransportInterface
private $deadTransports; private $deadTransports;
private $transports = []; private $transports = [];
private $retryPeriod; private $retryPeriod;
private $cursor = 0; private $cursor = -1;
/** /**
* @param TransportInterface[] $transports * @param TransportInterface[] $transports
@ -41,9 +41,6 @@ class RoundRobinTransport implements TransportInterface
$this->transports = $transports; $this->transports = $transports;
$this->deadTransports = new \SplObjectStorage(); $this->deadTransports = new \SplObjectStorage();
$this->retryPeriod = $retryPeriod; $this->retryPeriod = $retryPeriod;
// the cursor initial value is randomized so that
// when are not in a daemon, we are still rotating the transports
$this->cursor = mt_rand(0, \count($transports) - 1);
} }
public function __toString(): string public function __toString(): string
@ -64,6 +61,10 @@ class RoundRobinTransport implements TransportInterface
public function send(MessageInterface $message): SentMessage public function send(MessageInterface $message): SentMessage
{ {
if (!$this->supports($message)) {
throw new LogicException(sprintf('None of the configured Transports of "%s" supports the given message.', static::class));
}
while ($transport = $this->getNextTransport($message)) { while ($transport = $this->getNextTransport($message)) {
try { try {
return $transport->send($message); return $transport->send($message);
@ -80,12 +81,17 @@ class RoundRobinTransport implements TransportInterface
*/ */
protected function getNextTransport(MessageInterface $message): ?TransportInterface protected function getNextTransport(MessageInterface $message): ?TransportInterface
{ {
if (-1 === $this->cursor) {
$this->cursor = $this->getInitialCursor();
}
$cursor = $this->cursor; $cursor = $this->cursor;
while (true) { while (true) {
$transport = $this->transports[$cursor]; $transport = $this->transports[$cursor];
if (!$transport->supports($message)) { if (!$transport->supports($message)) {
$cursor = $this->moveCursor($cursor); $cursor = $this->moveCursor($cursor);
continue; continue;
} }
@ -114,6 +120,13 @@ class RoundRobinTransport implements TransportInterface
return $this->deadTransports->contains($transport); return $this->deadTransports->contains($transport);
} }
protected function getInitialCursor(): int
{
// the cursor initial value is randomized so that
// when are not in a daemon, we are still rotating the transports
return mt_rand(0, \count($this->transports) - 1);
}
protected function getNameSymbol(): string protected function getNameSymbol(): string
{ {
return '&&'; return '&&';

View File

@ -22,7 +22,8 @@
}, },
"require-dev": { "require-dev": {
"symfony/event-dispatcher-contracts": "^2", "symfony/event-dispatcher-contracts": "^2",
"symfony/http-client-contracts": "^2" "symfony/http-client-contracts": "^2",
"symfony/messenger": "^4.4 || ^5.0"
}, },
"conflict": { "conflict": {
"symfony/http-kernel": "<4.4", "symfony/http-kernel": "<4.4",

View File

@ -277,17 +277,15 @@ class PhpDocExtractor implements PropertyDescriptionExtractorInterface, Property
return null; return null;
} }
try { $reflector = $reflectionProperty->getDeclaringClass();
$reflector = $reflectionProperty->getDeclaringClass();
foreach ($reflector->getTraits() as $trait) { foreach ($reflector->getTraits() as $trait) {
if ($trait->hasProperty($property)) { if ($trait->hasProperty($property)) {
$reflector = $trait; return $this->getDocBlockFromProperty($trait->getName(), $property);
break;
}
} }
}
try {
return $this->docBlockFactory->create($reflectionProperty, $this->createFromReflector($reflector)); return $this->docBlockFactory->create($reflectionProperty, $this->createFromReflector($reflector));
} catch (\InvalidArgumentException $e) { } catch (\InvalidArgumentException $e) {
return null; return null;
@ -325,17 +323,15 @@ class PhpDocExtractor implements PropertyDescriptionExtractorInterface, Property
return null; return null;
} }
try { $reflector = $reflectionMethod->getDeclaringClass();
$reflector = $reflectionMethod->getDeclaringClass();
foreach ($reflector->getTraits() as $trait) { foreach ($reflector->getTraits() as $trait) {
if ($trait->hasMethod($methodName)) { if ($trait->hasMethod($methodName)) {
$reflector = $trait; return $this->getDocBlockFromMethod($trait->getName(), $ucFirstProperty, $type);
break;
}
} }
}
try {
return [$this->docBlockFactory->create($reflectionMethod, $this->createFromReflector($reflector)), $prefix]; return [$this->docBlockFactory->create($reflectionMethod, $this->createFromReflector($reflector)), $prefix];
} catch (\InvalidArgumentException $e) { } catch (\InvalidArgumentException $e) {
return null; return null;

View File

@ -156,20 +156,8 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
return $fromConstructor; return $fromConstructor;
} }
if ($fromDefaultValue = $this->extractFromDefaultValue($class, $property)) { if ($fromPropertyDeclaration = $this->extractFromPropertyDeclaration($class, $property)) {
return $fromDefaultValue; return $fromPropertyDeclaration;
}
if (\PHP_VERSION_ID >= 70400) {
try {
$reflectionProperty = new \ReflectionProperty($class, $property);
$type = $reflectionProperty->getType();
if (null !== $type && $types = $this->extractFromReflectionType($type, $reflectionProperty->getDeclaringClass())) {
return $types;
}
} catch (\ReflectionException $e) {
// noop
}
} }
return null; return null;
@ -530,10 +518,19 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
return null; return null;
} }
private function extractFromDefaultValue(string $class, string $property): ?array private function extractFromPropertyDeclaration(string $class, string $property): ?array
{ {
try { try {
$reflectionClass = new \ReflectionClass($class); $reflectionClass = new \ReflectionClass($class);
if (\PHP_VERSION_ID >= 70400) {
$reflectionProperty = $reflectionClass->getProperty($property);
$reflectionPropertyType = $reflectionProperty->getType();
if (null !== $reflectionPropertyType && $types = $this->extractFromReflectionType($reflectionPropertyType, $reflectionProperty->getDeclaringClass())) {
return $types;
}
}
} catch (\ReflectionException $e) { } catch (\ReflectionException $e) {
return null; return null;
} }
@ -547,7 +544,7 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
$type = \gettype($defaultValue); $type = \gettype($defaultValue);
$type = static::MAP_TYPES[$type] ?? $type; $type = static::MAP_TYPES[$type] ?? $type;
return [new Type($type, false, null, Type::BUILTIN_TYPE_ARRAY === $type)]; return [new Type($type, $this->isNullableProperty($class, $property), null, Type::BUILTIN_TYPE_ARRAY === $type)];
} }
private function extractFromReflectionType(\ReflectionType $reflectionType, \ReflectionClass $declaringClass): array private function extractFromReflectionType(\ReflectionType $reflectionType, \ReflectionClass $declaringClass): array
@ -587,12 +584,31 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
return $name; return $name;
} }
private function isNullableProperty(string $class, string $property): bool
{
try {
$reflectionProperty = new \ReflectionProperty($class, $property);
if (\PHP_VERSION_ID >= 70400) {
$reflectionPropertyType = $reflectionProperty->getType();
return null !== $reflectionPropertyType && $reflectionPropertyType->allowsNull();
}
return false;
} catch (\ReflectionException $e) {
// Return false if the property doesn't exist
}
return false;
}
private function isAllowedProperty(string $class, string $property): bool private function isAllowedProperty(string $class, string $property): bool
{ {
try { try {
$reflectionProperty = new \ReflectionProperty($class, $property); $reflectionProperty = new \ReflectionProperty($class, $property);
return $reflectionProperty->getModifiers() & $this->propertyReflectionFlags; return (bool) ($reflectionProperty->getModifiers() & $this->propertyReflectionFlags);
} catch (\ReflectionException $e) { } catch (\ReflectionException $e) {
// Return false if the property doesn't exist // Return false if the property doesn't exist
} }

View File

@ -109,7 +109,11 @@ class PhpDocExtractorTest extends TestCase
['a', [new Type(Type::BUILTIN_TYPE_INT)], 'A.', null], ['a', [new Type(Type::BUILTIN_TYPE_INT)], 'A.', null],
['b', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')], 'B.', null], ['b', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy')], 'B.', null],
['c', [new Type(Type::BUILTIN_TYPE_BOOL, true)], null, null], ['c', [new Type(Type::BUILTIN_TYPE_BOOL, true)], null, null],
['ct', [new Type(Type::BUILTIN_TYPE_TRUE, true)], null, null],
['cf', [new Type(Type::BUILTIN_TYPE_FALSE, true)], null, null],
['d', [new Type(Type::BUILTIN_TYPE_BOOL)], null, null], ['d', [new Type(Type::BUILTIN_TYPE_BOOL)], null, null],
['dt', [new Type(Type::BUILTIN_TYPE_TRUE)], null, null],
['df', [new Type(Type::BUILTIN_TYPE_FALSE)], null, null],
['e', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_RESOURCE))], null, null], ['e', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_RESOURCE))], null, null],
['f', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTime'))], null, null], ['f', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'DateTime'))], null, null],
['g', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)], 'Nullable array.', null], ['g', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true)], 'Nullable array.', null],
@ -341,6 +345,9 @@ class PhpDocExtractorTest extends TestCase
['propertyInTraitPrimitiveType', new Type(Type::BUILTIN_TYPE_STRING)], ['propertyInTraitPrimitiveType', new Type(Type::BUILTIN_TYPE_STRING)],
['propertyInTraitObjectSameNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, DummyUsedInTrait::class)], ['propertyInTraitObjectSameNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, DummyUsedInTrait::class)],
['propertyInTraitObjectDifferentNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)], ['propertyInTraitObjectDifferentNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)],
['propertyInExternalTraitPrimitiveType', new Type(Type::BUILTIN_TYPE_STRING)],
['propertyInExternalTraitObjectSameNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)],
['propertyInExternalTraitObjectDifferentNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, DummyUsedInTrait::class)],
]; ];
} }
@ -358,6 +365,9 @@ class PhpDocExtractorTest extends TestCase
['methodInTraitPrimitiveType', new Type(Type::BUILTIN_TYPE_STRING)], ['methodInTraitPrimitiveType', new Type(Type::BUILTIN_TYPE_STRING)],
['methodInTraitObjectSameNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, DummyUsedInTrait::class)], ['methodInTraitObjectSameNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, DummyUsedInTrait::class)],
['methodInTraitObjectDifferentNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)], ['methodInTraitObjectDifferentNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)],
['methodInExternalTraitPrimitiveType', new Type(Type::BUILTIN_TYPE_STRING)],
['methodInExternalTraitObjectSameNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)],
['methodInExternalTraitObjectDifferentNamespace', new Type(Type::BUILTIN_TYPE_OBJECT, false, DummyUsedInTrait::class)],
]; ];
} }

View File

@ -88,7 +88,11 @@ class ReflectionExtractorTest extends TestCase
'date', 'date',
'element', 'element',
'c', 'c',
'ct',
'cf',
'd', 'd',
'dt',
'df',
'e', 'e',
'f', 'f',
], ],
@ -134,7 +138,11 @@ class ReflectionExtractorTest extends TestCase
'parentAnnotationNoParent', 'parentAnnotationNoParent',
'date', 'date',
'c', 'c',
'ct',
'cf',
'd', 'd',
'dt',
'df',
'e', 'e',
'f', 'f',
], ],
@ -444,6 +452,7 @@ class ReflectionExtractorTest extends TestCase
$this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)], $this->extractor->getTypes(Php74Dummy::class, 'dummy')); $this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)], $this->extractor->getTypes(Php74Dummy::class, 'dummy'));
$this->assertEquals([new Type(Type::BUILTIN_TYPE_BOOL, true)], $this->extractor->getTypes(Php74Dummy::class, 'nullableBoolProp')); $this->assertEquals([new Type(Type::BUILTIN_TYPE_BOOL, true)], $this->extractor->getTypes(Php74Dummy::class, 'nullableBoolProp'));
$this->assertEquals([new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))], $this->extractor->getTypes(Php74Dummy::class, 'stringCollection')); $this->assertEquals([new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))], $this->extractor->getTypes(Php74Dummy::class, 'stringCollection'));
$this->assertEquals([new Type(Type::BUILTIN_TYPE_INT, true)], $this->extractor->getTypes(Php74Dummy::class, 'nullableWithDefault'));
$this->assertEquals([new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)], $this->extractor->getTypes(Php74Dummy::class, 'collection')); $this->assertEquals([new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)], $this->extractor->getTypes(Php74Dummy::class, 'collection'));
} }

View File

@ -0,0 +1,56 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\PropertyInfo\Tests\Fixtures;
use Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\DummyUsedInTrait;
trait DummyTraitExternal
{
/**
* @var string
*/
private $propertyInExternalTraitPrimitiveType;
/**
* @var Dummy
*/
private $propertyInExternalTraitObjectSameNamespace;
/**
* @var DummyUsedInTrait
*/
private $propertyInExternalTraitObjectDifferentNamespace;
/**
* @return string
*/
public function getMethodInExternalTraitPrimitiveType()
{
return 'value';
}
/**
* @return Dummy
*/
public function getMethodInExternalTraitObjectSameNamespace()
{
return new Dummy();
}
/**
* @return DummyUsedInTrait
*/
public function getMethodInExternalTraitObjectDifferentNamespace()
{
return new DummyUsedInTrait();
}
}

View File

@ -65,6 +65,20 @@ class ParentDummy
{ {
} }
/**
* @return true|null
*/
public function isCt()
{
}
/**
* @return false|null
*/
public function isCf()
{
}
/** /**
* @return bool * @return bool
*/ */
@ -72,6 +86,20 @@ class ParentDummy
{ {
} }
/**
* @return true
*/
public function canDt()
{
}
/**
* @return false
*/
public function canDf()
{
}
/** /**
* @param resource $e * @param resource $e
*/ */

View File

@ -20,6 +20,7 @@ class Php74Dummy
private ?bool $nullableBoolProp; private ?bool $nullableBoolProp;
/** @var string[] */ /** @var string[] */
private array $stringCollection; private array $stringCollection;
private ?int $nullableWithDefault = 1;
public array $collection = []; public array $collection = [];
public function addStringCollection(string $string): void public function addStringCollection(string $string): void

View File

@ -12,9 +12,12 @@
namespace Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage; namespace Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage;
use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyTraitExternal;
trait DummyTrait trait DummyTrait
{ {
use DummyTraitExternal;
/** /**
* @var string * @var string
*/ */

View File

@ -24,6 +24,8 @@ class Type
public const BUILTIN_TYPE_FLOAT = 'float'; public const BUILTIN_TYPE_FLOAT = 'float';
public const BUILTIN_TYPE_STRING = 'string'; public const BUILTIN_TYPE_STRING = 'string';
public const BUILTIN_TYPE_BOOL = 'bool'; public const BUILTIN_TYPE_BOOL = 'bool';
public const BUILTIN_TYPE_TRUE = 'true';
public const BUILTIN_TYPE_FALSE = 'false';
public const BUILTIN_TYPE_RESOURCE = 'resource'; public const BUILTIN_TYPE_RESOURCE = 'resource';
public const BUILTIN_TYPE_OBJECT = 'object'; public const BUILTIN_TYPE_OBJECT = 'object';
public const BUILTIN_TYPE_ARRAY = 'array'; public const BUILTIN_TYPE_ARRAY = 'array';
@ -41,6 +43,8 @@ class Type
self::BUILTIN_TYPE_FLOAT, self::BUILTIN_TYPE_FLOAT,
self::BUILTIN_TYPE_STRING, self::BUILTIN_TYPE_STRING,
self::BUILTIN_TYPE_BOOL, self::BUILTIN_TYPE_BOOL,
self::BUILTIN_TYPE_TRUE,
self::BUILTIN_TYPE_FALSE,
self::BUILTIN_TYPE_RESOURCE, self::BUILTIN_TYPE_RESOURCE,
self::BUILTIN_TYPE_OBJECT, self::BUILTIN_TYPE_OBJECT,
self::BUILTIN_TYPE_ARRAY, self::BUILTIN_TYPE_ARRAY,

View File

@ -64,12 +64,20 @@
</trans-unit> </trans-unit>
<trans-unit id="17"> <trans-unit id="17">
<source>Too many failed login attempts, please try again later.</source> <source>Too many failed login attempts, please try again later.</source>
<target>Troppi tentaivi di login falliti. Riprova tra un po'.</target> <target>Troppi tentativi di login falliti, riprova tra un po'.</target>
</trans-unit> </trans-unit>
<trans-unit id="18"> <trans-unit id="18">
<source>Invalid or expired login link.</source> <source>Invalid or expired login link.</source>
<target>Link di login scaduto o non valido.</target> <target>Link di login scaduto o non valido.</target>
</trans-unit> </trans-unit>
<trans-unit id="19">
<source>Too many failed login attempts, please try again in %minutes% minute.</source>
<target>Troppi tentativi di login falliti, riprova tra %minutes% minuto.</target>
</trans-unit>
<trans-unit id="20">
<source>Too many failed login attempts, please try again in %minutes% minutes.</source>
<target>Troppi tentativi di login falliti, riprova tra %minutes% minuti.</target>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View File

@ -63,13 +63,21 @@
<target>アカウントはロックされています。</target> <target>アカウントはロックされています。</target>
</trans-unit> </trans-unit>
<trans-unit id="17"> <trans-unit id="17">
<source>Too many failed login attempts, please try again later.</source> <source>Too many failed login attempts, please try again later.</source>
<target>ログイン試行回数を超えました。しばらくして再度お試しください。</target> <target>ログイン試行回数を超えました。しばらくして再度お試しください。</target>
</trans-unit> </trans-unit>
<trans-unit id="18"> <trans-unit id="18">
<source>Invalid or expired login link.</source> <source>Invalid or expired login link.</source>
<target>ログインリンクが有効期限切れ、もしくは無効です。</target> <target>ログインリンクが有効期限切れ、もしくは無効です。</target>
</trans-unit> </trans-unit>
<trans-unit id="19">
<source>Too many failed login attempts, please try again in %minutes% minute.</source>
<target>ログイン試行回数が多すぎます。%minutes%分後に再度お試しください。</target>
</trans-unit>
<trans-unit id="20">
<source>Too many failed login attempts, please try again in %minutes% minutes.</source>
<target>ログイン試行回数が多すぎます。%minutes%分後に再度お試しください。</target>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View File

@ -70,6 +70,14 @@
<source>Invalid or expired login link.</source> <source>Invalid or expired login link.</source>
<target>Nieprawidłowy lub wygasły link logowania.</target> <target>Nieprawidłowy lub wygasły link logowania.</target>
</trans-unit> </trans-unit>
<trans-unit id="19">
<source>Too many failed login attempts, please try again in %minutes% minute.</source>
<target>Zbyt wiele nieudanych prób logowania, spróbuj ponownie po upływie %minutes% minut.</target>
</trans-unit>
<trans-unit id="20">
<source>Too many failed login attempts, please try again in %minutes% minutes.</source>
<target>Zbyt wiele nieudanych prób logowania, spróbuj ponownie po upływie %minutes% minut.</target>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View File

@ -70,6 +70,14 @@
<source>Invalid or expired login link.</source> <source>Invalid or expired login link.</source>
<target>Link de login inválido ou expirado.</target> <target>Link de login inválido ou expirado.</target>
</trans-unit> </trans-unit>
<trans-unit id="19">
<source>Too many failed login attempts, please try again in %minutes% minute.</source>
<target>Muitas tentativas de login inválidas, por favor, tente novamente em um minuto.</target>
</trans-unit>
<trans-unit id="20">
<source>Too many failed login attempts, please try again in %minutes% minutes.</source>
<target>Muitas tentativas de login inválidas, por favor, tente novamente em %minutes% minutos.</target>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View File

@ -70,6 +70,14 @@
<source>Invalid or expired login link.</source> <source>Invalid or expired login link.</source>
<target>Ссылка для входа недействительна или просрочена.</target> <target>Ссылка для входа недействительна или просрочена.</target>
</trans-unit> </trans-unit>
<trans-unit id="19">
<source>Too many failed login attempts, please try again in %minutes% minute.</source>
<target>Слишком много неудачных попыток входа в систему, повторите попытку через %minutes% минуту.</target>
</trans-unit>
<trans-unit id="20">
<source>Too many failed login attempts, please try again in %minutes% minutes.</source>
<target>Слишком много неудачных попыток входа в систему, повторите попытку через %minutes% мин.</target>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View File

@ -23,6 +23,7 @@ class IdentityTranslatorTest extends TranslatorTest
parent::setUp(); parent::setUp();
$this->defaultLocale = \Locale::getDefault(); $this->defaultLocale = \Locale::getDefault();
\Locale::setDefault('en');
} }
protected function tearDown(): void protected function tearDown(): void

View File

@ -166,7 +166,6 @@ class VarCloner extends AbstractCloner
break; break;
case \is_object($v): case \is_object($v):
case $v instanceof \__PHP_Incomplete_Class:
if (empty($objRefs[$h = spl_object_id($v)])) { if (empty($objRefs[$h = spl_object_id($v)])) {
$stub = new Stub(); $stub = new Stub();
$stub->type = Stub::TYPE_OBJECT; $stub->type = Stub::TYPE_OBJECT;

View File

@ -60,7 +60,7 @@ class Exporter
$value = self::prepare($value, $objectsPool, $refsPool, $objectsCount, $valueIsStatic); $value = self::prepare($value, $objectsPool, $refsPool, $objectsCount, $valueIsStatic);
} }
goto handle_value; goto handle_value;
} elseif (!\is_object($value) && !$value instanceof \__PHP_Incomplete_Class) { } elseif (!\is_object($value) || $value instanceof \UnitEnum) {
goto handle_value; goto handle_value;
} }
@ -188,7 +188,7 @@ class Exporter
public static function export($value, string $indent = '') public static function export($value, string $indent = '')
{ {
switch (true) { switch (true) {
case \is_int($value) || \is_float($value): return var_export($value, true); case \is_int($value) || \is_float($value) || $value instanceof \UnitEnum: return var_export($value, true);
case [] === $value: return '[]'; case [] === $value: return '[]';
case false === $value: return 'false'; case false === $value: return 'false';
case true === $value: return 'true'; case true === $value: return 'true';

View File

@ -0,0 +1,8 @@
<?php
namespace Symfony\Component\VarExporter\Tests\Fixtures;
enum FooUnitEnum
{
case Bar;
}

View File

@ -0,0 +1,5 @@
<?php
return [
Symfony\Component\VarExporter\Tests\Fixtures\FooUnitEnum::Bar,
];

View File

@ -16,6 +16,7 @@ use Symfony\Component\VarDumper\Test\VarDumperTestTrait;
use Symfony\Component\VarExporter\Exception\ClassNotFoundException; use Symfony\Component\VarExporter\Exception\ClassNotFoundException;
use Symfony\Component\VarExporter\Exception\NotInstantiableTypeException; use Symfony\Component\VarExporter\Exception\NotInstantiableTypeException;
use Symfony\Component\VarExporter\Internal\Registry; use Symfony\Component\VarExporter\Internal\Registry;
use Symfony\Component\VarExporter\Tests\Fixtures\FooUnitEnum;
use Symfony\Component\VarExporter\VarExporter; use Symfony\Component\VarExporter\VarExporter;
class VarExporterTest extends TestCase class VarExporterTest extends TestCase
@ -209,6 +210,10 @@ class VarExporterTest extends TestCase
yield ['private-constructor', PrivateConstructor::create('bar')]; yield ['private-constructor', PrivateConstructor::create('bar')];
yield ['php74-serializable', new Php74Serializable()]; yield ['php74-serializable', new Php74Serializable()];
if (\PHP_VERSION_ID >= 80100) {
yield ['unit-enum', [FooUnitEnum::Bar], true];
}
} }
} }

View File

@ -44,7 +44,7 @@ final class VarExporter
{ {
$isStaticValue = true; $isStaticValue = true;
if (!\is_object($value) && !(\is_array($value) && $value) && !$value instanceof \__PHP_Incomplete_Class && !\is_resource($value)) { if (!\is_object($value) && !(\is_array($value) && $value) && !\is_resource($value) || $value instanceof \UnitEnum) {
return Exporter::export($value); return Exporter::export($value);
} }