Skip to content

Commit 00a6a2d

Browse files
Merge branch '7.1' into 7.2
* 7.1: remove conflict with symfony/serializer < 6.4 Reviewed and Translated zh_CN [PropertyInfo] Fix write visibility for Asymmetric Visibility and Virtual Properties [Translation] [Bridge][Lokalise] Fix empty keys array in PUT, DELETE requests causing Lokalise API error
2 parents 8be876c + 9e82d84 commit 00a6a2d

File tree

8 files changed

+215
-23
lines changed

8 files changed

+215
-23
lines changed

src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -709,12 +709,18 @@ private function isAllowedProperty(string $class, string $property, bool $writeA
709709
try {
710710
$reflectionProperty = new \ReflectionProperty($class, $property);
711711

712-
if ($writeAccessRequired && $reflectionProperty->isReadOnly()) {
713-
return false;
714-
}
712+
if ($writeAccessRequired) {
713+
if ($reflectionProperty->isReadOnly()) {
714+
return false;
715+
}
716+
717+
if (\PHP_VERSION_ID >= 80400 && ($reflectionProperty->isProtectedSet() || $reflectionProperty->isPrivateSet())) {
718+
return false;
719+
}
715720

716-
if (\PHP_VERSION_ID >= 80400 && $writeAccessRequired && ($reflectionProperty->isProtectedSet() || $reflectionProperty->isPrivateSet())) {
717-
return false;
721+
if (\PHP_VERSION_ID >= 80400 &&$reflectionProperty->isVirtual() && !$reflectionProperty->hasHook(\PropertyHookType::Set)) {
722+
return false;
723+
}
718724
}
719725

720726
return (bool) ($reflectionProperty->getModifiers() & $this->propertyReflectionFlags);
@@ -955,6 +961,20 @@ private function getReadVisiblityForMethod(\ReflectionMethod $reflectionMethod):
955961

956962
private function getWriteVisiblityForProperty(\ReflectionProperty $reflectionProperty): string
957963
{
964+
if (\PHP_VERSION_ID >= 80400) {
965+
if ($reflectionProperty->isVirtual() && !$reflectionProperty->hasHook(\PropertyHookType::Set)) {
966+
return PropertyWriteInfo::VISIBILITY_PRIVATE;
967+
}
968+
969+
if ($reflectionProperty->isPrivateSet()) {
970+
return PropertyWriteInfo::VISIBILITY_PRIVATE;
971+
}
972+
973+
if ($reflectionProperty->isProtectedSet()) {
974+
return PropertyWriteInfo::VISIBILITY_PROTECTED;
975+
}
976+
}
977+
958978
if ($reflectionProperty->isPrivate()) {
959979
return PropertyWriteInfo::VISIBILITY_PRIVATE;
960980
}

src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use Symfony\Component\PropertyInfo\Tests\Fixtures\Php81Dummy;
3333
use Symfony\Component\PropertyInfo\Tests\Fixtures\Php82Dummy;
3434
use Symfony\Component\PropertyInfo\Tests\Fixtures\SnakeCaseDummy;
35+
use Symfony\Component\PropertyInfo\Tests\Fixtures\VirtualProperties;
3536
use Symfony\Component\PropertyInfo\Type as LegacyType;
3637
use Symfony\Component\TypeInfo\Type;
3738
use Symfony\Component\TypeInfo\Type\NullableType;
@@ -702,6 +703,69 @@ public function testAsymmetricVisibility()
702703
$this->assertFalse($this->extractor->isWritable(AsymmetricVisibility::class, 'protectedPrivate'));
703704
}
704705

706+
/**
707+
* @requires PHP 8.4
708+
*/
709+
public function testVirtualProperties()
710+
{
711+
$this->assertTrue($this->extractor->isReadable(VirtualProperties::class, 'virtualNoSetHook'));
712+
$this->assertTrue($this->extractor->isReadable(VirtualProperties::class, 'virtualSetHookOnly'));
713+
$this->assertTrue($this->extractor->isReadable(VirtualProperties::class, 'virtualHook'));
714+
$this->assertFalse($this->extractor->isWritable(VirtualProperties::class, 'virtualNoSetHook'));
715+
$this->assertTrue($this->extractor->isWritable(VirtualProperties::class, 'virtualSetHookOnly'));
716+
$this->assertTrue($this->extractor->isWritable(VirtualProperties::class, 'virtualHook'));
717+
}
718+
719+
/**
720+
* @dataProvider provideAsymmetricVisibilityMutator
721+
* @requires PHP 8.4
722+
*/
723+
public function testAsymmetricVisibilityMutator(string $property, string $readVisibility, string $writeVisibility)
724+
{
725+
$extractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PUBLIC | ReflectionExtractor::ALLOW_PROTECTED | ReflectionExtractor::ALLOW_PRIVATE);
726+
$readMutator = $extractor->getReadInfo(AsymmetricVisibility::class, $property);
727+
$writeMutator = $extractor->getWriteInfo(AsymmetricVisibility::class, $property, [
728+
'enable_getter_setter_extraction' => true,
729+
]);
730+
731+
$this->assertSame(PropertyReadInfo::TYPE_PROPERTY, $readMutator->getType());
732+
$this->assertSame(PropertyWriteInfo::TYPE_PROPERTY, $writeMutator->getType());
733+
$this->assertSame($readVisibility, $readMutator->getVisibility());
734+
$this->assertSame($writeVisibility, $writeMutator->getVisibility());
735+
}
736+
737+
public static function provideAsymmetricVisibilityMutator(): iterable
738+
{
739+
yield ['publicPrivate', PropertyReadInfo::VISIBILITY_PUBLIC, PropertyWriteInfo::VISIBILITY_PRIVATE];
740+
yield ['publicProtected', PropertyReadInfo::VISIBILITY_PUBLIC, PropertyWriteInfo::VISIBILITY_PROTECTED];
741+
yield ['protectedPrivate', PropertyReadInfo::VISIBILITY_PROTECTED, PropertyWriteInfo::VISIBILITY_PRIVATE];
742+
}
743+
744+
/**
745+
* @dataProvider provideVirtualPropertiesMutator
746+
* @requires PHP 8.4
747+
*/
748+
public function testVirtualPropertiesMutator(string $property, string $readVisibility, string $writeVisibility)
749+
{
750+
$extractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PUBLIC | ReflectionExtractor::ALLOW_PROTECTED | ReflectionExtractor::ALLOW_PRIVATE);
751+
$readMutator = $extractor->getReadInfo(VirtualProperties::class, $property);
752+
$writeMutator = $extractor->getWriteInfo(VirtualProperties::class, $property, [
753+
'enable_getter_setter_extraction' => true,
754+
]);
755+
756+
$this->assertSame(PropertyReadInfo::TYPE_PROPERTY, $readMutator->getType());
757+
$this->assertSame(PropertyWriteInfo::TYPE_PROPERTY, $writeMutator->getType());
758+
$this->assertSame($readVisibility, $readMutator->getVisibility());
759+
$this->assertSame($writeVisibility, $writeMutator->getVisibility());
760+
}
761+
762+
public static function provideVirtualPropertiesMutator(): iterable
763+
{
764+
yield ['virtualNoSetHook', PropertyReadInfo::VISIBILITY_PUBLIC, PropertyWriteInfo::VISIBILITY_PRIVATE];
765+
yield ['virtualSetHookOnly', PropertyReadInfo::VISIBILITY_PUBLIC, PropertyWriteInfo::VISIBILITY_PUBLIC];
766+
yield ['virtualHook', PropertyReadInfo::VISIBILITY_PUBLIC, PropertyWriteInfo::VISIBILITY_PUBLIC];
767+
}
768+
705769
/**
706770
* @dataProvider typesProvider
707771
*/
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\PropertyInfo\Tests\Fixtures;
13+
14+
class VirtualProperties
15+
{
16+
public bool $virtualNoSetHook { get => true; }
17+
public bool $virtualSetHookOnly { set => $value; }
18+
public bool $virtualHook { get => true; set => $value; }
19+
}

src/Symfony/Component/PropertyInfo/composer.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@
3737
"conflict": {
3838
"phpdocumentor/reflection-docblock": "<5.2",
3939
"phpdocumentor/type-resolver": "<1.5.1",
40-
"symfony/dependency-injection": "<6.4",
41-
"symfony/serializer": "<6.4"
40+
"symfony/dependency-injection": "<6.4"
4241
},
4342
"autoload": {
4443
"psr-4": { "Symfony\\Component\\PropertyInfo\\": "" },

src/Symfony/Component/Security/Core/Resources/translations/security.zh_CN.xlf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
</trans-unit>
7777
<trans-unit id="20">
7878
<source>Too many failed login attempts, please try again in %minutes% minutes.</source>
79-
<target state="needs-review-translation">登录尝试失败次数过多,请在 %minutes% 分钟后再试。|登录尝试失败次数过多,请在 %minutes% 分钟后再试。</target>
79+
<target>登录尝试失败次数过多,请在 %minutes% 分钟后重试。</target>
8080
</trans-unit>
8181
</body>
8282
</file>

src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ public function delete(TranslatorBagInterface $translatorBag): void
113113
$keysIds += $this->getKeysIds($keysToDelete, $domain);
114114
}
115115

116+
if (!$keysIds) {
117+
return;
118+
}
119+
116120
$response = $this->client->request('DELETE', 'keys', [
117121
'json' => ['keys' => array_values($keysIds)],
118122
]);
@@ -245,6 +249,10 @@ private function updateTranslations(array $keysByDomain, TranslatorBagInterface
245249
}
246250
}
247251

