Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 24 additions & 9 deletions lib/Fhp/Segment/BaseDescriptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ abstract class BaseDescriptor
protected function __construct(\ReflectionClass $clazz)
{
// Use reflection to map PHP class fields to elements in the segment/Deg.
$implicitIndex = true;
$nextIndex = 0;
foreach (static::enumerateProperties($clazz) as $property) {
if ($nextIndex === null) {
throw new \InvalidArgumentException("Disallowed property $property after an @Unlimited field");
}
$docComment = $property->getDocComment() ?: '';
if (static::getBoolAnnotation('Ignore', $docComment)) {
continue; // Skip @Ignore-d propeties.
Expand All @@ -44,22 +46,35 @@ protected function __construct(\ReflectionClass $clazz)
$descriptor = new ElementDescriptor();
$descriptor->field = $property->getName();
$maxCount = static::getIntAnnotation('Max', $docComment);
$unlimitedCount = static::getBoolAnnotation('Unlimited', $docComment);
if ($type = static::getVarAnnotation($docComment)) {
if (str_ends_with($type, '|null')) { // Nullable field
$descriptor->optional = true;
$type = substr($type, 0, -5);
}
if (str_ends_with($type, '[]')) { // Array/repeated field
if ($maxCount === null) {
throw new \InvalidArgumentException("Repeated property $property needs @Max() annotation");
}
$descriptor->repeated = $maxCount;
$type = substr($type, 0, -2);
// If a repeated field is followed by anything at all, there will be an empty entry for each possible
// repeated value (in extreme cases, there can be hundreds of consecutive `+`, for instance).
$nextIndex += $maxCount;
if ($unlimitedCount) {
$descriptor->repeated = PHP_INT_MAX;
// A repeated field of unlimited size cannot be followed by anything, because it would not be
// clear which of the following values still belong to the repeated field vs to the next field.
$nextIndex = null;
} elseif ($maxCount !== null) {
$descriptor->repeated = $maxCount;
// If there's another field value after this repeated field, then a serialized message will
// contain placeholders (i.e. empty field values separated by possibly hundreds of `+`) to fill
// up to the repeated field's maximum length, after which the next message continues at the next
// index.
$nextIndex += $maxCount;
} else {
throw new \InvalidArgumentException(
"Repeated property $property needs @Max(.) or (rarely) @Unlimited annotation"
);
}
} elseif ($maxCount !== null) {
throw new \InvalidArgumentException("@Max() annotation not recognized on single $property");
} elseif ($unlimitedCount) {
throw new \InvalidArgumentException("@Unlimited annotation not recognized on single $property");
} else {
++$nextIndex; // Singular field, so the index advances by 1.
}
Expand Down Expand Up @@ -90,7 +105,7 @@ protected function __construct(\ReflectionClass $clazz)
throw new \InvalidArgumentException("No fields found in $clazz->name");
}
ksort($this->elements); // Make sure elements are parsed in wire-format order.
$this->maxIndex = $nextIndex - 1;
$this->maxIndex = $nextIndex === null ? PHP_INT_MAX : $nextIndex - 1;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ class ParameterNamensabgleichPruefauftragV1 extends BaseDeg

public string $unterstuetztePaymentStatusReportDatenformate;

/** @var string[] @Max(999999) Max length each: 6 */
/** @var string[] @Unlimited Max length each: 6 */
public array $vopPflichtigerZahlungsverkehrsauftrag;
}
19 changes: 17 additions & 2 deletions lib/Fhp/Syntax/Serializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,22 @@ private static function serializeElements($obj, BaseDescriptor $descriptor): arr
throw new \InvalidArgumentException(
"Expected array value for $descriptor->class.$elementDescriptor->field, got: $value");
}
for ($repetition = 0; $repetition < $elementDescriptor->repeated; ++$repetition) {
if ($elementDescriptor->repeated === PHP_INT_MAX) {
// For an uncapped repeated field (with @Unlimited), it must be the very last field and we do not
// need to insert padding elements, so we only output its actual contents.
if ($index !== $lastKey) {
throw new \AssertionError(
"Expected unlimited field at $index to be the last one, but the last one is $lastKey"
);
}
$numOutputElements = count($value);
} else {
// For a capped repeated field (with @Max), we need to output the specified number of elements, such
// that subsequent fields will be at the right place. If this is the last field, the trailing empty
// elements will be trimmed away again by flattenAndTrimEnd() later.
$numOutputElements = $elementDescriptor->repeated;
}
for ($repetition = 0; $repetition < $numOutputElements; ++$repetition) {
$serializedElements[$index + $repetition] = static::serializeElement(
$value === null || $repetition >= count($value) ? null : $value[$repetition],
$elementDescriptor->type, $isSegment);
Expand All @@ -129,7 +144,7 @@ private static function serializeElements($obj, BaseDescriptor $descriptor): arr
*/
private static function serializeElement($value, $type, bool $fullySerialize)
{
if (is_string($type)) {
if (is_string($type)) { // Scalar value / DE
return static::serializeDataElement($value, $type);
} elseif ($type->getName() === Bin::class) {
/* @var Bin|null $value */
Expand Down
44 changes: 44 additions & 0 deletions lib/Tests/Fhp/Segment/HIVPPSTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace Fhp\Segment;

use Fhp\Segment\VPP\HIVPPSv1;
use Fhp\Segment\VPP\ParameterNamensabgleichPruefauftragV1;
use Fhp\Syntax\Parser;
use PHPUnit\Framework\TestCase;

/**
* Among other things, this test covers the serialization of arrays with @Max annotation.
*/
class HIVPPSTest extends TestCase
{
private HIVPPSv1 $hivpps;

public function setUp(): void
{
$this->hivpps = HIVPPSv1::createEmpty();
$this->hivpps->setSegmentNumber(42);
$this->hivpps->maximaleAnzahlAuftraege = 43;
$this->hivpps->anzahlSignaturenMindestens = 44;
$this->hivpps->sicherheitsklasse = 45;
$this->hivpps->parameter = new ParameterNamensabgleichPruefauftragV1();
$this->hivpps->parameter->maximaleAnzahlCreditTransferTransactionInformationOptIn = 1;
$this->hivpps->parameter->aufklaerungstextStrukturiert = true;
$this->hivpps->parameter->artDerLieferungPaymentStatusReport = 'Art';
$this->hivpps->parameter->sammelzahlungenMitEinemAuftragErlaubt = false;
$this->hivpps->parameter->eingabeAnzahlEintraegeErlaubt = false;
$this->hivpps->parameter->unterstuetztePaymentStatusReportDatenformate = 'Test';
}

public function testPopulatedArray()
{
$this->hivpps->parameter->vopPflichtigerZahlungsverkehrsauftrag = ['HKFOO', 'HKBAR'];

$serialized = $this->hivpps->serialize();
$this->assertEquals("HIVPPS:42:1+43+44+45+1:J:Art:N:N:Test:HKFOO:HKBAR'", $serialized);

/** @var HIVPPSv1 $parsed */
$parsed = Parser::parseSegment($serialized, HIVPPSv1::class);
$this->assertEquals(['HKFOO', 'HKBAR'], $parsed->parameter->vopPflichtigerZahlungsverkehrsauftrag);
}
}
28 changes: 28 additions & 0 deletions lib/Tests/Fhp/Segment/HKVPPTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Fhp\Segment;

use Fhp\Segment\VPP\HKVPPv1;
use Fhp\Syntax\Parser;
use PHPUnit\Framework\TestCase;

/**
* Among other things, this test covers the serialization of arrays with @Max annotation.
*/
class HKVPPTest extends TestCase
{
public function testSerialize()
{
$hkvpp = HKVPPv1::createEmpty();
$hkvpp->setSegmentNumber(42);
$hkvpp->unterstuetztePaymentStatusReports->paymentStatusReportDescriptor = ['A', 'B', 'C'];

$serialized = $hkvpp->serialize();
$this->assertEquals("HKVPP:42:1+A:B:C'", $serialized);

/** @var HKVPPv1 $hkvpp */
$hkvpp = Parser::parseSegment($serialized, HKVPPv1::class);
$this->assertEquals(42, $hkvpp->getSegmentNumber());
$this->assertEquals(['A', 'B', 'C'], $hkvpp->unterstuetztePaymentStatusReports->paymentStatusReportDescriptor);
}
}