Skip to content

Commit d824d53

Browse files
Merge branch '7.2' into 7.3
* 7.2: Fixes XliffFileDumperTest for 6.4 [HttpClient] Don't send any default content-type when the body is empty [VarExporter] Fix lazy objects with hooked properties [Translation] check empty notes ntfy-notifier: tfix in description
2 parents fd9c5b4 + 983e8fa commit d824d53

File tree

12 files changed

+249
-10
lines changed

12 files changed

+249
-10
lines changed

src/Symfony/Component/HttpClient/HttpClientTrait.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,9 +355,11 @@ private static function normalizeBody($body, array &$normalizedHeaders = [])
355355
}
356356
});
357357

358-
$body = http_build_query($body, '', '&');
358+
if ('' === $body = http_build_query($body, '', '&')) {
359+
return '';
360+
}
359361

360-
if ('' === $body || !$streams && !str_contains($normalizedHeaders['content-type'][0] ?? '', 'multipart/form-data')) {
362+
if (!$streams && !str_contains($normalizedHeaders['content-type'][0] ?? '', 'multipart/form-data')) {
361363
if (!str_contains($normalizedHeaders['content-type'][0] ?? '', 'application/x-www-form-urlencoded')) {
362364
$normalizedHeaders['content-type'] = ['Content-Type: application/x-www-form-urlencoded'];
363365
}

src/Symfony/Component/Notifier/Bridge/Ntfy/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "symfony/ntfy-notifier",
33
"type": "symfony-notifier-bridge",
4-
"description": "Symfony Ntyf Notifier Bridge",
4+
"description": "Symfony Ntfy Notifier Bridge",
55
"keywords": ["ntfy", "notifier"],
66
"homepage": "https://symfony.com",
77
"license": "MIT",

src/Symfony/Component/Translation/Dumper/XliffFileDumper.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ private function dumpXliff2(string $defaultLocale, MessageCatalogue $messages, ?
176176
$metadata = $messages->getMetadata($source, $domain);
177177

178178
// Add notes section
179-
if ($this->hasMetadataArrayInfo('notes', $metadata)) {
179+
if ($this->hasMetadataArrayInfo('notes', $metadata) && $metadata['notes']) {
180180
$notesElement = $dom->createElement('notes');
181181
foreach ($metadata['notes'] as $note) {
182182
$n = $dom->createElement('note');

src/Symfony/Component/Translation/Tests/Dumper/XliffFileDumperTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,24 @@ public function testDumpCatalogueWithXliffExtension()
148148
);
149149
}
150150

151+
public function testEmptyMetadataNotes()
152+
{
153+
$catalogue = new MessageCatalogue('en_US');
154+
$catalogue->add([
155+
'empty' => 'notes',
156+
'full' => 'notes',
157+
]);
158+
$catalogue->setMetadata('empty', ['notes' => []]);
159+
$catalogue->setMetadata('full', ['notes' => [['category' => 'file-source', 'priority' => 1, 'content' => 'test/path/to/translation/Example.1.html.twig:27']]]);
160+
161+
$dumper = new XliffFileDumper();
162+
163+
$this->assertStringEqualsFile(
164+
__DIR__.'/../Fixtures/resources-2.0-empty-notes.xlf',
165+
$dumper->formatCatalogue($catalogue, 'messages', ['default_locale' => 'fr_FR', 'xliff_version' => '2.0'])
166+
);
167+
}
168+
151169
public function testFormatCatalogueXliff2WithSegmentAttributes()
152170
{
153171
$catalogue = new MessageCatalogue('en_US');
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="fr-FR" trgLang="en-US">
3+
<file id="messages.en_US">
4+
<unit id="Ll7LrI6" name="empty">
5+
<segment>
6+
<source>empty</source>
7+
<target>notes</target>
8+
</segment>
9+
</unit>
10+
<unit id="hU8PAYC" name="full">
11+
<notes>
12+
<note category="file-source" priority="1">test/path/to/translation/Example.1.html.twig:27</note>
13+
</notes>
14+
<segment>
15+
<source>full</source>
16+
<target>notes</target>
17+
</segment>
18+
</unit>
19+
</file>
20+
</xliff>

src/Symfony/Component/VarExporter/Internal/Hydrator.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,8 @@ public static function getPropertyScopes($class): array
277277

278278
if (\ReflectionProperty::IS_PROTECTED & $flags) {
279279
$propertyScopes["\0*\0$name"] = $propertyScopes[$name];
280+
} elseif (\PHP_VERSION_ID >= 80400 && $property->getHooks()) {
281+
$propertyScopes[$name][] = true;
280282
}
281283
}
282284

src/Symfony/Component/VarExporter/Internal/LazyObjectRegistry.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class LazyObjectRegistry
5050
public static function getClassResetters($class)
5151
{
5252
$classProperties = [];
53+
$hookedProperties = [];
5354

5455
if ((self::$classReflectors[$class] ??= new \ReflectionClass($class))->isInternal()) {
5556
$propertyScopes = [];
@@ -60,7 +61,13 @@ public static function getClassResetters($class)
6061
foreach ($propertyScopes as $key => [$scope, $name, $readonlyScope]) {
6162
$propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name;
6263

63-
if ($k === $key && "\0$class\0lazyObjectState" !== $k) {
64+
if ($k !== $key || "\0$class\0lazyObjectState" === $k) {
65+
continue;
66+
}
67+
68+
if ($k === $name && ($propertyScopes[$k][4] ?? false)) {
69+
$hookedProperties[$k] = true;
70+
} else {
6471
$classProperties[$readonlyScope ?? $scope][$name] = $key;
6572
}
6673
}

src/Symfony/Component/VarExporter/ProxyHelper.php

Lines changed: 104 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,37 @@ public static function generateLazyGhost(\ReflectionClass $class): string
5858
throw new LogicException(\sprintf('Cannot generate lazy ghost: class "%s" extends "%s" which is internal.', $class->name, $parent->name));
5959
}
6060
}
61+
62+
$hooks = '';
63+
$propertyScopes = Hydrator::$propertyScopes[$class->name] ??= Hydrator::getPropertyScopes($class->name);
64+
foreach ($propertyScopes as $name => $scope) {
65+
if (!isset($scope[4]) || ($p = $scope[3])->isVirtual()) {
66+
continue;
67+
}
68+
69+
$type = self::exportType($p);
70+
$hooks .= "\n public {$type} \${$name} {\n";
71+
72+
foreach ($p->getHooks() as $hook => $method) {
73+
if ($method->isFinal()) {
74+
throw new LogicException(sprintf('Cannot generate lazy ghost: hook "%s::%s()" is final.', $class->name, $method->name));
75+
}
76+
77+
if ('get' === $hook) {
78+
$ref = ($method->returnsReference() ? '&' : '');
79+
$hooks .= " {$ref}get { \$this->initializeLazyObject(); return parent::\${$name}::get(); }\n";
80+
} elseif ('set' === $hook) {
81+
$parameters = self::exportParameters($method, true);
82+
$arg = '$'.$method->getParameters()[0]->name;
83+
$hooks .= " set({$parameters}) { \$this->initializeLazyObject(); parent::\${$name}::set({$arg}); }\n";
84+
} else {
85+
throw new LogicException(sprintf('Cannot generate lazy ghost: hook "%s::%s()" is not supported.', $class->name, $method->name));
86+
}
87+
}
88+
89+
$hooks .= " }\n";
90+
}
91+
6192
$propertyScopes = self::exportPropertyScopes($class->name);
6293

6394
return <<<EOPHP
@@ -66,7 +97,7 @@ public static function generateLazyGhost(\ReflectionClass $class): string
6697
use \Symfony\Component\VarExporter\LazyGhostTrait;
6798
6899
private const LAZY_OBJECT_PROPERTY_SCOPES = {$propertyScopes};
69-
}
100+
{$hooks}}
70101
71102
// Help opcache.preload discover always-needed symbols
72103
class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class);
@@ -95,14 +126,74 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf
95126
throw new LogicException(\sprintf('Cannot generate lazy proxy with PHP < 8.3: class "%s" is readonly.', $class->name));
96127
}
97128

129+
$hookedProperties = [];
130+
if (\PHP_VERSION_ID >= 80400 && $class) {
131+
$propertyScopes = Hydrator::$propertyScopes[$class->name] ??= Hydrator::getPropertyScopes($class->name);
132+
foreach ($propertyScopes as $name => $scope) {
133+
if (isset($scope[4]) && !($p = $scope[3])->isVirtual()) {
134+
$hookedProperties[$name] = [$p, $p->getHooks()];
135+
}
136+
}
137+
}
138+
98139
$methodReflectors = [$class?->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) ?? []];
99140
foreach ($interfaces as $interface) {
100141
if (!$interface->isInterface()) {
101142
throw new LogicException(\sprintf('Cannot generate lazy proxy: "%s" is not an interface.', $interface->name));
102143
}
103144
$methodReflectors[] = $interface->getMethods();
145+
146+
if (\PHP_VERSION_ID >= 80400 && !$class) {
147+
foreach ($interface->getProperties() as $p) {
148+
$hookedProperties[$p->name] ??= [$p, []];
149+
$hookedProperties[$p->name][1] += $p->getHooks();
150+
}
151+
}
152+
}
153+
154+
$hooks = '';
155+
foreach ($hookedProperties as $name => [$p, $methods]) {
156+
$type = self::exportType($p);
157+
$hooks .= "\n public {$type} \${$p->name} {\n";
158+
159+
foreach ($methods as $hook => $method) {
160+
if ($method->isFinal()) {
161+
throw new LogicException(sprintf('Cannot generate lazy proxy: hook "%s::%s()" is final.', $class->name, $method->name));
162+
}
163+
164+
if ('get' === $hook) {
165+
$ref = ($method->returnsReference() ? '&' : '');
166+
$hooks .= <<<EOPHP
167+
{$ref}get {
168+
if (isset(\$this->lazyObjectState)) {
169+
return (\$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)())->{$p->name};
170+
}
171+
172+
return parent::\${$p->name}::get();
173+
}
174+
175+
EOPHP;
176+
} elseif ('set' === $hook) {
177+
$parameters = self::exportParameters($method, true);
178+
$arg = '$'.$method->getParameters()[0]->name;
179+
$hooks .= <<<EOPHP
180+
set({$parameters}) {
181+
if (isset(\$this->lazyObjectState)) {
182+
\$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)();
183+
\$this->lazyObjectState->realInstance->{$p->name} = {$arg};
184+
}
185+
186+
parent::\${$p->name}::set({$arg});
187+
}
188+
189+
EOPHP;
190+
} else {
191+
throw new LogicException(sprintf('Cannot generate lazy proxy: hook "%s::%s()" is not supported.', $class->name, $method->name));
192+
}
193+
}
194+
195+
$hooks .= " }\n";
104196
}
105-
$methodReflectors = array_merge(...$methodReflectors);
106197

