diff --git a/src/File/Validation/ChoiceMaxOccursRule.php b/src/File/Validation/ChoiceMaxOccursRule.php
index 0e7f6156..7b60481b 100644
--- a/src/File/Validation/ChoiceMaxOccursRule.php
+++ b/src/File/Validation/ChoiceMaxOccursRule.php
@@ -2,10 +2,35 @@
namespace WsdlToPhp\PackageGenerator\File\Validation;
+/**
+ * The choice's maxOccurs attribute bounds the number of times the choice group may repeat,
+ * not the number of times the chosen element may occur within the group. When the element
+ * itself may occur more than once (its own maxOccurs is greater than 1 or unbounded), the
+ * element's occurrences count is constrained by its own maxOccurs rule, and applying the
+ * choice's bound to the element's items count would wrongly reject valid contents.
+ *
+ * @see https://github.com/WsdlToPhp/PackageGenerator/issues/340
+ */
class ChoiceMaxOccursRule extends MaxOccursRule
{
public function name(): string
{
return 'choiceMaxOccurs';
}
+
+ public function testConditions(string $parameterName, $value, bool $itemType = false): string
+ {
+ $elementMaxOccurs = $this->getAttribute()->getMetaValueFirstSet([
+ 'maxOccurs',
+ 'maxoccurs',
+ 'MaxOccurs',
+ 'Maxoccurs',
+ ], 1);
+
+ if ('unbounded' === $elementMaxOccurs || 1 < (int) $elementMaxOccurs) {
+ return '';
+ }
+
+ return parent::testConditions($parameterName, $value, $itemType);
+ }
}
diff --git a/src/File/Validation/MaxOccursRule.php b/src/File/Validation/MaxOccursRule.php
index 9752dfd8..d5a0fb5a 100644
--- a/src/File/Validation/MaxOccursRule.php
+++ b/src/File/Validation/MaxOccursRule.php
@@ -45,7 +45,7 @@ public function symbol(): string
*
* @param mixed $value
*/
- final public function testConditions(string $parameterName, $value, bool $itemType = false): string
+ public function testConditions(string $parameterName, $value, bool $itemType = false): string
{
$test = '';
if ($this->getAttribute()->isArray() && ((is_scalar($value) && 'unbounded' !== $value) || (is_array($value) && !in_array('unbounded', $value)))) {
diff --git a/tests/File/StructTest.php b/tests/File/StructTest.php
index 5703bc10..770cb0eb 100755
--- a/tests/File/StructTest.php
+++ b/tests/File/StructTest.php
@@ -510,6 +510,27 @@ public function testStructValueListTypeFromUnitTests(): void
}
}
+ /**
+ * The choice elements have their own maxOccurs="5", the choice's maxOccurs (1) must
+ * not be applied to the elements' items count.
+ *
+ * @see https://github.com/WsdlToPhp/PackageGenerator/issues/340
+ */
+ public function testStructItemsChoiceTypeFromUnitTests(): void
+ {
+ $generator = self::unitTestsInstance();
+ if (($model = $generator->getStructByName('ItemsChoiceType')) instanceof StructModel) {
+ $struct = new StructFile($generator, $model->getName());
+ $struct
+ ->setModel($model)
+ ->write()
+ ;
+ $this->assertSameFileContent('ValidUnitTestsStructItemsChoiceType', $struct);
+ } else {
+ $this->fail('Unable to find ItemsChoiceType struct for file generation');
+ }
+ }
+
public function testWriteDeliveryDetails(): void
{
$generator = self::deliveryServiceInstance();
diff --git a/tests/File/Validation/AbstractRule.php b/tests/File/Validation/AbstractRule.php
index ffd69272..f85e58fa 100644
--- a/tests/File/Validation/AbstractRule.php
+++ b/tests/File/Validation/AbstractRule.php
@@ -103,6 +103,11 @@ public static function getOrderContractAddressDeliveryTypeInstance(bool $reset =
return self::getClassInstance('orderContractInstance', 'AddressDelivery_Type', $reset);
}
+ public static function getUnitTestsItemsChoiceTypeInstance(bool $reset = false)
+ {
+ return self::getClassInstance('unitTestsInstance', 'ItemsChoiceType', $reset);
+ }
+
public static function getEwsWorkingPeriodInstance(bool $reset = false)
{
// required for validating enumeration values
diff --git a/tests/File/Validation/ChoiceMaxOccursRuleTest.php b/tests/File/Validation/ChoiceMaxOccursRuleTest.php
new file mode 100644
index 00000000..b0b16b63
--- /dev/null
+++ b/tests/File/Validation/ChoiceMaxOccursRuleTest.php
@@ -0,0 +1,91 @@
+assertSame($instance, $instance->setItemIdentifier([1, 2, 3, 4, 5]));
+ }
+
+ /**
+ * The ItemIdentifier
+ * Meta information extracted from the WSDL
+ * - choice: ItemIdentifier | ItemName
+ * - choiceMaxOccurs: 1
+ * - choiceMinOccurs: 1
+ * - maxOccurs: 5
+ * - minOccurs: 1.
+ */
+ public function testAddToItemIdentifierWithSeveralItemsMustPass(): void
+ {
+ $instance = self::getUnitTestsItemsChoiceTypeInstance(true);
+
+ $this->assertSame($instance, $instance->setItemIdentifier([1])->addToItemIdentifier(2)->addToItemIdentifier(3));
+ }
+
+ /**
+ * The ItemIdentifier
+ * Meta information extracted from the WSDL
+ * - choice: ItemIdentifier | ItemName
+ * - choiceMaxOccurs: 1
+ * - choiceMinOccurs: 1
+ * - maxOccurs: 5
+ * - minOccurs: 1.
+ */
+ public function testSetItemIdentifierWithTooManyItemsMustThrowAnException(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Invalid count of 6, the number of elements contained by the property must be less than or equal to 5');
+
+ $instance = self::getUnitTestsItemsChoiceTypeInstance(true);
+
+ $instance->setItemIdentifier([1, 2, 3, 4, 5, 6]);
+ }
+
+ /**
+ * The ItemName
+ * Meta information extracted from the WSDL
+ * - choice: ItemIdentifier | ItemName
+ * - choiceMaxOccurs: 1
+ * - choiceMinOccurs: 1
+ * - maxOccurs: 5
+ * - minOccurs: 1.
+ */
+ public function testSetItemNameAfterItemIdentifierMustThrowAnException(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('The property ItemName can\'t be set as the property ItemIdentifier is already set. Only one property must be set among these properties: ItemName, ItemIdentifier.');
+
+ $instance = self::getUnitTestsItemsChoiceTypeInstance(true);
+
+ $instance
+ ->setItemIdentifier([1, 2])
+ ->setItemName(['one', 'two'])
+ ;
+ }
+}
diff --git a/tests/resources/generated/ValidUnitTestsStructItemsChoiceType.php b/tests/resources/generated/ValidUnitTestsStructItemsChoiceType.php
new file mode 100644
index 00000000..e41ff05c
--- /dev/null
+++ b/tests/resources/generated/ValidUnitTestsStructItemsChoiceType.php
@@ -0,0 +1,350 @@
+setItemIdentifier($itemIdentifier)
+ ->setItemName($itemName);
+ }
+ /**
+ * Get ItemIdentifier value
+ * @return int[]|null
+ */
+ public function getItemIdentifier(): ?array
+ {
+ return $this->ItemIdentifier ?? null;
+ }
+ /**
+ * This method is responsible for validating the value(s) passed to the setItemIdentifier method
+ * This method is willingly generated in order to preserve the one-line inline validation within the setItemIdentifier method
+ * This has to validate that each item contained by the array match the itemType constraint
+ * @param array $values
+ * @return string A non-empty message if the values does not match the validation rules
+ */
+ public static function validateItemIdentifierForArrayConstraintFromSetItemIdentifier(?array $values = []): string
+ {
+ if (!is_array($values)) {
+ return '';
+ }
+ $message = '';
+ $invalidValues = [];
+ foreach ($values as $itemsChoiceTypeItemIdentifierItem) {
+ // validation for constraint: itemType
+ if (!(is_int($itemsChoiceTypeItemIdentifierItem) || ctype_digit($itemsChoiceTypeItemIdentifierItem))) {
+ $invalidValues[] = is_object($itemsChoiceTypeItemIdentifierItem) ? get_class($itemsChoiceTypeItemIdentifierItem) : sprintf('%s(%s)', gettype($itemsChoiceTypeItemIdentifierItem), var_export($itemsChoiceTypeItemIdentifierItem, true));
+ }
+ }
+ if (!empty($invalidValues)) {
+ $message = sprintf('The ItemIdentifier property can only contain items of type int, %s given', is_object($invalidValues) ? get_class($invalidValues) : (is_array($invalidValues) ? implode(', ', $invalidValues) : gettype($invalidValues)));
+ }
+ unset($invalidValues);
+
+ return $message;
+ }
+ /**
+ * This method is responsible for validating the value(s) passed to the setItemIdentifier method
+ * This method is willingly generated in order to preserve the one-line inline validation within the setItemIdentifier method
+ * This has to validate that the property which is being set is the only one among the given choices
+ * @param mixed $value
+ * @return string A non-empty message if the values does not match the validation rules
+ */
+ public function validateItemIdentifierForChoiceConstraintFromSetItemIdentifier($value): string
+ {
+ $message = '';
+ if (is_null($value)) {
+ return $message;
+ }
+ $properties = [
+ 'ItemName',
+ ];
+ try {
+ foreach ($properties as $property) {
+ if (isset($this->{$property})) {
+ throw new InvalidArgumentException(sprintf('The property ItemIdentifier can\'t be set as the property %s is already set. Only one property must be set among these properties: ItemIdentifier, %s.', $property, implode(', ', $properties)), __LINE__);
+ }
+ }
+ } catch (InvalidArgumentException $e) {
+ $message = $e->getMessage();
+ }
+
+ return $message;
+ }
+ /**
+ * Set ItemIdentifier value
+ * This property belongs to a choice that allows only one property to exist. It is
+ * therefore removable from the request, consequently if the value assigned to this
+ * property is null, the property is removed from this object
+ * @throws InvalidArgumentException
+ * @throws InvalidArgumentException
+ * @param int[] $itemIdentifier
+ * @return \StructType\ApiItemsChoiceType
+ */
+ public function setItemIdentifier(?array $itemIdentifier = null): self
+ {
+ // validation for constraint: array
+ if ('' !== ($itemIdentifierArrayErrorMessage = self::validateItemIdentifierForArrayConstraintFromSetItemIdentifier($itemIdentifier))) {
+ throw new InvalidArgumentException($itemIdentifierArrayErrorMessage, __LINE__);
+ }
+ // validation for constraint: choice(ItemIdentifier, ItemName)
+ if ('' !== ($itemIdentifierChoiceErrorMessage = self::validateItemIdentifierForChoiceConstraintFromSetItemIdentifier($itemIdentifier))) {
+ throw new InvalidArgumentException($itemIdentifierChoiceErrorMessage, __LINE__);
+ }
+ // validation for constraint: maxOccurs(5)
+ if (is_array($itemIdentifier) && count($itemIdentifier) > 5) {
+ throw new InvalidArgumentException(sprintf('Invalid count of %s, the number of elements contained by the property must be less than or equal to 5', count($itemIdentifier)), __LINE__);
+ }
+ if (is_null($itemIdentifier) || (is_array($itemIdentifier) && empty($itemIdentifier))) {
+ unset($this->ItemIdentifier);
+ } else {
+ $this->ItemIdentifier = $itemIdentifier;
+ }
+
+ return $this;
+ }
+ /**
+ * This method is responsible for validating the value(s) passed to the addToItemIdentifier method
+ * This method is willingly generated in order to preserve the one-line inline validation within the addToItemIdentifier method
+ * This has to validate that the property which is being set is the only one among the given choices
+ * @param mixed $value
+ * @return string A non-empty message if the values does not match the validation rules
+ */
+ public function validateItemForChoiceConstraintFromAddToItemIdentifier($value): string
+ {
+ $message = '';
+ if (is_null($value)) {
+ return $message;
+ }
+ $properties = [
+ 'ItemName',
+ ];
+ try {
+ foreach ($properties as $property) {
+ if (isset($this->{$property})) {
+ throw new InvalidArgumentException(sprintf('The property ItemIdentifier can\'t be set as the property %s is already set. Only one property must be set among these properties: ItemIdentifier, %s.', $property, implode(', ', $properties)), __LINE__);
+ }
+ }
+ } catch (InvalidArgumentException $e) {
+ $message = $e->getMessage();
+ }
+
+ return $message;
+ }
+ /**
+ * Add item to ItemIdentifier value
+ * @throws InvalidArgumentException
+ * @param int $item
+ * @return \StructType\ApiItemsChoiceType
+ */
+ public function addToItemIdentifier(int $item): self
+ {
+ // validation for constraint: itemType
+ if (!(is_int($item) || ctype_digit($item))) {
+ throw new InvalidArgumentException(sprintf('The ItemIdentifier property can only contain items of type int, %s given', is_object($item) ? get_class($item) : (is_array($item) ? implode(', ', $item) : gettype($item))), __LINE__);
+ }
+ // validation for constraint: choice(ItemIdentifier, ItemName)
+ if ('' !== ($itemChoiceErrorMessage = self::validateItemForChoiceConstraintFromAddToItemIdentifier($item))) {
+ throw new InvalidArgumentException($itemChoiceErrorMessage, __LINE__);
+ }
+ // validation for constraint: maxOccurs(5)
+ if (is_array($this->ItemIdentifier) && count($this->ItemIdentifier) >= 5) {
+ throw new InvalidArgumentException(sprintf('You can\'t add anymore element to this property that already contains %s elements, the number of elements contained by the property must be less than or equal to 5', count($this->ItemIdentifier)), __LINE__);
+ }
+ $this->ItemIdentifier[] = $item;
+
+ return $this;
+ }
+ /**
+ * Get ItemName value
+ * @return string[]|null
+ */
+ public function getItemName(): ?array
+ {
+ return $this->ItemName ?? null;
+ }
+ /**
+ * This method is responsible for validating the value(s) passed to the setItemName method
+ * This method is willingly generated in order to preserve the one-line inline validation within the setItemName method
+ * This has to validate that each item contained by the array match the itemType constraint
+ * @param array $values
+ * @return string A non-empty message if the values does not match the validation rules
+ */
+ public static function validateItemNameForArrayConstraintFromSetItemName(?array $values = []): string
+ {
+ if (!is_array($values)) {
+ return '';
+ }
+ $message = '';
+ $invalidValues = [];
+ foreach ($values as $itemsChoiceTypeItemNameItem) {
+ // validation for constraint: itemType
+ if (!is_string($itemsChoiceTypeItemNameItem)) {
+ $invalidValues[] = is_object($itemsChoiceTypeItemNameItem) ? get_class($itemsChoiceTypeItemNameItem) : sprintf('%s(%s)', gettype($itemsChoiceTypeItemNameItem), var_export($itemsChoiceTypeItemNameItem, true));
+ }
+ }
+ if (!empty($invalidValues)) {
+ $message = sprintf('The ItemName property can only contain items of type string, %s given', is_object($invalidValues) ? get_class($invalidValues) : (is_array($invalidValues) ? implode(', ', $invalidValues) : gettype($invalidValues)));
+ }
+ unset($invalidValues);
+
+ return $message;
+ }
+ /**
+ * This method is responsible for validating the value(s) passed to the setItemName method
+ * This method is willingly generated in order to preserve the one-line inline validation within the setItemName method
+ * This has to validate that the property which is being set is the only one among the given choices
+ * @param mixed $value
+ * @return string A non-empty message if the values does not match the validation rules
+ */
+ public function validateItemNameForChoiceConstraintFromSetItemName($value): string
+ {
+ $message = '';
+ if (is_null($value)) {
+ return $message;
+ }
+ $properties = [
+ 'ItemIdentifier',
+ ];
+ try {
+ foreach ($properties as $property) {
+ if (isset($this->{$property})) {
+ throw new InvalidArgumentException(sprintf('The property ItemName can\'t be set as the property %s is already set. Only one property must be set among these properties: ItemName, %s.', $property, implode(', ', $properties)), __LINE__);
+ }
+ }
+ } catch (InvalidArgumentException $e) {
+ $message = $e->getMessage();
+ }
+
+ return $message;
+ }
+ /**
+ * Set ItemName value
+ * This property belongs to a choice that allows only one property to exist. It is
+ * therefore removable from the request, consequently if the value assigned to this
+ * property is null, the property is removed from this object
+ * @throws InvalidArgumentException
+ * @throws InvalidArgumentException
+ * @param string[] $itemName
+ * @return \StructType\ApiItemsChoiceType
+ */
+ public function setItemName(?array $itemName = null): self
+ {
+ // validation for constraint: array
+ if ('' !== ($itemNameArrayErrorMessage = self::validateItemNameForArrayConstraintFromSetItemName($itemName))) {
+ throw new InvalidArgumentException($itemNameArrayErrorMessage, __LINE__);
+ }
+ // validation for constraint: choice(ItemIdentifier, ItemName)
+ if ('' !== ($itemNameChoiceErrorMessage = self::validateItemNameForChoiceConstraintFromSetItemName($itemName))) {
+ throw new InvalidArgumentException($itemNameChoiceErrorMessage, __LINE__);
+ }
+ // validation for constraint: maxOccurs(5)
+ if (is_array($itemName) && count($itemName) > 5) {
+ throw new InvalidArgumentException(sprintf('Invalid count of %s, the number of elements contained by the property must be less than or equal to 5', count($itemName)), __LINE__);
+ }
+ if (is_null($itemName) || (is_array($itemName) && empty($itemName))) {
+ unset($this->ItemName);
+ } else {
+ $this->ItemName = $itemName;
+ }
+
+ return $this;
+ }
+ /**
+ * This method is responsible for validating the value(s) passed to the addToItemName method
+ * This method is willingly generated in order to preserve the one-line inline validation within the addToItemName method
+ * This has to validate that the property which is being set is the only one among the given choices
+ * @param mixed $value
+ * @return string A non-empty message if the values does not match the validation rules
+ */
+ public function validateItemForChoiceConstraintFromAddToItemName($value): string
+ {
+ $message = '';
+ if (is_null($value)) {
+ return $message;
+ }
+ $properties = [
+ 'ItemIdentifier',
+ ];
+ try {
+ foreach ($properties as $property) {
+ if (isset($this->{$property})) {
+ throw new InvalidArgumentException(sprintf('The property ItemName can\'t be set as the property %s is already set. Only one property must be set among these properties: ItemName, %s.', $property, implode(', ', $properties)), __LINE__);
+ }
+ }
+ } catch (InvalidArgumentException $e) {
+ $message = $e->getMessage();
+ }
+
+ return $message;
+ }
+ /**
+ * Add item to ItemName value
+ * @throws InvalidArgumentException
+ * @param string $item
+ * @return \StructType\ApiItemsChoiceType
+ */
+ public function addToItemName(string $item): self
+ {
+ // validation for constraint: itemType
+ if (!is_string($item)) {
+ throw new InvalidArgumentException(sprintf('The ItemName property can only contain items of type string, %s given', is_object($item) ? get_class($item) : (is_array($item) ? implode(', ', $item) : gettype($item))), __LINE__);
+ }
+ // validation for constraint: choice(ItemIdentifier, ItemName)
+ if ('' !== ($itemChoiceErrorMessage = self::validateItemForChoiceConstraintFromAddToItemName($item))) {
+ throw new InvalidArgumentException($itemChoiceErrorMessage, __LINE__);
+ }
+ // validation for constraint: maxOccurs(5)
+ if (is_array($this->ItemName) && count($this->ItemName) >= 5) {
+ throw new InvalidArgumentException(sprintf('You can\'t add anymore element to this property that already contains %s elements, the number of elements contained by the property must be less than or equal to 5', count($this->ItemName)), __LINE__);
+ }
+ $this->ItemName[] = $item;
+
+ return $this;
+ }
+}
diff --git a/tests/resources/generated/parsed_unit_tests_none.json b/tests/resources/generated/parsed_unit_tests_none.json
index 358d7685..8ee236ec 100644
--- a/tests/resources/generated/parsed_unit_tests_none.json
+++ b/tests/resources/generated/parsed_unit_tests_none.json
@@ -283,6 +283,60 @@
},
"name": "UserType",
"__CLASS__": "WsdlToPhp\\PackageGenerator\\Model\\Struct"
+ },
+ {
+ "attributes": [
+ {
+ "containsElements": true,
+ "removableFromRequest": false,
+ "type": "int",
+ "inheritance": "",
+ "abstract": false,
+ "meta": {
+ "choice": [
+ "ItemIdentifier",
+ "ItemName"
+ ],
+ "choiceMaxOccurs": 1,
+ "choiceMinOccurs": 1,
+ "maxOccurs": "5",
+ "minOccurs": "1",
+ "ref": "tns:ItemIdentifier"
+ },
+ "name": "ItemIdentifier",
+ "__CLASS__": "WsdlToPhp\\PackageGenerator\\Model\\StructAttribute"
+ },
+ {
+ "containsElements": true,
+ "removableFromRequest": false,
+ "type": "string",
+ "inheritance": "",
+ "abstract": false,
+ "meta": {
+ "choice": [
+ "ItemIdentifier",
+ "ItemName"
+ ],
+ "choiceMaxOccurs": 1,
+ "choiceMinOccurs": 1,
+ "maxOccurs": "5",
+ "minOccurs": "1",
+ "ref": "tns:ItemName"
+ },
+ "name": "ItemName",
+ "__CLASS__": "WsdlToPhp\\PackageGenerator\\Model\\StructAttribute"
+ }
+ ],
+ "restriction": false,
+ "struct": true,
+ "types": [],
+ "values": [],
+ "list": "",
+ "inheritance": "",
+ "abstract": false,
+ "meta": [],
+ "name": "ItemsChoiceType",
+ "__CLASS__": "WsdlToPhp\\PackageGenerator\\Model\\Struct"
}
]
},
diff --git a/tests/resources/generated/parsed_unit_tests_start.json b/tests/resources/generated/parsed_unit_tests_start.json
index 4a0d7663..102b1247 100644
--- a/tests/resources/generated/parsed_unit_tests_start.json
+++ b/tests/resources/generated/parsed_unit_tests_start.json
@@ -283,6 +283,60 @@
},
"name": "UserType",
"__CLASS__": "WsdlToPhp\\PackageGenerator\\Model\\Struct"
+ },
+ {
+ "attributes": [
+ {
+ "containsElements": true,
+ "removableFromRequest": false,
+ "type": "int",
+ "inheritance": "",
+ "abstract": false,
+ "meta": {
+ "choice": [
+ "ItemIdentifier",
+ "ItemName"
+ ],
+ "choiceMaxOccurs": 1,
+ "choiceMinOccurs": 1,
+ "maxOccurs": "5",
+ "minOccurs": "1",
+ "ref": "tns:ItemIdentifier"
+ },
+ "name": "ItemIdentifier",
+ "__CLASS__": "WsdlToPhp\\PackageGenerator\\Model\\StructAttribute"
+ },
+ {
+ "containsElements": true,
+ "removableFromRequest": false,
+ "type": "string",
+ "inheritance": "",
+ "abstract": false,
+ "meta": {
+ "choice": [
+ "ItemIdentifier",
+ "ItemName"
+ ],
+ "choiceMaxOccurs": 1,
+ "choiceMinOccurs": 1,
+ "maxOccurs": "5",
+ "minOccurs": "1",
+ "ref": "tns:ItemName"
+ },
+ "name": "ItemName",
+ "__CLASS__": "WsdlToPhp\\PackageGenerator\\Model\\StructAttribute"
+ }
+ ],
+ "restriction": false,
+ "struct": true,
+ "types": [],
+ "values": [],
+ "list": "",
+ "inheritance": "",
+ "abstract": false,
+ "meta": [],
+ "name": "ItemsChoiceType",
+ "__CLASS__": "WsdlToPhp\\PackageGenerator\\Model\\Struct"
}
]
},
diff --git a/tests/resources/unit_tests.wsdl b/tests/resources/unit_tests.wsdl
index 59a74290..95f2a68c 100644
--- a/tests/resources/unit_tests.wsdl
+++ b/tests/resources/unit_tests.wsdl
@@ -62,6 +62,16 @@
+
+
+
+
+
+
+
+
+
+