From e2b8b1e523015d6839ba065cf34a3d50461085cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=CC=8Ctekl?= Date: Mon, 27 Feb 2017 23:18:54 +0100 Subject: [PATCH 1/7] Implemented nullable association using empty --- src/Schematic/Entry.php | 21 ++++--- tests/SchematicTests/EntryTest.phpt | 92 +++++++++++++++++++++++++++-- tests/SchematicTests/entries.php | 10 ++-- 3 files changed, 107 insertions(+), 16 deletions(-) diff --git a/src/Schematic/Entry.php b/src/Schematic/Entry.php index 4e64974..660c735 100644 --- a/src/Schematic/Entry.php +++ b/src/Schematic/Entry.php @@ -11,6 +11,7 @@ class Entry const INDEX_ENTRYCLASS = 0; const INDEX_MULTIPLICITY = 1; const INDEX_EMBEDDING = 2; + const INDEX_NULLABLE = 3; /** * @var array @@ -64,18 +65,19 @@ private static function parseAssociations($class) foreach (static::$associations as $association => $entryClass) { $matches = []; - $result = preg_match('#^([^.[\]]+)(\.[^.[\]]*)?(\[\])?$#', $association, $matches); + $result = preg_match('#^(\?)?([^.[\]]+)(\.[^.[\]]*)?(\[\])?$#', $association, $matches); - if ($result === 0 || (!empty($matches[2]) && !empty($matches[3]))) { + if ($result === 0 || (!empty($matches[3]) && !empty($matches[4]))) { throw new InvalidArgumentException('Invalid association definition given: ' . $association); } - self::$parsedAssociations[$class][$matches[1]] = [ + self::$parsedAssociations[$class][$matches[2]] = [ self::INDEX_ENTRYCLASS => $entryClass, - self::INDEX_MULTIPLICITY => !empty($matches[3]), - self::INDEX_EMBEDDING => !empty($matches[2]) ? - ($matches[2] === '.' ? $matches[1] . '_' : substr($matches[2], 1)) : + self::INDEX_MULTIPLICITY => !empty($matches[4]), + self::INDEX_EMBEDDING => !empty($matches[3]) ? + ($matches[3] === '.' ? $matches[2] . '_' : substr($matches[3], 1)) : FALSE, + self::INDEX_NULLABLE => !empty($matches[1]), ]; } } @@ -101,7 +103,10 @@ public function __get($name) $this->readEmbeddedEntry($association[self::INDEX_EMBEDDING]) : $this->readData($name); - if ($data === NULL) { + if (!$association[self::INDEX_MULTIPLICITY] && ( + $data === NULL + || ($association[self::INDEX_NULLABLE] && empty($data)) + )) { return $this->data[$name] = NULL; } @@ -109,7 +114,7 @@ public function __get($name) $entriesClass = $this->entriesClass; return $this->data[$name] = $association[self::INDEX_MULTIPLICITY] ? - new $entriesClass($data, $entryClass) : + new $entriesClass((array) $data, $entryClass) : new $entryClass($data, $this->entriesClass); } diff --git a/tests/SchematicTests/EntryTest.phpt b/tests/SchematicTests/EntryTest.phpt index 6e59917..e972a3b 100644 --- a/tests/SchematicTests/EntryTest.phpt +++ b/tests/SchematicTests/EntryTest.phpt @@ -55,8 +55,35 @@ class EntryTest extends TestCase Assert::same(100, $order->customer->id); } + /** + * @dataProvider provideDataNullableRelation + */ + public function testEntriesAccessToNullableParameter($customer) + { + $order = new Order([ + 'customer' => $customer, + 'orderItems' => [], + ]); + Assert::null($order->customer); + } - public function testEntriesClass() + + public function provideDataNullableRelation() + { + return [ + [[]], + [false], + [null], + [0], + [0.0], + [''], + ]; + } + + /** + * @dataProvider provideDataNullableCollection + */ + public function testEntriesClass($lastCollectionValue) { $order = new Order([ 'orderItems' => [ @@ -71,6 +98,10 @@ class EntryTest extends TestCase 'id' => 2, 'tags' => [], ], + [ + 'id' => 3, + 'tags' => $lastCollectionValue, + ], ], ], CustomEntries::class); @@ -79,18 +110,37 @@ class EntryTest extends TestCase $orderItems = $order->orderItems->toArray(); /** @var OrderItem $firstOrderItem */ $firstOrderItem = reset($orderItems); + /** @var OrderItem $lastOrderItem */ + $lastOrderItem = end($orderItems); Assert::type(CustomEntries::class, $firstOrderItem->tags); + Assert::type(CustomEntries::class, $lastOrderItem->tags); } - public function testEmbeddedEntries() + public function provideDataNullableCollection() + { + return [ + [[]], + [false], + [null], + [0], + [0.0], + [''], + ]; + } + + /** + * @dataProvider provideDataNullableCustomer + */ + public function testEmbeddedEntries($customerId, $customerName) { $book = new Book([ 'id' => 12, 'title' => 'PHP: The Bad Parts', 'tag_name' => 'bestseller', - 'customer_id' => 20, + 'customer_id' => $customerId, + 'customer_name' => $customerName, 'a_firstname' => 'John', 'a_surname' => 'Doe', ]); @@ -99,7 +149,41 @@ class EntryTest extends TestCase Assert::same('bestseller', $book->tag->name); Assert::type(Customer::class, $book->customer); - Assert::same(20, $book->customer->id); + Assert::same($customerId, $book->customer->id); + Assert::same($customerName, $book->customer->name); + + Assert::type(Author::class, $book->author); + Assert::same('John', $book->author->firstname); + Assert::same('Doe', $book->author->surname); + } + + + public function provideDataNullableCustomer() + { + return [ + [20, 'Jack'], + [null, 'Jack'], + [20, null], + ]; + } + + + public function testNullableEmbeddedEntries() + { + $book = new Book([ + 'id' => 12, + 'title' => 'PHP: The Bad Parts', + 'tag_name' => 'bestseller', + 'customer_id' => null, + 'customer_name' => null, + 'a_firstname' => 'John', + 'a_surname' => 'Doe', + ]); + + Assert::type(Tag::class, $book->tag); + Assert::same('bestseller', $book->tag->name); + + Assert::null($book->customer); Assert::type(Author::class, $book->author); Assert::same('John', $book->author->firstname); diff --git a/tests/SchematicTests/entries.php b/tests/SchematicTests/entries.php index 67f0936..e48ff82 100644 --- a/tests/SchematicTests/entries.php +++ b/tests/SchematicTests/entries.php @@ -26,7 +26,7 @@ class Order extends Identified { protected static $associations = [ - 'customer' => Customer::class, + '?customer' => Customer::class, 'orderItems[]' => OrderItem::class, ]; @@ -40,12 +40,14 @@ class OrderItem extends Identified { protected static $associations = [ - 'tags[]' => Tag::class, + '?tags[]' => Tag::class, ]; } - +/** + * @property-read string|null $name + */ class Customer extends Identified { @@ -88,7 +90,7 @@ class Book extends Identified protected static $associations = [ 'tag.' => Tag::class, - 'customer.' => Customer::class, + '?customer.' => Customer::class, 'author.a_' => Author::class, ]; From 8e5bea2e0d81e017c205cb6e0749ed7d8a14123e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=CC=8Ctekl?= Date: Fri, 3 Mar 2017 11:15:56 +0100 Subject: [PATCH 2/7] Added doc comment for nullable index in parsed associations --- src/Schematic/Entry.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Schematic/Entry.php b/src/Schematic/Entry.php index 660c735..1d4f740 100644 --- a/src/Schematic/Entry.php +++ b/src/Schematic/Entry.php @@ -19,7 +19,7 @@ class Entry protected static $associations = []; /** - * @var array entryClass => [INDEX_ENTRYCLASS => relatedEntryClass, INDEX_MULTIPLICITY => multiplicity, INDEX_EMBEDDING => embedding] + * @var array entryClass => [INDEX_ENTRYCLASS => relatedEntryClass, INDEX_MULTIPLICITY => multiplicity, INDEX_EMBEDDING => embedding, INDEX_NULLABLE => null value allowed] */ private static $parsedAssociations = []; From 634325bea851fc2dde82d1a83545cecd7d5bdfea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=CC=8Ctekl?= Date: Fri, 3 Mar 2017 11:16:12 +0100 Subject: [PATCH 3/7] Used one generic data provider instead of two --- tests/SchematicTests/EntryTest.phpt | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/tests/SchematicTests/EntryTest.phpt b/tests/SchematicTests/EntryTest.phpt index e972a3b..ba23554 100644 --- a/tests/SchematicTests/EntryTest.phpt +++ b/tests/SchematicTests/EntryTest.phpt @@ -56,7 +56,7 @@ class EntryTest extends TestCase } /** - * @dataProvider provideDataNullableRelation + * @dataProvider provideFalsyValues */ public function testEntriesAccessToNullableParameter($customer) { @@ -67,21 +67,8 @@ class EntryTest extends TestCase Assert::null($order->customer); } - - public function provideDataNullableRelation() - { - return [ - [[]], - [false], - [null], - [0], - [0.0], - [''], - ]; - } - /** - * @dataProvider provideDataNullableCollection + * @dataProvider provideFalsyValues */ public function testEntriesClass($lastCollectionValue) { @@ -118,7 +105,7 @@ class EntryTest extends TestCase } - public function provideDataNullableCollection() + public function provideFalsyValues() { return [ [[]], From e3bda86e71c7da94b5aa0482739af0470e7b66b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=CC=8Ctekl?= Date: Fri, 3 Mar 2017 11:18:08 +0100 Subject: [PATCH 4/7] Made nullable condition more readable --- src/Schematic/Entry.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Schematic/Entry.php b/src/Schematic/Entry.php index 1d4f740..2474fc6 100644 --- a/src/Schematic/Entry.php +++ b/src/Schematic/Entry.php @@ -103,10 +103,13 @@ public function __get($name) $this->readEmbeddedEntry($association[self::INDEX_EMBEDDING]) : $this->readData($name); - if (!$association[self::INDEX_MULTIPLICITY] && ( - $data === NULL - || ($association[self::INDEX_NULLABLE] && empty($data)) - )) { + if ( + !$association[self::INDEX_MULTIPLICITY] + && ( + $data === NULL + || ($association[self::INDEX_NULLABLE] && empty($data)) + ) + ) { return $this->data[$name] = NULL; } From 815d33d171c02f5ec456f29341fd031f8c810311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=CC=8Ctekl?= Date: Fri, 3 Mar 2017 11:21:04 +0100 Subject: [PATCH 5/7] Allowed overriding function for empty value detection --- src/Schematic/Entry.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Schematic/Entry.php b/src/Schematic/Entry.php index 2474fc6..18867ba 100644 --- a/src/Schematic/Entry.php +++ b/src/Schematic/Entry.php @@ -107,7 +107,7 @@ public function __get($name) !$association[self::INDEX_MULTIPLICITY] && ( $data === NULL - || ($association[self::INDEX_NULLABLE] && empty($data)) + || ($association[self::INDEX_NULLABLE] && static::isEmpty($data)) ) ) { return $this->data[$name] = NULL; @@ -138,6 +138,16 @@ public function __isset($name) } + /** + * @param mixed $value + * @return bool + */ + protected static function isEmpty($value) + { + return empty($value); + } + + /** * @param string $prefix * @return array|NULL From e1572c62fd89012522a8a407d05a59e4e2bd5bcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=CC=8Ctekl?= Date: Fri, 3 Mar 2017 12:24:18 +0100 Subject: [PATCH 6/7] Updated entry tests to validate all property combinations --- tests/SchematicTests/EntryTest.phpt | 306 +++++++++++++++++++++++++--- tests/SchematicTests/entries.php | 20 ++ 2 files changed, 302 insertions(+), 24 deletions(-) diff --git a/tests/SchematicTests/EntryTest.phpt b/tests/SchematicTests/EntryTest.phpt index ba23554..56becf8 100644 --- a/tests/SchematicTests/EntryTest.phpt +++ b/tests/SchematicTests/EntryTest.phpt @@ -8,6 +8,7 @@ use InvalidArgumentException; use Schematic\Entries; use Tester\Assert; use Tester\TestCase; +use TypeError; /** @@ -16,6 +17,282 @@ use Tester\TestCase; class EntryTest extends TestCase { + /** + * @dataProvider provideNotEmptyScalarValues + * @dataProvider provideEmptyScalarValues + * @dataProvider provideNullValue + * @dataProvider provideNotEmptyArray + * @dataProvider provideEmptyArray + * @dataProvider provideMinimalEntityData + */ + public function testGeneralProperty($value) + { + $entity = new UniversalProperties([ + 'value' => $value, + 'valueRequired' => [], + 'valuesRequired' => [], + ]); + + Assert::same($value, $entity->value); + } + + + /** + * @dataProvider provideNotEmptyArray + * @dataProvider provideEmptyArray + * @dataProvider provideMinimalEntityData + */ + public function testRequiredPropertyFromArray($value) + { + $entity = new UniversalProperties([ + 'value' => null, + 'valueRequired' => $value, + 'valuesRequired' => [], + ]); + + Assert::type(UniversalProperties::class, $entity->valueRequired); + } + + + /** + * @dataProvider provideNullValue + */ + public function testRequiredPropertyFromNull($value) + { + $entity = new UniversalProperties([ + 'value' => null, + 'valueRequired' => $value, + 'valuesRequired' => [], + ]); + + Assert::null($entity->valueRequired); + } + + + /** + * @dataProvider provideNotEmptyScalarValues + * @dataProvider provideEmptyScalarValues + */ + public function testRequiredPropertyFromScalar($value) + { + Assert::exception(function () use ($value) { + $entity = new UniversalProperties([ + 'value' => null, + 'valueRequired' => $value, + 'valuesRequired' => [], + ]); + $entity->valueRequired; + }, TypeError::class); + } + + + /** + * @dataProvider provideNotEmptyArray + * @dataProvider provideMinimalEntityData + */ + public function testNullablePropertyFromNotEmptyArray($value) + { + $entity = new UniversalProperties([ + 'value' => null, + 'valueRequired' => [], + 'valueNullable' => $value, + 'valuesRequired' => [], + ]); + + Assert::type(UniversalProperties::class, $entity->valueNullable); + } + + + /** + * @dataProvider provideNotEmptyScalarValues + */ + public function testNullablePropertyFromNotEmptyScalar($value) + { + Assert::exception(function () use ($value) { + $entity = new UniversalProperties([ + 'value' => null, + 'valueRequired' => [], + 'valueNullable' => $value, + 'valuesRequired' => [], + ]); + $entity->valueNullable; + }, TypeError::class); + } + + + /** + * @dataProvider provideEmptyScalarValues + * @dataProvider provideNullValue + * @dataProvider provideEmptyArray + */ + public function testNullablePropertyFromEmptyValue($value) + { + $entity = new UniversalProperties([ + 'value' => null, + 'valueRequired' => [], + 'valueNullable' => $value, + 'valuesRequired' => [], + ]); + + Assert::null($entity->valueNullable); + } + + + /** + * @dataProvider provideNotEmptyArray + * @dataProvider provideEmptyArray + * @dataProvider provideMinimalEntityData + */ + public function testRequiredCollectionFromArray($value) + { + $entity = new UniversalProperties([ + 'value' => null, + 'valueRequired' => [], + 'valuesRequired' => $value, + ]); + + Assert::type(Entries::class, $entity->valuesRequired); + } + + + /** + * @dataProvider provideNullValue + */ + public function testRequiredCollectionFromNull($value) + { + $entity = new UniversalProperties([ + 'value' => null, + 'valueRequired' => [], + 'valuesRequired' => $value, + ]); + + Assert::null($entity->valuesRequired); + } + + + /** + * @dataProvider provideNotEmptyScalarValues + * @dataProvider provideEmptyScalarValues + */ + public function testRequiredCollectionFromScalar($value) + { + Assert::exception(function () use ($value) { + $entity = new UniversalProperties([ + 'value' => null, + 'valueRequired' => [], + 'valuesRequired' => $value, + ]); + $entity->valuesRequired; + }, TypeError::class); + } + + + /** + * @dataProvider provideNotEmptyArray + * @dataProvider provideMinimalEntityData + */ + public function testNullableCollectionFromNotEmptyArray($value) + { + $entity = new UniversalProperties([ + 'value' => null, + 'valueRequired' => [], + 'valuesRequired' => [], + 'valuesNullable' => $value, + ]); + + Assert::type(Entries::class, $entity->valuesNullable); + } + + + /** + * @dataProvider provideNotEmptyScalarValues + */ + public function testNullableCollectionFromNotEmptyScalar($value) + { + Assert::exception(function () use ($value) { + $entity = new UniversalProperties([ + 'value' => null, + 'valueRequired' => [], + 'valuesRequired' => [], + 'valuesNullable' => $value, + ]); + $entity->valuesNullable; + }, TypeError::class); + } + + + /** + * @dataProvider provideEmptyScalarValues + * @dataProvider provideNullValue + * @dataProvider provideEmptyArray + */ + public function testNullableCollectionFromEmptyValue($value) + { + $entity = new UniversalProperties([ + 'value' => null, + 'valueRequired' => [], + 'valuesRequired' => [], + 'valuesNullable' => $value, + ]); + + Assert::null($entity->valuesNullable); + } + + + public function provideNotEmptyScalarValues() + { + return [ + ['dummy text'], + [10], + [1.5], + [true], + ]; + } + + + public function provideEmptyScalarValues() + { + return [ + [''], + [0], + [0.0], + [false], + ]; + } + + + public function provideNullValue() + { + return [ + [null], + ]; + } + + + public function provideNotEmptyArray() + { + return [ + [['value-01', 'value-02']], + ]; + } + + + public function provideEmptyArray() + { + return [ + [[]], + ]; + } + + + public function provideMinimalEntityData() + { + return [ + [['valueRequired' => 'value', 'valuesRequired' => []]], + ]; + } + + public function testFieldAccess() { $id = 1; @@ -56,7 +333,9 @@ class EntryTest extends TestCase } /** - * @dataProvider provideFalsyValues + * @dataProvider provideEmptyScalarValues + * @dataProvider provideNullValue + * @dataProvider provideEmptyArray */ public function testEntriesAccessToNullableParameter($customer) { @@ -67,10 +346,8 @@ class EntryTest extends TestCase Assert::null($order->customer); } - /** - * @dataProvider provideFalsyValues - */ - public function testEntriesClass($lastCollectionValue) + + public function testEntriesClass() { $order = new Order([ 'orderItems' => [ @@ -85,10 +362,6 @@ class EntryTest extends TestCase 'id' => 2, 'tags' => [], ], - [ - 'id' => 3, - 'tags' => $lastCollectionValue, - ], ], ], CustomEntries::class); @@ -97,26 +370,11 @@ class EntryTest extends TestCase $orderItems = $order->orderItems->toArray(); /** @var OrderItem $firstOrderItem */ $firstOrderItem = reset($orderItems); - /** @var OrderItem $lastOrderItem */ - $lastOrderItem = end($orderItems); Assert::type(CustomEntries::class, $firstOrderItem->tags); - Assert::type(CustomEntries::class, $lastOrderItem->tags); } - public function provideFalsyValues() - { - return [ - [[]], - [false], - [null], - [0], - [0.0], - [''], - ]; - } - /** * @dataProvider provideDataNullableCustomer */ diff --git a/tests/SchematicTests/entries.php b/tests/SchematicTests/entries.php index e48ff82..bcf03c8 100644 --- a/tests/SchematicTests/entries.php +++ b/tests/SchematicTests/entries.php @@ -95,3 +95,23 @@ class Book extends Identified ]; } + + +/** + * @property-read mixed $value + * @property-read UniversalProperties $valueRequired + * @property-read UniversalProperties|null $valueNullable + * @property-read UniversalProperties[] $valuesRequired + * @property-read UniversalProperties[]|null $valuesNullable + */ +class UniversalProperties extends Entry +{ + + protected static $associations = [ + 'valueRequired' => UniversalProperties::class, + '?valueNullable' => UniversalProperties::class, + 'valuesRequired[]' => UniversalProperties::class, + '?valuesNullable[]' => UniversalProperties::class, + ]; + +} From d8da1bb07eb4fc1c76e2618e9f3bf937c324bdfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=CC=8Ctekl?= Date: Fri, 3 Mar 2017 12:25:04 +0100 Subject: [PATCH 7/7] Fixed entry class to support nullable collections --- src/Schematic/Entry.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Schematic/Entry.php b/src/Schematic/Entry.php index 18867ba..b3e6469 100644 --- a/src/Schematic/Entry.php +++ b/src/Schematic/Entry.php @@ -103,13 +103,7 @@ public function __get($name) $this->readEmbeddedEntry($association[self::INDEX_EMBEDDING]) : $this->readData($name); - if ( - !$association[self::INDEX_MULTIPLICITY] - && ( - $data === NULL - || ($association[self::INDEX_NULLABLE] && static::isEmpty($data)) - ) - ) { + if ($data === NULL || ($association[self::INDEX_NULLABLE] && static::isEmpty($data))) { return $this->data[$name] = NULL; } @@ -117,7 +111,7 @@ public function __get($name) $entriesClass = $this->entriesClass; return $this->data[$name] = $association[self::INDEX_MULTIPLICITY] ? - new $entriesClass((array) $data, $entryClass) : + new $entriesClass($data, $entryClass) : new $entryClass($data, $this->entriesClass); }