107198
$extendsInternalClass = false;
108199
if ($parent = $class) {
@@ -112,6 +203,7 @@ public static function generateLazyProxy(?\ReflectionClass $class, array $interf
112203
}
113204
$methodsHaveToBeProxied = $extendsInternalClass;
114205
$methods = [];
206+
$methodReflectors = array_merge(...$methodReflectors);
115207

116208
foreach ($methodReflectors as $method) {
117209
if ('__get' !== strtolower($method->name) || 'mixed' === ($type = self::exportType($method) ?? 'mixed')) {
@@ -228,7 +320,7 @@ public function __unserialize(\$data): void
228320
{$lazyProxyTraitStatement}
229321
230322
private const LAZY_OBJECT_PROPERTY_SCOPES = {$propertyScopes};
231-
{$body}}
323+
{$hooks}{$body}}
232324
233325
// Help opcache.preload discover always-needed symbols
234326
class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class);
@@ -238,7 +330,7 @@ class_exists(\Symfony\Component\VarExporter\Internal\LazyObjectState::class);
238330
EOPHP;
239331
}
240332

241-
public static function exportSignature(\ReflectionFunctionAbstract $function, bool $withParameterTypes = true, ?string &$args = null): string
333+
public static function exportParameters(\ReflectionFunctionAbstract $function, bool $withParameterTypes = true, ?string &$args = null): string
242334
{
243335
$byRefIndex = 0;
244336
$args = '';
@@ -268,8 +360,15 @@ public static function exportSignature(\ReflectionFunctionAbstract $function, bo
268360
$args = implode(', ', $args);
269361
}
270362

363+
return implode(', ', $parameters);
364+
}
365+
366+
public static function exportSignature(\ReflectionFunctionAbstract $function, bool $withParameterTypes = true, ?string &$args = null): string
367+
{
368+
$parameters = self::exportParameters($function, $withParameterTypes, $args);
369+
271370
$signature = 'function '.($function->returnsReference() ? '&' : '')
272-
.($function->isClosure() ? '' : $function->name).'('.implode(', ', $parameters).')';
371+
.($function->isClosure() ? '' : $function->name).'('.$parameters.')';
273372

274373
if ($function instanceof \ReflectionMethod) {
275374
$signature = ($function->isPublic() ? 'public ' : ($function->isProtected() ? 'protected ' : 'private '))
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\VarExporter\Tests\Fixtures;
13+
14+
class Hooked
15+
{
16+
public int $notBacked {
17+
get { return 123; }
18+
set { throw \LogicException('Cannot set value.'); }
19+
}
20+
21+
public int $backed {
22+
get { return $this->backed ??= 234; }
23+
set { $this->backed = $value; }
24+
}
25+
}

src/Symfony/Component/VarExporter/Tests/LazyGhostTraitTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
1717
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
1818
use Symfony\Component\VarExporter\ProxyHelper;
19+
use Symfony\Component\VarExporter\Tests\Fixtures\Hooked;
1920
use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\ChildMagicClass;
2021
use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\ChildStdClass;
2122
use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\ChildTestClass;
@@ -291,6 +292,31 @@ public function testReinitLazyGhost()
291292
$this->assertSame(3, $object->public);
292293
}
293294

295+
/**
296+
* @requires PHP 8.4
297+
*/
298+
public function testPropertyHooks()
299+
{
300+
$initialized = false;
301+
$object = $this->createLazyGhost(Hooked::class, function ($instance) use (&$initialized) {
302+
$initialized = true;
303+
});
304+
305+
$this->assertSame(123, $object->notBacked);
306+
$this->assertFalse($initialized);
307+
$this->assertSame(234, $object->backed);
308+
$this->assertTrue($initialized);
309+
310+
$initialized = false;
311+
$object = $this->createLazyGhost(Hooked::class, function ($instance) use (&$initialized) {
312+
$initialized = true;
313+
});
314+
315+
$object->backed = 345;
316+
$this->assertTrue($initialized);
317+
$this->assertSame(345, $object->backed);
318+
}
319+
294320
/**
295321
* @template T
296322
*

src/Symfony/Component/VarExporter/Tests/LazyProxyTraitTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\Component\VarExporter\Exception\LogicException;
1919
use Symfony\Component\VarExporter\LazyProxyTrait;
2020
use Symfony\Component\VarExporter\ProxyHelper;
21+
use Symfony\Component\VarExporter\Tests\Fixtures\Hooked;
2122
use Symfony\Component\VarExporter\Tests\Fixtures\LazyGhost\RegularClass;
2223
use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\FinalPublicClass;
2324
use Symfony\Component\VarExporter\Tests\Fixtures\LazyProxy\ReadOnlyClass;
@@ -321,6 +322,33 @@ public function testReinitReadonlyLazyProxy()
321322
$this->assertSame(234, $object->foo);
322323
}
323324

325+
/**
326+
* @requires PHP 8.4
327+
*/
328+
public function testPropertyHooks()
329+
{
330+
$initialized = false;
331+
$object = $this->createLazyProxy(Hooked::class, function () use (&$initialized) {
332+
$initialized = true;
333+
return new Hooked();
334+
});
335+
336+
$this->assertSame(123, $object->notBacked);
337+
$this->assertFalse($initialized);
338+
$this->assertSame(234, $object->backed);
339+
$this->assertTrue($initialized);
340+
341+
$initialized = false;
342+
$object = $this->createLazyProxy(Hooked::class, function () use (&$initialized) {
343+
$initialized = true;
344+
return new Hooked();
345+
});
346+
347+
$object->backed = 345;
348+
$this->assertTrue($initialized);
349+
$this->assertSame(345, $object->backed);
350+
}
351+
324352
/**
325353
* @template T
326354
*

0 commit comments

Comments
 (0)