252+
if (!$keysToUpdate) {
253+
return;
254+
}
255+
248256
$response = $this->client->request('PUT', 'keys', [
249257
'json' => ['keys' => $keysToUpdate],
250258
]);

src/Symfony/Component/Translation/Bridge/Lokalise/Tests/LokaliseProviderTest.php

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,56 @@ public function testCompleteWriteProcess()
251251
$this->assertTrue($updateProcessed, 'Translations update was not called.');
252252
}
253253

254+
public function testUpdateProcessWhenLocalTranslationsMatchLokaliseTranslations()
255+
{
256+
$getLanguagesResponse = function (string $method, string $url): ResponseInterface {
257+
$this->assertSame('GET', $method);
258+
$this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/languages', $url);
259+
260+
return new MockResponse(json_encode([
261+
'languages' => [
262+
['lang_iso' => 'en'],
263+
['lang_iso' => 'fr'],
264+
],
265+
]));
266+
};
267+
268+
$failOnPutRequest = function (string $method, string $url, array $options = []): void {
269+
$this->assertSame('PUT', $method);
270+
$this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/keys', $url);
271+
$this->assertSame(json_encode(['keys' => []]), $options['body']);
272+
273+
$this->fail('PUT request is invalid: an empty `keys` array was provided, resulting in a Lokalise API error');
274+
};
275+
276+
$mockHttpClient = (new MockHttpClient([
277+
$getLanguagesResponse,
278+
$failOnPutRequest,
279+
]))->withOptions([
280+
'base_uri' => 'https://api.lokalise.com/api2/projects/PROJECT_ID/',
281+
'headers' => ['X-Api-Token' => 'API_KEY'],
282+
]);
283+
284+
$provider = self::createProvider(
285+
$mockHttpClient,
286+
$this->getLoader(),
287+
$this->getLogger(),
288+
$this->getDefaultLocale(),
289+
'api.lokalise.com'
290+
);
291+
292+
// TranslatorBag with catalogues that do not store any message to mimic the behaviour of
293+
// Symfony\Component\Translation\Command\TranslationPushCommand when local translations and Lokalise
294+
// translations match without any changes in both translation sets
295+
$translatorBag = new TranslatorBag();
296+
$translatorBag->addCatalogue(new MessageCatalogue('en', []));
297+
$translatorBag->addCatalogue(new MessageCatalogue('fr', []));
298+
299+
$provider->write($translatorBag);
300+
301+
$this->assertSame(1, $mockHttpClient->getRequestsCount());
302+
}
303+
254304
public function testWriteGetLanguageServerError()
255305
{
256306
$getLanguagesResponse = function (string $method, string $url, array $options = []): ResponseInterface {
@@ -723,6 +773,38 @@ public function testDeleteProcess()
723773
$provider->delete($translatorBag);
724774
}
725775

776+
public function testDeleteProcessWhenLocalTranslationsMatchLokaliseTranslations()
777+
{
778+
$failOnDeleteRequest = function (string $method, string $url, array $options = []): void {
779+
$this->assertSame('DELETE', $method);
780+
$this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/keys', $url);
781+
$this->assertSame(json_encode(['keys' => []]), $options['body']);
782+
783+
$this->fail('DELETE request is invalid: an empty `keys` array was provided, resulting in a Lokalise API error');
784+
};
785+
786+
// TranslatorBag with catalogues that do not store any message to mimic the behaviour of
787+
// Symfony\Component\Translation\Command\TranslationPushCommand when local translations and Lokalise
788+
// translations match without any changes in both translation sets
789+
$translatorBag = new TranslatorBag();
790+
$translatorBag->addCatalogue(new MessageCatalogue('en', []));
791+
$translatorBag->addCatalogue(new MessageCatalogue('fr', []));
792+
793+
$mockHttpClient = new MockHttpClient([$failOnDeleteRequest], 'https://api.lokalise.com/api2/projects/PROJECT_ID/');
794+
795+
$provider = self::createProvider(
796+
$mockHttpClient,
797+
$this->getLoader(),
798+
$this->getLogger(),
799+
$this->getDefaultLocale(),
800+
'api.lokalise.com'
801+
);
802+
803+
$provider->delete($translatorBag);
804+
805+
$this->assertSame(0, $mockHttpClient->getRequestsCount());
806+
}
807+
726808
public static function getResponsesForOneLocaleAndOneDomain(): \Generator
727809
{
728810
$arrayLoader = new ArrayLoader();

src/Symfony/Component/Validator/Resources/translations/validators.zh_CN.xlf

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@
136136
</trans-unit>
137137
<trans-unit id="37" resname="This is not a valid IP address.">
138138
<source>This value is not a valid IP address.</source>
139-
<target state="needs-review-translation">该值不是有效的IP地址。</target>
139+
<target>该值不是有效的IP地址。</target>
140140
</trans-unit>
141141
<trans-unit id="38">
142142
<source>This value is not a valid language.</source>
@@ -192,7 +192,7 @@
192192
</trans-unit>
193193
<trans-unit id="51" resname="No temporary folder was configured in php.ini.">
194194
<source>No temporary folder was configured in php.ini, or the configured folder does not exist.</source>
195-
<target state="needs-review-translation">php.ini 中没有配置临时文件夹,或配置的文件夹不存在。</target>
195+
<target>php.ini 中未配置临时文件夹,或配置的文件夹不存在。</target>
196196
</trans-unit>
197197
<trans-unit id="52">
198198
<source>Cannot write temporary file to disk.</source>
@@ -224,7 +224,7 @@
224224
</trans-unit>
225225
<trans-unit id="59" resname="This is not a valid International Bank Account Number (IBAN).">
226226
<source>This value is not a valid International Bank Account Number (IBAN).</source>
227-
<target state="needs-review-translation">该值不是有效的国际银行账号(IBAN)。</target>
227+
<target>该值不是有效的国际银行账号(IBAN)。</target>
228228
</trans-unit>
229229
<trans-unit id="60">
230230
<source>This value is not a valid ISBN-10.</source>
@@ -312,15 +312,15 @@
312312
</trans-unit>
313313
<trans-unit id="81" resname="This is not a valid Business Identifier Code (BIC).">
314314
<source>This value is not a valid Business Identifier Code (BIC).</source>
315-
<target state="needs-review-translation">该值不是有效的业务标识符代码(BIC)。</target>
315+
<target>该值不是有效的银行识别代码(BIC)。</target>
316316
</trans-unit>
317317
<trans-unit id="82">
318318
<source>Error</source>
319319
<target>错误</target>
320320
</trans-unit>
321321
<trans-unit id="83" resname="This is not a valid UUID.">
322322
<source>This value is not a valid UUID.</source>
323-
<target state="needs-review-translation">该值不是有效的UUID。</target>
323+
<target>该值不是有效的UUID。</target>
324324
</trans-unit>
325325
<trans-unit id="84">
326326
<source>This value should be a multiple of {{ compared_value }}.</source>
@@ -428,43 +428,43 @@
428428
</trans-unit>
429429
<trans-unit id="110">
430430
<source>The extension of the file is invalid ({{ extension }}). Allowed extensions are {{ extensions }}.</source>
431-
<target state="needs-review-translation">文件的扩展名无效 ({{ extension }})。允许的扩展名为 {{ extensions }}。</target>
431+
<target>文件的扩展名无效 ({{ extension }})。允许的扩展名为 {{ extensions }}。</target>
432432
</trans-unit>
433433
<trans-unit id="111">
434434
<source>The detected character encoding is invalid ({{ detected }}). Allowed encodings are {{ encodings }}.</source>
435-
<target state="needs-review-translation">检测到的字符编码无效 ({{ detected }})。允许的编码为 {{ encodings }}。</target>
435+
<target>检测到的字符编码无效 ({{ detected }})。允许的编码为 {{ encodings }}。</target>
436436
</trans-unit>
437437
<trans-unit id="112">
438438
<source>This value is not a valid MAC address.</source>
439-
<target state="needs-review-translation">该值不是有效的MAC地址。</target>
439+
<target>该值不是有效的MAC地址。</target>
440440
</trans-unit>
441441
<trans-unit id="113">
442442
<source>This URL is missing a top-level domain.</source>
443-
<target state="needs-review-translation">此URL缺少顶级域名。</target>
443+
<target>此URL缺少顶级域名。</target>
444444
</trans-unit>
445445
<trans-unit id="114">
446446
<source>This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words.</source>
447-
<target state="needs-translation">This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words.</target>
447+
<target>该值太短,应该至少包含一个词。|该值太短,应该至少包含 {{ min }} 个词。</target>
448448
</trans-unit>
449449
<trans-unit id="115">
450450
<source>This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less.</source>
451-
<target state="needs-translation">This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less.</target>
451+
<target>该值太长,应该只包含一个词。|该值太长,应该只包含 {{ max }} 个或更少个词。</target>
452452
</trans-unit>
453453
<trans-unit id="116">
454454
<source>This value does not represent a valid week in the ISO 8601 format.</source>
455-
<target state="needs-translation">This value does not represent a valid week in the ISO 8601 format.</target>
455+
<target>该值不代表 ISO 8601 格式中的有效周。</target>
456456
</trans-unit>
457457
<trans-unit id="117">
458458
<source>This value is not a valid week.</source>
459-
<target state="needs-translation">This value is not a valid week.</target>
459+
<target>该值不是一个有效周。</target>
460460
</trans-unit>
461461
<trans-unit id="118">
462462
<source>This value should not be before week "{{ min }}".</source>
463-
<target state="needs-translation">This value should not be before week "{{ min }}".</target>
463+
<target>该值不应位于 "{{ min }}" 周之前。</target>
464464
</trans-unit>
465465
<trans-unit id="119">
466466
<source>This value should not be after week "{{ max }}".</source>
467-
<target state="needs-translation">This value should not be after week "{{ max }}".</target>
467+
<target>该值不应位于 "{{ max }}"周之后。</target>
468468
</trans-unit>
469469
</body>
470470
</file>

0 commit comments

Comments
 (0)