Skip to content

Commit 7b11cda

Browse files
authored
Merge pull request #516 from Philipp91/unlimited-repeated
Add support for @unlimited repeated fields
2 parents ba68825 + 20ee220 commit 7b11cda

File tree

5 files changed

+114
-12
lines changed

5 files changed

+114
-12
lines changed

lib/Fhp/Segment/BaseDescriptor.php

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@ abstract class BaseDescriptor
3232
protected function __construct(\ReflectionClass $clazz)
3333
{
3434
// Use reflection to map PHP class fields to elements in the segment/Deg.
35-
$implicitIndex = true;
3635
$nextIndex = 0;
3736
foreach (static::enumerateProperties($clazz) as $property) {
37+
if ($nextIndex === null) {
38+
throw new \InvalidArgumentException("Disallowed property $property after an @Unlimited field");
39+
}
3840
$docComment = $property->getDocComment() ?: '';
3941
if (static::getBoolAnnotation('Ignore', $docComment)) {
4042
continue; // Skip @Ignore-d propeties.
@@ -44,22 +46,35 @@ protected function __construct(\ReflectionClass $clazz)
4446
$descriptor = new ElementDescriptor();
4547
$descriptor->field = $property->getName();
4648
$maxCount = static::getIntAnnotation('Max', $docComment);
49+
$unlimitedCount = static::getBoolAnnotation('Unlimited', $docComment);
4750
if ($type = static::getVarAnnotation($docComment)) {
4851
if (str_ends_with($type, '|null')) { // Nullable field
4952
$descriptor->optional = true;
5053
$type = substr($type, 0, -5);
5154
}
5255
if (str_ends_with($type, '[]')) { // Array/repeated field
53-
if ($maxCount === null) {
54-
throw new \InvalidArgumentException("Repeated property $property needs @Max() annotation");
55-
}
56-
$descriptor->repeated = $maxCount;
5756
$type = substr($type, 0, -2);
58-
// If a repeated field is followed by anything at all, there will be an empty entry for each possible
59-
// repeated value (in extreme cases, there can be hundreds of consecutive `+`, for instance).
60-
$nextIndex += $maxCount;
57+
if ($unlimitedCount) {
58+
$descriptor->repeated = PHP_INT_MAX;
59+
// A repeated field of unlimited size cannot be followed by anything, because it would not be
60+
// clear which of the following values still belong to the repeated field vs to the next field.
61+
$nextIndex = null;
62+
} elseif ($maxCount !== null) {
63+
$descriptor->repeated = $maxCount;
64+
// If there's another field value after this repeated field, then a serialized message will
65+
// contain placeholders (i.e. empty field values separated by possibly hundreds of `+`) to fill
66+
// up to the repeated field's maximum length, after which the next message continues at the next
67+
// index.
68+
$nextIndex += $maxCount;
69+
} else {
70+
throw new \InvalidArgumentException(
71+
"Repeated property $property needs @Max(.) or (rarely) @Unlimited annotation"
72+
);
73+
}
6174
} elseif ($maxCount !== null) {
6275
throw new \InvalidArgumentException("@Max() annotation not recognized on single $property");
76+
} elseif ($unlimitedCount) {
77+
throw new \InvalidArgumentException("@Unlimited annotation not recognized on single $property");
6378
} else {
6479
++$nextIndex; // Singular field, so the index advances by 1.
6580
}
@@ -90,7 +105,7 @@ protected function __construct(\ReflectionClass $clazz)
90105
throw new \InvalidArgumentException("No fields found in $clazz->name");
91106
}
92107
ksort($this->elements); // Make sure elements are parsed in wire-format order.
93-
$this->maxIndex = $nextIndex - 1;
108+
$this->maxIndex = $nextIndex === null ? PHP_INT_MAX : $nextIndex - 1;
94109
}
95110

96111
/**

lib/Fhp/Segment/VPP/ParameterNamensabgleichPruefauftragV1.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ class ParameterNamensabgleichPruefauftragV1 extends BaseDeg
2525

2626
public string $unterstuetztePaymentStatusReportDatenformate;
2727

28-
/** @var string[] @Max(999999) Max length each: 6 */
28+
/** @var string[] @Unlimited Max length each: 6 */
2929
public array $vopPflichtigerZahlungsverkehrsauftrag;
3030
}

lib/Fhp/Syntax/Serializer.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,22 @@ private static function serializeElements($obj, BaseDescriptor $descriptor): arr
110110
throw new \InvalidArgumentException(
111111
"Expected array value for $descriptor->class.$elementDescriptor->field, got: $value");
112112
}
113-
for ($repetition = 0; $repetition < $elementDescriptor->repeated; ++$repetition) {
113+
if ($elementDescriptor->repeated === PHP_INT_MAX) {
114+
// For an uncapped repeated field (with @Unlimited), it must be the very last field and we do not
115+
// need to insert padding elements, so we only output its actual contents.
116+
if ($index !== $lastKey) {
117+
throw new \AssertionError(
118+
"Expected unlimited field at $index to be the last one, but the last one is $lastKey"
119+
);
120+
}
121+
$numOutputElements = count($value);
122+
} else {
123+
// For a capped repeated field (with @Max), we need to output the specified number of elements, such
124+
// that subsequent fields will be at the right place. If this is the last field, the trailing empty
125+
// elements will be trimmed away again by flattenAndTrimEnd() later.
126+
$numOutputElements = $elementDescriptor->repeated;
127+
}
128+
for ($repetition = 0; $repetition < $numOutputElements; ++$repetition) {
114129
$serializedElements[$index + $repetition] = static::serializeElement(
115130
$value === null || $repetition >= count($value) ? null : $value[$repetition],
116131
$elementDescriptor->type, $isSegment);
@@ -129,7 +144,7 @@ private static function serializeElements($obj, BaseDescriptor $descriptor): arr
129144
*/
130145
private static function serializeElement($value, $type, bool $fullySerialize)
131146
{
132-
if (is_string($type)) {
147+
if (is_string($type)) { // Scalar value / DE
133148
return static::serializeDataElement($value, $type);
134149
} elseif ($type->getName() === Bin::class) {
135150
/* @var Bin|null $value */
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace Fhp\Segment;
4+
5+
use Fhp\Segment\VPP\HIVPPSv1;
6+
use Fhp\Segment\VPP\ParameterNamensabgleichPruefauftragV1;
7+
use Fhp\Syntax\Parser;
8+
use PHPUnit\Framework\TestCase;
9+
10+
/**
11+
* Among other things, this test covers the serialization of arrays with @Max annotation.
12+
*/
13+
class HIVPPSTest extends TestCase
14+
{
15+
private HIVPPSv1 $hivpps;
16+
17+
public function setUp(): void
18+
{
19+
$this->hivpps = HIVPPSv1::createEmpty();
20+
$this->hivpps->setSegmentNumber(42);
21+
$this->hivpps->maximaleAnzahlAuftraege = 43;
22+
$this->hivpps->anzahlSignaturenMindestens = 44;
23+
$this->hivpps->sicherheitsklasse = 45;
24+
$this->hivpps->parameter = new ParameterNamensabgleichPruefauftragV1();
25+
$this->hivpps->parameter->maximaleAnzahlCreditTransferTransactionInformationOptIn = 1;
26+
$this->hivpps->parameter->aufklaerungstextStrukturiert = true;
27+
$this->hivpps->parameter->artDerLieferungPaymentStatusReport = 'Art';
28+
$this->hivpps->parameter->sammelzahlungenMitEinemAuftragErlaubt = false;
29+
$this->hivpps->parameter->eingabeAnzahlEintraegeErlaubt = false;
30+
$this->hivpps->parameter->unterstuetztePaymentStatusReportDatenformate = 'Test';
31+
}
32+
33+
public function testPopulatedArray()
34+
{
35+
$this->hivpps->parameter->vopPflichtigerZahlungsverkehrsauftrag = ['HKFOO', 'HKBAR'];
36+
37+
$serialized = $this->hivpps->serialize();
38+
$this->assertEquals("HIVPPS:42:1+43+44+45+1:J:Art:N:N:Test:HKFOO:HKBAR'", $serialized);
39+
40+
/** @var HIVPPSv1 $parsed */
41+
$parsed = Parser::parseSegment($serialized, HIVPPSv1::class);
42+
$this->assertEquals(['HKFOO', 'HKBAR'], $parsed->parameter->vopPflichtigerZahlungsverkehrsauftrag);
43+
}
44+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace Fhp\Segment;
4+
5+
use Fhp\Segment\VPP\HKVPPv1;
6+
use Fhp\Syntax\Parser;
7+
use PHPUnit\Framework\TestCase;
8+
9+
/**
10+
* Among other things, this test covers the serialization of arrays with @Max annotation.
11+
*/
12+
class HKVPPTest extends TestCase
13+
{
14+
public function testSerialize()
15+
{
16+
$hkvpp = HKVPPv1::createEmpty();
17+
$hkvpp->setSegmentNumber(42);
18+
$hkvpp->unterstuetztePaymentStatusReports->paymentStatusReportDescriptor = ['A', 'B', 'C'];
19+
20+
$serialized = $hkvpp->serialize();
21+
$this->assertEquals("HKVPP:42:1+A:B:C'", $serialized);
22+
23+
/** @var HKVPPv1 $hkvpp */
24+
$hkvpp = Parser::parseSegment($serialized, HKVPPv1::class);
25+
$this->assertEquals(42, $hkvpp->getSegmentNumber());
26+
$this->assertEquals(['A', 'B', 'C'], $hkvpp->unterstuetztePaymentStatusReports->paymentStatusReportDescriptor);
27+
}
28+
}

0 commit comments

Comments
 (0)