Skip to content

Commit f132c09

Browse files
Merge branch '5.4' into 6.4
* 5.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 eec07d9 + 5955c9b commit f132c09

File tree

7 files changed

+214
-21
lines changed

7 files changed

+214
-21
lines changed

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

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

579-
if ($writeAccessRequired && $reflectionProperty->isReadOnly()) {
580-
return false;
581-
}
579+
if ($writeAccessRequired) {
580+
if ($reflectionProperty->isReadOnly()) {
581+
return false;
582+
}
583+
584+
if (\PHP_VERSION_ID >= 80400 && ($reflectionProperty->isProtectedSet() || $reflectionProperty->isPrivateSet())) {
585+
return false;
586+
}
582587

583-
if (\PHP_VERSION_ID >= 80400 && $writeAccessRequired && ($reflectionProperty->isProtectedSet() || $reflectionProperty->isPrivateSet())) {
584-
return false;
588+
if (\PHP_VERSION_ID >= 80400 &&$reflectionProperty->isVirtual() && !$reflectionProperty->hasHook(\PropertyHookType::Set)) {
589+
return false;
590+
}
585591
}
586592

587593
return (bool) ($reflectionProperty->getModifiers() & $this->propertyReflectionFlags);
@@ -822,6 +828,20 @@ private function getReadVisiblityForMethod(\ReflectionMethod $reflectionMethod):
822828

823829
private function getWriteVisiblityForProperty(\ReflectionProperty $reflectionProperty): string
824830
{
831+
if (\PHP_VERSION_ID >= 80400) {
832+
if ($reflectionProperty->isVirtual() && !$reflectionProperty->hasHook(\PropertyHookType::Set)) {
833+
return PropertyWriteInfo::VISIBILITY_PRIVATE;
834+
}
835+
836+
if ($reflectionProperty->isPrivateSet()) {
837+
return PropertyWriteInfo::VISIBILITY_PRIVATE;
838+
}
839+
840+
if ($reflectionProperty->isProtectedSet()) {
841+
return PropertyWriteInfo::VISIBILITY_PROTECTED;
842+
}
843+
}
844+
825845
if ($reflectionProperty->isPrivate()) {
826846
return PropertyWriteInfo::VISIBILITY_PRIVATE;
827847
}

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use Symfony\Component\PropertyInfo\Tests\Fixtures\Php7ParentDummy;
2929
use Symfony\Component\PropertyInfo\Tests\Fixtures\Php81Dummy;
3030
use Symfony\Component\PropertyInfo\Tests\Fixtures\SnakeCaseDummy;
31+
use Symfony\Component\PropertyInfo\Tests\Fixtures\VirtualProperties;
3132
use Symfony\Component\PropertyInfo\Type;
3233

3334
/**
@@ -670,4 +671,67 @@ public function testAsymmetricVisibility()
670671
$this->assertFalse($this->extractor->isWritable(AsymmetricVisibility::class, 'publicProtected'));
671672
$this->assertFalse($this->extractor->isWritable(AsymmetricVisibility::class, 'protectedPrivate'));
672673
}
674+
675+
/**
676+
* @requires PHP 8.4
677+
*/
678+
public function testVirtualProperties()
679+
{
680+
$this->assertTrue($this->extractor->isReadable(VirtualProperties::class, 'virtualNoSetHook'));
681+
$this->assertTrue($this->extractor->isReadable(VirtualProperties::class, 'virtualSetHookOnly'));
682+
$this->assertTrue($this->extractor->isReadable(VirtualProperties::class, 'virtualHook'));
683+
$this->assertFalse($this->extractor->isWritable(VirtualProperties::class, 'virtualNoSetHook'));
684+
$this->assertTrue($this->extractor->isWritable(VirtualProperties::class, 'virtualSetHookOnly'));
685+
$this->assertTrue($this->extractor->isWritable(VirtualProperties::class, 'virtualHook'));
686+
}
687+
688+
/**
689+
* @dataProvider provideAsymmetricVisibilityMutator
690+
* @requires PHP 8.4
691+
*/
692+
public function testAsymmetricVisibilityMutator(string $property, string $readVisibility, string $writeVisibility)
693+
{
694+
$extractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PUBLIC | ReflectionExtractor::ALLOW_PROTECTED | ReflectionExtractor::ALLOW_PRIVATE);
695+
$readMutator = $extractor->getReadInfo(AsymmetricVisibility::class, $property);
696+
$writeMutator = $extractor->getWriteInfo(AsymmetricVisibility::class, $property, [
697+
'enable_getter_setter_extraction' => true,
698+
]);
699+
700+
$this->assertSame(PropertyReadInfo::TYPE_PROPERTY, $readMutator->getType());
701+
$this->assertSame(PropertyWriteInfo::TYPE_PROPERTY, $writeMutator->getType());
702+
$this->assertSame($readVisibility, $readMutator->getVisibility());
703+
$this->assertSame($writeVisibility, $writeMutator->getVisibility());
704+
}
705+
706+
public static function provideAsymmetricVisibilityMutator(): iterable
707+
{
708+
yield ['publicPrivate', PropertyReadInfo::VISIBILITY_PUBLIC, PropertyWriteInfo::VISIBILITY_PRIVATE];
709+
yield ['publicProtected', PropertyReadInfo::VISIBILITY_PUBLIC, PropertyWriteInfo::VISIBILITY_PROTECTED];
710+
yield ['protectedPrivate', PropertyReadInfo::VISIBILITY_PROTECTED, PropertyWriteInfo::VISIBILITY_PRIVATE];
711+
}
712+
713+
/**
714+
* @dataProvider provideVirtualPropertiesMutator
715+
* @requires PHP 8.4
716+
*/
717+
public function testVirtualPropertiesMutator(string $property, string $readVisibility, string $writeVisibility)
718+
{
719+
$extractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PUBLIC | ReflectionExtractor::ALLOW_PROTECTED | ReflectionExtractor::ALLOW_PRIVATE);
720+
$readMutator = $extractor->getReadInfo(VirtualProperties::class, $property);
721+
$writeMutator = $extractor->getWriteInfo(VirtualProperties::class, $property, [
722+
'enable_getter_setter_extraction' => true,
723+
]);
724+
725+
$this->assertSame(PropertyReadInfo::TYPE_PROPERTY, $readMutator->getType());
726+
$this->assertSame(PropertyWriteInfo::TYPE_PROPERTY, $writeMutator->getType());
727+
$this->assertSame($readVisibility, $readMutator->getVisibility());
728+
$this->assertSame($writeVisibility, $writeMutator->getVisibility());
729+
}
730+
731+
public static function provideVirtualPropertiesMutator(): iterable
732+
{
733+
yield ['virtualNoSetHook', PropertyReadInfo::VISIBILITY_PUBLIC, PropertyWriteInfo::VISIBILITY_PRIVATE];
734+
yield ['virtualSetHookOnly', PropertyReadInfo::VISIBILITY_PUBLIC, PropertyWriteInfo::VISIBILITY_PUBLIC];
735+
yield ['virtualHook', PropertyReadInfo::VISIBILITY_PUBLIC, PropertyWriteInfo::VISIBILITY_PUBLIC];
736+
}
673737
}
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/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
@@ -127,6 +127,10 @@ public function delete(TranslatorBagInterface $translatorBag): void
127127
$keysIds += $this->getKeysIds($keysToDelete, $domain);
128128
}
129129

130+
if (!$keysIds) {
131+
return;
132+
}
133+
130134
$response = $this->client->request('DELETE', 'keys', [
131135
'json' => ['keys' => array_values($keysIds)],
132136
]);
@@ -259,6 +263,10 @@ private function updateTranslations(array $keysByDomain, TranslatorBagInterface
259263
}
260264
}
261265

266+
if (!$keysToUpdate) {
267+
return;
268+
}
269+
262270
$response = $this->client->request('PUT', 'keys', [
263271
'json' => ['keys' => $keysToUpdate],
264272
]);

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)