diff --git a/Zend/tests/collection/collection_dict_basic.phpt b/Zend/tests/collection/collection_dict_basic.phpt new file mode 100644 index 0000000000000..5b7845e1ff833 --- /dev/null +++ b/Zend/tests/collection/collection_dict_basic.phpt @@ -0,0 +1,67 @@ +--TEST-- +Collection: Dictionary +--FILE-- + Article> +{ +} + +$c = new Articles; +$c["nine"] = new Article("First Test"); +$c["ten"] = new Article("Second Test"); +$c->add("seven", new Article("Third Test")); + +var_dump($c); + +var_dump(isset($c["eight"])); +var_dump(isset($c["nine"])); +var_dump(isset($c["ten"])); + +var_dump($c["nine"]); + +unset($c["nine"]); +var_dump(isset($c["nine"])); + +try { + var_dump(isset($c[11])); +} catch (Error $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +object(Articles)#%d (%d) { + ["value"]=> + array(3) { + ["nine"]=> + object(Article)#%d (%d) { + ["title"]=> + string(10) "First Test" + } + ["ten"]=> + object(Article)#%d (%d) { + ["title"]=> + string(11) "Second Test" + } + ["seven"]=> + object(Article)#%d (%d) { + ["title"]=> + string(10) "Third Test" + } + } +} +bool(false) +bool(true) +bool(true) +object(Article)#%d (%d) { + ["title"]=> + string(10) "First Test" +} +bool(false) +TypeError: Key type int of element does not match Articles dictionary key type string diff --git a/Zend/tests/collection/collection_dict_concat-errors.phpt b/Zend/tests/collection/collection_dict_concat-errors.phpt new file mode 100644 index 0000000000000..eaea477075e89 --- /dev/null +++ b/Zend/tests/collection/collection_dict_concat-errors.phpt @@ -0,0 +1,82 @@ +--TEST-- +Collection: Dictionary: Concat Errors +--FILE-- + Book> {} +collection(Dict) YBooks Book> {} +collection(Dict) DutchBooks DutchBook> {} +collection(Dict) Articles Article> {} + +$c1 = new Books(); +$c1->add("one", new Book('Title 1')); +$c1->add("two", new Book('Title 2')); + +$y1 = new YBooks(); +$y1->add(1970, new Book('Title 1')); +$y1->add(1971, new Book('Title 2')); + +$d1 = new DutchBooks(); +$d1->add("één", new DutchBook('Titel 1')); +$d1->add("twee", new DutchBook('Titel 2')); + +$a2 = new Articles(); +$a2->add("three", new Article('Subject 1')); +$a2->add("four", new Article('Subject 2')); + +try { + $c1->concat(new Book('Title E')); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $c1->concat($a2); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $cr = $c1->concat($d1); + echo "Inheritence: OK\n"; +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $cr = $d1->concat($c1); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $why = $c1->concat($y1); + var_dump( $why ); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $why = $y1->concat($c1); + var_dump( $why ); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +TypeError: Books::concat(): Argument #1 ($other) must be of type DictCollection, Book given +TypeError: Value type Books does not match Article collection item type Book +Inheritence: OK +TypeError: Value type DutchBooks does not match Book collection item type DutchBook +TypeError: Key type int of element does not match Books dictionary key type string +TypeError: Key type string of element does not match YBooks dictionary key type int diff --git a/Zend/tests/collection/collection_dict_concat_idx.phpt b/Zend/tests/collection/collection_dict_concat_idx.phpt new file mode 100644 index 0000000000000..cc131fa4d57eb --- /dev/null +++ b/Zend/tests/collection/collection_dict_concat_idx.phpt @@ -0,0 +1,70 @@ +--TEST-- +Collection: Dictionary: Concat (int key) +--FILE-- + Book> {} + +$c1 = new Books(); +$c2 = new Books(); + +$c1->add(41, new Book('Title 1')); +$c1->add(42, new Book('Title 2')); + +$c2->add(43, new Book('Title 3')); +$c2->add(44, new Book('Title 4')); + +$c3 = $c1->concat($c2); + +// Four items. +var_dump($c3); + +// Still two items. +var_dump($c1); + +?> +--EXPECTF-- +object(Books)#%d (1) { + ["value"]=> + array(4) { + [41]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 1" + } + [42]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 2" + } + [43]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 3" + } + [44]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 4" + } + } +} +object(Books)#%d (1) { + ["value"]=> + array(2) { + [41]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 1" + } + [42]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 2" + } + } +} diff --git a/Zend/tests/collection/collection_dict_concat_key.phpt b/Zend/tests/collection/collection_dict_concat_key.phpt new file mode 100644 index 0000000000000..d574223ee9584 --- /dev/null +++ b/Zend/tests/collection/collection_dict_concat_key.phpt @@ -0,0 +1,70 @@ +--TEST-- +Collection: Dictionary: Concat (string key) +--FILE-- + Book> {} + +$c1 = new Books(); +$c2 = new Books(); + +$c1->add('one', new Book('Title 1')); +$c1->add('two', new Book('Title 2')); + +$c2->add('three', new Book('Title 3')); +$c2->add('four', new Book('Title 4')); + +$c3 = $c1->concat($c2); + +// Four items. +var_dump($c3); + +// Still two items. +var_dump($c1); + +?> +--EXPECTF-- +object(Books)#%d (1) { + ["value"]=> + array(4) { + ["one"]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 1" + } + ["two"]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 2" + } + ["three"]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 3" + } + ["four"]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 4" + } + } +} +object(Books)#%d (1) { + ["value"]=> + array(2) { + ["one"]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 1" + } + ["two"]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 2" + } + } +} diff --git a/Zend/tests/collection/collection_dict_equality.phpt b/Zend/tests/collection/collection_dict_equality.phpt new file mode 100644 index 0000000000000..1f96771b57dab --- /dev/null +++ b/Zend/tests/collection/collection_dict_equality.phpt @@ -0,0 +1,95 @@ +--TEST-- +Collection: Dictionary: Equals +--FILE-- + Book> {} +collection(Dict) BooksOther Book> {} +collection(Dict) YearBooks Book> {} + +$c1 = new Books(); +$c1->add('one', new Book('Title 1')); +$c1->add('two', new Book('Title 2')); + +$c2 = new Books(); +$c2->add('one', new Book('Title 1')); +$c2->add('two', new Book('Title 2')); + +$c3 = new Books(); +$c3->add('one', new Book('Title 1')); +$c3->add('two', new Book('Title 2')); +$c3->add('tri', new Book('Title 3')); + +$c4 = new Books(); +$c4->add('one', new Book('Title 1')); +$c4->add('XXX', new Book('Title X')); +$c4->add('two', new Book('Title 2')); + +$c5 = new Books(); +$c5->add('one', new Book('Title 1')); +$c5->add('two', new Book('Title X')); +$c5->add('tri', new Book('Title 2')); + +$c6 = new Books(); +$c6->add('one', new Book('Title 1')); +$c6->add('XXX', new Book('Title 2')); +$c6->add('tri', new Book('Title 3')); + +$c7 = new BooksOther(); +$c7->add('one', new Book('Title 1')); +$c7->add('two', new Book('Title 2')); + +$c8 = new YearBooks(); +$c8->add(1970, new Book('Title 1')); +$c8->add(1971, new Book('Title 2')); + +$c9 = new Books(); +$c9->add('two', new Book('Title 2')); +$c9->add('one', new Book('Title 1')); + + +// True +var_dump($c1->equals($c1)); +var_dump($c1->equals($c2)); +var_dump($c1->equals($c7)); +var_dump($c1->equals($c9)); + +$c2['tri'] = new Book('Title 3'); + +var_dump($c2->equals($c3)); + +// False +var_dump($c1->equals($c2)); +var_dump($c1->equals($c8)); +var_dump($c3->equals($c4)); +var_dump($c3->equals($c5)); +var_dump($c3->equals($c6)); + +unset($c4['XXX']); + +// True +var_dump($c1->equals($c4)); + +unset($c6['XXX']); +$c6['two'] = new Book('Title 2'); + +// True +var_dump($c3->equals($c6)); +?> +--EXPECTF-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(true) +bool(true) diff --git a/Zend/tests/collection/collection_dict_equality_handler.phpt b/Zend/tests/collection/collection_dict_equality_handler.phpt new file mode 100644 index 0000000000000..ad27274da3506 --- /dev/null +++ b/Zend/tests/collection/collection_dict_equality_handler.phpt @@ -0,0 +1,94 @@ +--TEST-- +Collection: Dictionary: Comparison Handler +--FILE-- + Book> {} +collection(Dict) BooksOther Book> {} +collection(Dict) YearBooks Book> {} + +$c1 = new Books(); +$c1->add('one', new Book('Title 1')); +$c1->add('two', new Book('Title 2')); + +$c2 = new Books(); +$c2->add('one', new Book('Title 1')); +$c2->add('two', new Book('Title 2')); + +$c3 = new Books(); +$c3->add('one', new Book('Title 1')); +$c3->add('two', new Book('Title 2')); +$c3->add('tri', new Book('Title 3')); + +$c4 = new Books(); +$c4->add('one', new Book('Title 1')); +$c4->add('XXX', new Book('Title X')); +$c4->add('two', new Book('Title 2')); + +$c5 = new Books(); +$c5->add('one', new Book('Title 1')); +$c5->add('two', new Book('Title X')); +$c5->add('tri', new Book('Title 2')); + +$c6 = new Books(); +$c6->add('one', new Book('Title 1')); +$c6->add('XXX', new Book('Title 2')); +$c6->add('tri', new Book('Title 3')); + +$c7 = new BooksOther(); +$c7->add('one', new Book('Title 1')); +$c7->add('two', new Book('Title 2')); + +$c8 = new YearBooks(); +$c8->add(1970, new Book('Title 1')); +$c8->add(1971, new Book('Title 2')); + +$c9 = new Books(); +$c9->add('two', new Book('Title 2')); +$c9->add('one', new Book('Title 1')); + +// True +var_dump($c1 == $c1); +var_dump($c1 == $c2); +var_dump($c1 == $c7); +var_dump($c1 == $c9); + +$c2['tri'] = new Book('Title 3'); + +var_dump($c2 == $c3); + +// False +var_dump($c1 == $c2); +var_dump($c1 == $c8); +var_dump($c3 == $c4); +var_dump($c3 == $c5); +var_dump($c3 == $c6); + +unset($c4['XXX']); + +// True +var_dump($c1 == $c4); + +unset($c6['XXX']); +$c6['two'] = new Book('Title 2'); + +// True +var_dump($c3 == $c6); +?> +--EXPECTF-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(true) +bool(true) diff --git a/Zend/tests/collection/collection_dict_int_equality.phpt b/Zend/tests/collection/collection_dict_int_equality.phpt new file mode 100644 index 0000000000000..a2a9dd666e047 --- /dev/null +++ b/Zend/tests/collection/collection_dict_int_equality.phpt @@ -0,0 +1,36 @@ +--TEST-- +Collection: Dictionary/Sequence: Equals +--FILE-- + {} +collection(Dict) YearBooks Book> {} + +$s1 = new Books(); +$s1->add(new Book('Title 1')); +$s1->add(new Book('Title 2')); + +$d1 = new YearBooks(); +$d1->add(0, new Book('Title 1')); +$d1->add(1, new Book('Title 2')); + +// Exception +try { + var_dump($s1->equals($d1)); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + var_dump($d1->equals($s1)); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +TypeError: Books::equals(): Argument #1 ($other) must be of type SeqCollection, YearBooks given +TypeError: YearBooks::equals(): Argument #1 ($other) must be of type DictCollection, Books given diff --git a/Zend/tests/collection/collection_dict_int_map.phpt b/Zend/tests/collection/collection_dict_int_map.phpt new file mode 100644 index 0000000000000..bd15303c71fbf --- /dev/null +++ b/Zend/tests/collection/collection_dict_int_map.phpt @@ -0,0 +1,34 @@ +--TEST-- +Collection: Dictionary: map (int key) +--FILE-- + Book> {} + +collection(Dict) Titles string> {} + +$c1 = new Books(); + +$c1->add(74, new Book('Title 1')); +$c1->add(75, new Book('Title 2')); + +$c2 = $c1->map(fn(Book $b, int $k) => sprintf('%s (%d)', $b->title, $k), Titles::class); +var_dump($c2 instanceof Titles); +var_dump($c2); + +?> +--EXPECTF-- +bool(true) +object(Titles)#%d (%d) { + ["value"]=> + array(2) { + [74]=> + string(12) "Title 1 (74)" + [75]=> + string(12) "Title 2 (75)" + } +} diff --git a/Zend/tests/collection/collection_dict_map_error.phpt b/Zend/tests/collection/collection_dict_map_error.phpt new file mode 100644 index 0000000000000..bc3d58dd5b51a --- /dev/null +++ b/Zend/tests/collection/collection_dict_map_error.phpt @@ -0,0 +1,75 @@ +--TEST-- +Collection: Dictionary: map errors +--FILE-- + string> {} + +collection(Dict) Books Book> {} + +$c1 = new Books(); + +$c1->add('seventy-eight', new Book('Title 1', 1978)); +$c1->add('twenty-three', new Book('Title 2', 2023)); + +try { + $c2 = $c1->map(fn(Book $b, string $k): string => "{$k}: {$b->title}", Unknown::class); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +echo "\n"; + +try { + $c2 = $c1->map(fn(Book $b, string $k): string => "{$k}: {$b->title}", NormalTitle::class); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +echo "\n"; + +try { + $c2 = $c1->map(fn(Book $b, string $k): int => "{$k}: {$b->title}", Titles::class); +} catch (TypeError $e) { + echo get_class($e->getPrevious()), ': ', $e->getPrevious()->getMessage(), "\n"; + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +echo "\n"; + +try { + $c2 = $c1->map(fn(Book $b): string => "{$k}: {$b->title}", Titles::class); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +echo "\n"; + +try { + $c3 = $c1->map(fn(Book $b, int $k) => sprintf('%d: %s', $k, $b->title), Titles::class); +} catch (TypeError $e) { + echo get_class($e->getPrevious()), ': ', $e->getPrevious()->getMessage(), "\n"; + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +TypeError: Type 'Unknown' can not be fetched + +TypeError: Type 'NormalTitle' must implement DictCollection interface + +TypeError: {closure}(): Return value must be of type int, string returned +TypeError: Value type Titles does not match null collection item type string + + +Warning: Undefined variable $k in %scollection_dict_map_error.php on line 45 + +Warning: Undefined variable $k in %scollection_dict_map_error.php on line 45 + +TypeError: {closure}(): Argument #2 ($k) must be of type int, string given +TypeError: Value type Titles does not match null collection item type string diff --git a/Zend/tests/collection/collection_dict_modify.phpt b/Zend/tests/collection/collection_dict_modify.phpt new file mode 100644 index 0000000000000..05f1839e0182e --- /dev/null +++ b/Zend/tests/collection/collection_dict_modify.phpt @@ -0,0 +1,182 @@ +--TEST-- +Collection: Dictionary: Modify +--FILE-- + Book> {} + +$c = new Books(); + +$c->add('one', new Book('Title 1')); +$c->add('two', new Book('Title 2')); + +echo "\nShould have 2 items:\n"; +var_dump($c); + +$c->remove('one'); + +echo "\nShould have one item:\n"; +var_dump($c); + +echo "\nShould be false:\n"; +var_dump($c->has('one')); +var_dump(isset($c['one'])); + +echo "\nShould be true:\n"; +var_dump($c->has('two')); +var_dump(isset($c['two'])); + +$c2 = $c->with('three', new Book('Title 3')); + +echo "\nStill one item:\n"; +var_dump($c); + +echo "\nBut this has 2 items:\n"; +var_dump($c2); + +$c3 = $c2->without('two'); + +echo "\nStill two items:\n"; +var_dump($c2); + +echo "\nBut only one item here:\n"; +var_dump($c3); + +$c3->set('three', new Book('Title 4')); + +echo "\nOnly Title 4 now exists:\n"; +var_dump($c3); + +unset($c3['three']); + +echo "\nEmpty:\n"; +var_dump($c3); + +echo "\nThrows OutOfBoundsException:\n"; +try { + $c3->set('fourtytwo', new Book('Title 1')); +} catch (OutOfBoundsException $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +Should have 2 items: +object(Books)#1 (1) { + ["value"]=> + array(2) { + ["one"]=> + object(Book)#2 (1) { + ["title"]=> + string(7) "Title 1" + } + ["two"]=> + object(Book)#3 (1) { + ["title"]=> + string(7) "Title 2" + } + } +} + +Should have one item: +object(Books)#1 (1) { + ["value"]=> + array(1) { + ["two"]=> + object(Book)#3 (1) { + ["title"]=> + string(7) "Title 2" + } + } +} + +Should be false: +bool(false) +bool(false) + +Should be true: +bool(true) +bool(true) + +Still one item: +object(Books)#1 (1) { + ["value"]=> + array(1) { + ["two"]=> + object(Book)#3 (1) { + ["title"]=> + string(7) "Title 2" + } + } +} + +But this has 2 items: +object(Books)#4 (1) { + ["value"]=> + array(2) { + ["two"]=> + object(Book)#3 (1) { + ["title"]=> + string(7) "Title 2" + } + ["three"]=> + object(Book)#2 (1) { + ["title"]=> + string(7) "Title 3" + } + } +} + +Still two items: +object(Books)#4 (1) { + ["value"]=> + array(2) { + ["two"]=> + object(Book)#3 (1) { + ["title"]=> + string(7) "Title 2" + } + ["three"]=> + object(Book)#2 (1) { + ["title"]=> + string(7) "Title 3" + } + } +} + +But only one item here: +object(Books)#5 (1) { + ["value"]=> + array(1) { + ["three"]=> + object(Book)#2 (1) { + ["title"]=> + string(7) "Title 3" + } + } +} + +Only Title 4 now exists: +object(Books)#5 (1) { + ["value"]=> + array(1) { + ["three"]=> + object(Book)#6 (1) { + ["title"]=> + string(7) "Title 4" + } + } +} + +Empty: +object(Books)#5 (1) { + ["value"]=> + array(0) { + } +} + +Throws OutOfBoundsException: +OutOfBoundsException: Index 'fourtytwo' does not exist in the Books dictionary diff --git a/Zend/tests/collection/collection_dict_modify_errors-001.phpt b/Zend/tests/collection/collection_dict_modify_errors-001.phpt new file mode 100644 index 0000000000000..34e861ccba6b6 --- /dev/null +++ b/Zend/tests/collection/collection_dict_modify_errors-001.phpt @@ -0,0 +1,59 @@ +--TEST-- +Collection: Dictionary: Modify with wrong key type (string) +--FILE-- + Book> {} + +$c = new Books(); + +$c->add('one', new Book('Title 1')); +$c->add('two', new Book('Title 2')); + +try { + $c->remove(1); +} catch (\TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $c->has(1); +} catch (\TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + isset($c[1]); +} catch (\TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $c->with(3, new Book('Title 3')); +} catch (\TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $c->without(2); +} catch (\TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $c->set(3, new Book('Title 4')); +} catch (\TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +TypeError: Key type int of element does not match Books dictionary key type string +TypeError: Key type int of element does not match Books dictionary key type string +TypeError: Key type int of element does not match Books dictionary key type string +TypeError: Key type int of element does not match Books dictionary key type string +TypeError: Key type int of element does not match Books dictionary key type string +TypeError: Key type int of element does not match Books dictionary key type string diff --git a/Zend/tests/collection/collection_dict_modify_errors-002.phpt b/Zend/tests/collection/collection_dict_modify_errors-002.phpt new file mode 100644 index 0000000000000..1cdb7a5cd665e --- /dev/null +++ b/Zend/tests/collection/collection_dict_modify_errors-002.phpt @@ -0,0 +1,59 @@ +--TEST-- +Collection: Dictionary: Modify with wrong key type (int) +--FILE-- + Book> {} + +$c = new Books(); + +$c->add(1, new Book('Title 1')); +$c->add(2, new Book('Title 2')); + +try { + $c->remove('one'); +} catch (\TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $c->has('one'); +} catch (\TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + isset($c['one']); +} catch (\TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $c->with('three', new Book('Title 3')); +} catch (\TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $c->without('two'); +} catch (\TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $c->set('three', new Book('Title 4')); +} catch (\TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +TypeError: Key type string of element does not match Books dictionary key type int +TypeError: Key type string of element does not match Books dictionary key type int +TypeError: Key type string of element does not match Books dictionary key type int +TypeError: Key type string of element does not match Books dictionary key type int +TypeError: Key type string of element does not match Books dictionary key type int +TypeError: Key type string of element does not match Books dictionary key type int diff --git a/Zend/tests/collection/collection_dict_modify_errors-003.phpt b/Zend/tests/collection/collection_dict_modify_errors-003.phpt new file mode 100644 index 0000000000000..f278bb8f52ccc --- /dev/null +++ b/Zend/tests/collection/collection_dict_modify_errors-003.phpt @@ -0,0 +1,63 @@ +--TEST-- +Collection: Dictionary: Modify with unsupported key types +--FILE-- + Book> {} + +$c = new Books(); + +try { + $c->add(M_PI, new Book('Title 1')); +} catch (\TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $c->remove(M_PI); +} catch (\TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $c->has(new Book("This is not a key")); +} catch (\TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + isset($c[M_PI]); +} catch (\TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $c->with(false, new Book("Pie is tasty")); +} catch (\TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $c->without(NULL); +} catch (\TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $c->set(M_PI, new Book("Pai e cho blasta")); +} catch (\TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +TypeError: Key type float of element does not match Books dictionary key type string +TypeError: Key type float of element does not match Books dictionary key type string +TypeError: Key type Book of element does not match Books dictionary key type string +TypeError: Key type float of element does not match Books dictionary key type string +TypeError: Key type bool of element does not match Books dictionary key type string +TypeError: Key type null of element does not match Books dictionary key type string +TypeError: Key type float of element does not match Books dictionary key type string diff --git a/Zend/tests/collection/collection_dict_string_map.phpt b/Zend/tests/collection/collection_dict_string_map.phpt new file mode 100644 index 0000000000000..bd0822251d685 --- /dev/null +++ b/Zend/tests/collection/collection_dict_string_map.phpt @@ -0,0 +1,34 @@ +--TEST-- +Collection: Dictionary: map (string key) +--FILE-- + Book> {} + +collection(Dict) Titles string> {} + +$c1 = new Books(); + +$c1->add('one', new Book('Title 1')); +$c1->add('two', new Book('Title 2')); + +$c2 = $c1->map(fn(Book $b, string $k) => sprintf('%s: %s', $k, $b->title), Titles::class); +var_dump($c2 instanceof Titles); +var_dump($c2); + +?> +--EXPECTF-- +bool(true) +object(Titles)#%d (%d) { + ["value"]=> + array(2) { + ["one"]=> + string(12) "one: Title 1" + ["two"]=> + string(12) "two: Title 2" + } +} diff --git a/Zend/tests/collection/collection_dict_syntax_errors.phpt b/Zend/tests/collection/collection_dict_syntax_errors.phpt new file mode 100644 index 0000000000000..a44c098f2f2af --- /dev/null +++ b/Zend/tests/collection/collection_dict_syntax_errors.phpt @@ -0,0 +1,10 @@ +--TEST-- +Collection: Dictionary without key type defined +--FILE-- + +{ +} +?> +--EXPECTF-- +Fatal error: Collection dictionaries must have a key type defined in %s on line %d diff --git a/Zend/tests/collection/collection_dict_with_trait.phpt b/Zend/tests/collection/collection_dict_with_trait.phpt new file mode 100644 index 0000000000000..c19a07c864aab --- /dev/null +++ b/Zend/tests/collection/collection_dict_with_trait.phpt @@ -0,0 +1,40 @@ +--TEST-- +Collection with Trait +--FILE-- +value; + shuffle($values); + + return $values; + } +} + + +collection(Dict) Articles Article> +{ + use Shuffler; +} + +class Article +{ + public function __construct(private string $title) {} +} + + +$c = new Articles; +$c[0] = new Article("First Test"); + +var_dump($c->shuffle()); +?> +--EXPECTF-- +array(1) { + [0]=> + object(Article)#%d (%d) { + ["title":"Article":private]=> + string(10) "First Test" + } +} diff --git a/Zend/tests/collection/collection_interface_type_check.phpt b/Zend/tests/collection/collection_interface_type_check.phpt new file mode 100644 index 0000000000000..a01e8c2964d72 --- /dev/null +++ b/Zend/tests/collection/collection_interface_type_check.phpt @@ -0,0 +1,37 @@ +--TEST-- +Collection: Type checking against interface +--FILE-- + Article> +{ +} + +collection(Seq) ArticleList
+{ +} + +$c = new Articles(); + +$l = new ArticleList(); + +function needsDict(DictCollection $dict) { + var_dump($dict); +} + +function needsSeq(SeqCollection $seq) { + var_dump($seq); +} + +needsDict($c); +needsSeq($l); + +?> +--EXPECTF-- +object(Articles)#%d (%d) { + ["value"]=> + uninitialized(array) +} +object(ArticleList)#%d (%d) { + ["value"]=> + uninitialized(array) +} diff --git a/Zend/tests/collection/collection_seq_basic.phpt b/Zend/tests/collection/collection_seq_basic.phpt new file mode 100644 index 0000000000000..e6bcab88e5c74 --- /dev/null +++ b/Zend/tests/collection/collection_seq_basic.phpt @@ -0,0 +1,70 @@ +--TEST-- +Collection: Sequence +--FILE-- + +{ +} + +$c = new Articles; +$c[] = new Article("First Test"); +$c[] = new Article("Second Test"); + +$c->add(new Article("Third Test")); + +var_dump($c); + +var_dump(isset($c[0])); +var_dump(isset($c[1])); +var_dump(isset($c[3])); + +var_dump($c[1]); + +unset($c[1]); +var_dump(isset($c[1])); +var_dump(isset($c[2])); + +try { + var_dump(isset($c["eleven"])); +} catch (Error $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +object(Articles)#%d (%d) { + ["value"]=> + array(3) { + [0]=> + object(Article)#%d (%d) { + ["title"]=> + string(10) "First Test" + } + [1]=> + object(Article)#%d (%d) { + ["title"]=> + string(11) "Second Test" + } + [2]=> + object(Article)#%d (%d) { + ["title"]=> + string(10) "Third Test" + } + } +} +bool(true) +bool(true) +bool(false) +object(Article)#%d (%d) { + ["title"]=> + string(11) "Second Test" +} +bool(true) +bool(false) +TypeError: Key type string of element does not match Articles sequence key type int diff --git a/Zend/tests/collection/collection_seq_concat-errors.phpt b/Zend/tests/collection/collection_seq_concat-errors.phpt new file mode 100644 index 0000000000000..c4d78ce0f1c49 --- /dev/null +++ b/Zend/tests/collection/collection_seq_concat-errors.phpt @@ -0,0 +1,61 @@ +--TEST-- +Collection: Sequence: Concat Errors +--FILE-- + {} +collection(Seq) DutchBooks {} +collection(Seq) Articles
{} + +$c1 = new Books(); +$c1->add(new Book('Title 1')); +$c1->add(new Book('Title 2')); + +$d1 = new DutchBooks(); +$d1->add(new DutchBook('Titel 1')); +$d1->add(new DutchBook('Titel 2')); + +$a2 = new Articles(); +$a2->add(new Article('Subject 1')); +$a2->add(new Article('Subject 2')); + +try { + $c1->concat(new Book('Title E')); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $c1->concat($a2); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $cr = $c1->concat($d1); + echo "Inheritence: OK\n"; +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $cr = $d1->concat($c1); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +TypeError: Books::concat(): Argument #1 ($other) must be of type SeqCollection, Book given +TypeError: Value type Books does not match Article collection item type Book +Inheritence: OK +TypeError: Value type DutchBooks does not match Book collection item type DutchBook diff --git a/Zend/tests/collection/collection_seq_concat.phpt b/Zend/tests/collection/collection_seq_concat.phpt new file mode 100644 index 0000000000000..231e96233de8a --- /dev/null +++ b/Zend/tests/collection/collection_seq_concat.phpt @@ -0,0 +1,70 @@ +--TEST-- +Collection: Sequence: Concat +--FILE-- + {} + +$c1 = new Books(); +$c2 = new Books(); + +$c1->add(new Book('Title 1')); +$c1->add(new Book('Title 2')); + +$c2->add(new Book('Title 3')); +$c2->add(new Book('Title 4')); + +$c3 = $c1->concat($c2); + +// Four items. +var_dump($c3); + +// Still two items. +var_dump($c1); + +?> +--EXPECTF-- +object(Books)#%d (1) { + ["value"]=> + array(4) { + [0]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 1" + } + [1]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 2" + } + [2]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 3" + } + [3]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 4" + } + } +} +object(Books)#%d (1) { + ["value"]=> + array(2) { + [0]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 1" + } + [1]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 2" + } + } +} diff --git a/Zend/tests/collection/collection_seq_equality-errors.phpt b/Zend/tests/collection/collection_seq_equality-errors.phpt new file mode 100644 index 0000000000000..9c8fb249473a6 --- /dev/null +++ b/Zend/tests/collection/collection_seq_equality-errors.phpt @@ -0,0 +1,60 @@ +--TEST-- +Collection: Sequence: Equals Errors +--FILE-- + {} +collection(Seq) BooksOther {} + +$c1 = new Books(); +$c1->add(new Book('Title 1')); +$c1->add(new Book('Title 2')); + +try { + $c1->equals(new Book('Title 3')); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $c1->equals(null); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + var_dump($c1 == new Book('Title 3')); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + var_dump(new Book('Title 3') == $c1); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + var_dump($c1 == M_PI); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + var_dump(false == $c1); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +TypeError: Books::equals(): Argument #1 ($other) must be of type SeqCollection, Book given +TypeError: Books::equals(): Argument #1 ($other) must be of type SeqCollection, null given +bool(false) +bool(false) +bool(false) +bool(false) diff --git a/Zend/tests/collection/collection_seq_equality.phpt b/Zend/tests/collection/collection_seq_equality.phpt new file mode 100644 index 0000000000000..aa93c3a9c081d --- /dev/null +++ b/Zend/tests/collection/collection_seq_equality.phpt @@ -0,0 +1,63 @@ +--TEST-- +Collection: Sequence: Equals +--FILE-- + {} +collection(Seq) BooksOther {} + + +$c1 = new Books(); +$c1->add(new Book('Title 1')); +$c1->add(new Book('Title 2')); + +$c2 = new Books(); +$c2->add(new Book('Title 1')); +$c2->add(new Book('Title 2')); + +$c3 = new Books(); +$c3->add(new Book('Title 1')); +$c3->add(new Book('Title 2')); +$c3->add(new Book('Title 3')); + +$c4 = new Books(); +$c4->add(new Book('Title 1')); +$c4->add(new Book('Title X')); +$c4->add(new Book('Title 2')); + +$c5 = new BooksOther(); +$c5->add(new Book('Title 1')); +$c5->add(new Book('Title 2')); + +// True +var_dump($c1->equals($c1)); +var_dump($c1->equals($c2)); +var_dump($c1->equals($c5)); + +$c2[] = new Book('Title 3'); + +// False +var_dump($c1->equals($c2)); +var_dump($c3->equals($c4)); + +unset($c3[2]); +unset($c4[1]); + +// True +var_dump($c1->equals($c3)); + +// True (indexes have been automatically renumbered) +var_dump($c1->equals($c4)); +?> +--EXPECTF-- +bool(true) +bool(true) +bool(true) +bool(false) +bool(false) +bool(true) +bool(true) diff --git a/Zend/tests/collection/collection_seq_equality_handler.phpt b/Zend/tests/collection/collection_seq_equality_handler.phpt new file mode 100644 index 0000000000000..06f48e7a659a8 --- /dev/null +++ b/Zend/tests/collection/collection_seq_equality_handler.phpt @@ -0,0 +1,63 @@ +--TEST-- +Collection: Sequence: Comparison Handler +--FILE-- + {} +collection(Seq) BooksOther {} + + +$c1 = new Books(); +$c1->add(new Book('Title 1')); +$c1->add(new Book('Title 2')); + +$c2 = new Books(); +$c2->add(new Book('Title 1')); +$c2->add(new Book('Title 2')); + +$c3 = new Books(); +$c3->add(new Book('Title 1')); +$c3->add(new Book('Title 2')); +$c3->add(new Book('Title 3')); + +$c4 = new Books(); +$c4->add(new Book('Title 1')); +$c4->add(new Book('Title X')); +$c4->add(new Book('Title 2')); + +$c5 = new BooksOther(); +$c5->add(new Book('Title 1')); +$c5->add(new Book('Title 2')); + +// True +var_dump($c1 == $c1); +var_dump($c1 == $c2); +var_dump($c1 == $c5); + +$c2[] = new Book('Title 3'); + +// False +var_dump($c1 == $c2); +var_dump($c3 == $c4); + +unset($c3[2]); +unset($c4[1]); + +// True +var_dump($c1 == $c3); + +// True (indexes have been automatically renumbered) +var_dump($c1 == $c4); +?> +--EXPECTF-- +bool(true) +bool(true) +bool(true) +bool(false) +bool(false) +bool(true) +bool(true) diff --git a/Zend/tests/collection/collection_seq_errors.phpt b/Zend/tests/collection/collection_seq_errors.phpt new file mode 100644 index 0000000000000..cb5adc0eb9cd4 --- /dev/null +++ b/Zend/tests/collection/collection_seq_errors.phpt @@ -0,0 +1,25 @@ +--TEST-- +Collection: Sequence with errors +--FILE-- + +{ +} + +$c = new Articles; + +try { + $c["first"] = new Article("First Test"); +} catch (Error $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +ValueError: Specifying an offset for sequence collections is not allowed diff --git a/Zend/tests/collection/collection_seq_map.phpt b/Zend/tests/collection/collection_seq_map.phpt new file mode 100644 index 0000000000000..265fc1da5792a --- /dev/null +++ b/Zend/tests/collection/collection_seq_map.phpt @@ -0,0 +1,38 @@ +--TEST-- +Collection: Sequence: map +--FILE-- + {} + +collection(Seq) Titles {} + +$c1 = new Books(); + +$c1->add(new Book('Title 1')); +$c1->add(new Book('Title 2')); + +$c2 = $c1->map(fn(Book $b): string => $b->title, Titles::class); + +// True +var_dump($c2 instanceof Titles); + +// List of 2 strings +var_dump($c2); + +?> +--EXPECTF-- +bool(true) +object(Titles)#5 (1) { + ["value"]=> + array(2) { + [0]=> + string(7) "Title 1" + [1]=> + string(7) "Title 2" + } +} diff --git a/Zend/tests/collection/collection_seq_map_error.phpt b/Zend/tests/collection/collection_seq_map_error.phpt new file mode 100644 index 0000000000000..3506e8a9c4bdc --- /dev/null +++ b/Zend/tests/collection/collection_seq_map_error.phpt @@ -0,0 +1,46 @@ +--TEST-- +Collection: Sequence: map errors +--FILE-- + {} + +collection(Seq) Books {} + +$c1 = new Books(); + +$c1->add(new Book('Title 1', 1978)); +$c1->add(new Book('Title 2', 2023)); + +try { + $c2 = $c1->map(fn(Book $b): string => $b->title, Unknown::class); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $c2 = $c1->map(fn(Book $b): string => $b->title, NormalTitle::class); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $c2 = $c1->map(fn(Book $b): int => $b->copyright, Titles::class); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +TypeError: Type 'Unknown' can not be fetched +TypeError: Type 'NormalTitle' must implement SeqCollection interface +TypeError: Value type Titles does not match int collection item type string diff --git a/Zend/tests/collection/collection_seq_modify.phpt b/Zend/tests/collection/collection_seq_modify.phpt new file mode 100644 index 0000000000000..17d27867e949e --- /dev/null +++ b/Zend/tests/collection/collection_seq_modify.phpt @@ -0,0 +1,182 @@ +--TEST-- +Collection: Sequence: Modify +--FILE-- + {} + +$c = new Books(); + +$c->add(new Book('Title 1')); +$c->add(new Book('Title 2')); + +echo "\nShould have 2 items:\n"; +var_dump($c); + +$c->remove(0); + +echo "\nShould have one item:\n"; +var_dump($c); + +echo "\nShould be false:\n"; +var_dump($c->has(1)); +var_dump(isset($c[1])); + +echo "\nShould be true:\n"; +var_dump($c->has(0)); +var_dump(isset($c[0])); + +$c2 = $c->with(new Book('Title 3')); + +echo "\nStill one item:\n"; +var_dump($c); + +echo "\nBut this has 2 items:\n"; +var_dump($c2); + +$c3 = $c2->without(1); + +echo "\nStill two items:\n"; +var_dump($c2); + +echo "\nBut only one item here:\n"; +var_dump($c3); + +$c3->set(0, new Book('Title 4')); + +echo "\nOnly 'Title 4' now exists:\n"; +var_dump($c3); + +unset($c3[0]); + +echo "\nEmpty:\n"; +var_dump($c3); + +echo "\nThrows OutOfBoundsException:\n"; +try { + $c3->set(42, new Book('Title 1')); +} catch (OutOfBoundsException $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +Should have 2 items: +object(Books)#1 (1) { + ["value"]=> + array(2) { + [0]=> + object(Book)#2 (1) { + ["title"]=> + string(7) "Title 1" + } + [1]=> + object(Book)#3 (1) { + ["title"]=> + string(7) "Title 2" + } + } +} + +Should have one item: +object(Books)#1 (1) { + ["value"]=> + array(1) { + [0]=> + object(Book)#3 (1) { + ["title"]=> + string(7) "Title 2" + } + } +} + +Should be false: +bool(false) +bool(false) + +Should be true: +bool(true) +bool(true) + +Still one item: +object(Books)#1 (1) { + ["value"]=> + array(1) { + [0]=> + object(Book)#3 (1) { + ["title"]=> + string(7) "Title 2" + } + } +} + +But this has 2 items: +object(Books)#4 (1) { + ["value"]=> + array(2) { + [0]=> + object(Book)#3 (1) { + ["title"]=> + string(7) "Title 2" + } + [1]=> + object(Book)#2 (1) { + ["title"]=> + string(7) "Title 3" + } + } +} + +Still two items: +object(Books)#4 (1) { + ["value"]=> + array(2) { + [0]=> + object(Book)#3 (1) { + ["title"]=> + string(7) "Title 2" + } + [1]=> + object(Book)#2 (1) { + ["title"]=> + string(7) "Title 3" + } + } +} + +But only one item here: +object(Books)#5 (1) { + ["value"]=> + array(1) { + [0]=> + object(Book)#3 (1) { + ["title"]=> + string(7) "Title 2" + } + } +} + +Only 'Title 4' now exists: +object(Books)#5 (1) { + ["value"]=> + array(1) { + [0]=> + object(Book)#6 (1) { + ["title"]=> + string(7) "Title 4" + } + } +} + +Empty: +object(Books)#5 (1) { + ["value"]=> + array(0) { + } +} + +Throws OutOfBoundsException: +OutOfBoundsException: Index '42' does not exist in the Books sequence diff --git a/Zend/tests/collection/collection_seq_modify_errors-001.phpt b/Zend/tests/collection/collection_seq_modify_errors-001.phpt new file mode 100644 index 0000000000000..d033ed0aa1fd5 --- /dev/null +++ b/Zend/tests/collection/collection_seq_modify_errors-001.phpt @@ -0,0 +1,49 @@ +--TEST-- +Collection: Sequence: Modify with unsupported key types +--FILE-- + {} + +$c = new Books(); + +try { + $c->remove('zero'); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $c->has(new Book("Ne pas un cle")); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $c->get(false); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $c->without(M_PI); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +try { + $c->set(null, new Book("A Random Title")); +} catch (TypeError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +TypeError: Key type string of element does not match Books sequence key type int +TypeError: Key type Book of element does not match Books sequence key type int +TypeError: Key type bool of element does not match Books sequence key type int +TypeError: Key type float of element does not match Books sequence key type int +TypeError: Key type null of element does not match Books sequence key type int diff --git a/Zend/tests/collection/collection_seq_operator_concat.phpt b/Zend/tests/collection/collection_seq_operator_concat.phpt new file mode 100644 index 0000000000000..2da9bd0e5a4a6 --- /dev/null +++ b/Zend/tests/collection/collection_seq_operator_concat.phpt @@ -0,0 +1,118 @@ +--TEST-- +Collection: Sequence: Concat +--FILE-- + {} + +$c1 = new Books(); +$c2 = new Books(); + +$c1->add(new Book('Title 1')); +$c1->add(new Book('Title 2')); + +$c2->add(new Book('Title 3')); +$c2->add(new Book('Title 4')); + +$c3 = $c1 + $c2; + +// Four items. +var_dump($c3); + +// Still two items. +var_dump($c1); + +$c1 += $c2; + +// Four items. +var_dump($c1); + +// Still two items. +var_dump($c2); + +?> +--EXPECTF-- +object(Books)#%d (1) { + ["value"]=> + array(4) { + [0]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 1" + } + [1]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 2" + } + [2]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 3" + } + [3]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 4" + } + } +} +object(Books)#%d (1) { + ["value"]=> + array(2) { + [0]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 1" + } + [1]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 2" + } + } +} +object(Books)#%d (1) { + ["value"]=> + array(4) { + [0]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 1" + } + [1]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 2" + } + [2]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 3" + } + [3]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 4" + } + } +} +object(Books)#%d (1) { + ["value"]=> + array(2) { + [0]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 3" + } + [1]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 4" + } + } +} diff --git a/Zend/tests/collection/collection_seq_operator_subtract.phpt b/Zend/tests/collection/collection_seq_operator_subtract.phpt new file mode 100644 index 0000000000000..e3f8d2c1f999f --- /dev/null +++ b/Zend/tests/collection/collection_seq_operator_subtract.phpt @@ -0,0 +1,43 @@ +--TEST-- +Collection: Sequence: Concat +--FILE-- + {} + +$c1 = new Books(); +$c2 = new Books(); + +$c1->add(new Book('Title 1')); +$c1->add(new Book('Title 2')); + +$c2->add(new Book('Title 3')); +$c2->add(new Book('Title 4')); + +$c3 = $c1 + $c2; +$c4 = $c3 - $c2; + +// Two items again +var_dump($c4); + +?> +--EXPECTF-- +object(Books)#%d (1) { + ["value"]=> + array(2) { + [0]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 1" + } + [1]=> + object(Book)#%d (1) { + ["title"]=> + string(7) "Title 2" + } + } +} diff --git a/Zend/tests/collection/collection_seq_syntax_errors.phpt b/Zend/tests/collection/collection_seq_syntax_errors.phpt new file mode 100644 index 0000000000000..edae30b2cae01 --- /dev/null +++ b/Zend/tests/collection/collection_seq_syntax_errors.phpt @@ -0,0 +1,10 @@ +--TEST-- +Collection: Sequence with key type defined +--FILE-- + Article> +{ +} +?> +--EXPECTF-- +Fatal error: Collection sequences may not have a key type defined in %s on line %d diff --git a/Zend/tests/collection/collection_syntax-001.phpt b/Zend/tests/collection/collection_syntax-001.phpt new file mode 100644 index 0000000000000..f7a8603f4538a --- /dev/null +++ b/Zend/tests/collection/collection_syntax-001.phpt @@ -0,0 +1,26 @@ +--TEST-- +Collection: Syntax +--FILE-- + Article> +{ +} + +CoLleCtION(dICT) Books Book> +{ +} + +$a = new Articles; +$b = new Books; + +var_dump($a, $b); +?> +--EXPECTF-- +object(Articles)#%d (%d) { + ["value"]=> + uninitialized(array) +} +object(Books)#%d (%d) { + ["value"]=> + uninitialized(array) +} diff --git a/Zend/tests/collection/collection_syntax-002.phpt b/Zend/tests/collection/collection_syntax-002.phpt new file mode 100644 index 0000000000000..61cf09b7d8eec --- /dev/null +++ b/Zend/tests/collection/collection_syntax-002.phpt @@ -0,0 +1,17 @@ +--TEST-- +Collection: Sequence syntax with scalars +--FILE-- + +{ +} + +$c = new Titles; + +var_dump($c); +?> +--EXPECTF-- +object(Titles)#%d (%d) { + ["value"]=> + uninitialized(array) +} diff --git a/Zend/tests/collection/collection_syntax-003.phpt b/Zend/tests/collection/collection_syntax-003.phpt new file mode 100644 index 0000000000000..cb5e10016b036 --- /dev/null +++ b/Zend/tests/collection/collection_syntax-003.phpt @@ -0,0 +1,20 @@ +--TEST-- +Collection: Syntax with 'Collection' class +--FILE-- + +{ +} + +$a = new Articles; + +var_dump($a); +?> +--EXPECTF-- +object(Articles)#%d (%d) { + ["value"]=> + uninitialized(array) +} diff --git a/Zend/tests/collection/collection_syntax_errors.phpt b/Zend/tests/collection/collection_syntax_errors.phpt new file mode 100644 index 0000000000000..59757e8fda28f --- /dev/null +++ b/Zend/tests/collection/collection_syntax_errors.phpt @@ -0,0 +1,10 @@ +--TEST-- +Collection: Syntax Errors +--FILE-- + Article => three> +{ +} +?> +--EXPECTF-- +Fatal error: Invalid collection data types signature in %s on line %d diff --git a/Zend/tests/collection/collection_syntax_unknown_collection.phpt b/Zend/tests/collection/collection_syntax_unknown_collection.phpt new file mode 100644 index 0000000000000..611a3ada7cf6d --- /dev/null +++ b/Zend/tests/collection/collection_syntax_unknown_collection.phpt @@ -0,0 +1,10 @@ +--TEST-- +Collection with unsupported structure type +--FILE-- + Article> +{ +} +?> +--EXPECTF-- +Parse error: syntax error, unexpected identifier "Articles" in %s on line %d diff --git a/Zend/zend.h b/Zend/zend.h index 4fe0703d42f69..8016032291dad 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -144,6 +144,9 @@ struct _zend_inheritance_cache_entry { zend_class_entry *traits_and_interfaces[1]; }; +#define ZEND_COLLECTION_SEQ 1 +#define ZEND_COLLECTION_DICT 2 + struct _zend_class_entry { char type; zend_string *name; @@ -218,10 +221,14 @@ struct _zend_class_entry { zend_trait_precedence **trait_precedences; HashTable *attributes; + zend_string *doc_comment; + uint32_t enum_backing_type; HashTable *backed_enum_table; - zend_string *doc_comment; + uint16_t collection_data_structure; + uint32_t collection_key_type; + zend_type collection_item_type; union { struct { diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index 5ecda76870452..2294f66064945 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -1878,6 +1878,8 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio smart_str_appends(str, "trait "); } else if (decl->flags & ZEND_ACC_ENUM) { smart_str_appends(str, "enum "); + } else if (decl->flags & ZEND_ACC_COLLECTION) { + smart_str_appends(str, "collection "); } else { if (decl->flags & ZEND_ACC_EXPLICIT_ABSTRACT_CLASS) { smart_str_appends(str, "abstract "); diff --git a/Zend/zend_collection.c b/Zend/zend_collection.c new file mode 100644 index 0000000000000..01607e8777093 --- /dev/null +++ b/Zend/zend_collection.c @@ -0,0 +1,1029 @@ +/* + +----------------------------------------------------------------------+ + | Zend Engine | + +----------------------------------------------------------------------+ + | Copyright (c) Zend Technologies Ltd. (http://www.zend.com) | + +----------------------------------------------------------------------+ + | This source file is subject to version 2.00 of the Zend license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | http://www.zend.com/license/2_00.txt. | + | If you did not receive a copy of the Zend license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@zend.com so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: Derick Rethans | + +----------------------------------------------------------------------+ +*/ + +#include "zend.h" +#include "zend_API.h" +#include "zend_collection_arginfo.h" +#include "zend_execute.h" +#include "zend_exceptions.h" +#include "zend_extensions.h" +#include "zend_observer.h" + +#include "ext/spl/spl_exceptions.h" + +ZEND_API zend_class_entry *zend_ce_seq_collection; +ZEND_API zend_class_entry *zend_ce_dict_collection; +ZEND_API zend_object_handlers zend_seq_collection_object_handlers; +ZEND_API zend_object_handlers zend_dict_collection_object_handlers; + +static int seq_collection_compare(zval *object1, zval *object2); +static int dict_collection_compare(zval *object1, zval *object2); + +static zend_result seq_collection_do_operation(uint8_t opcode, zval *result, zval *op1, zval *op2); + +static int zend_implement_collection(zend_class_entry *interface, zend_class_entry *class_type) +{ + if (class_type->ce_flags & ZEND_ACC_COLLECTION) { + return SUCCESS; + } + + zend_error_noreturn(E_ERROR, "Non-collection class %s cannot implement interface %s", + ZSTR_VAL(class_type->name), + ZSTR_VAL(interface->name)); + + return FAILURE; +} + +void zend_register_collection_ce(void) +{ + zend_ce_seq_collection = register_class_SeqCollection(); + zend_ce_seq_collection->interface_gets_implemented = zend_implement_collection; + + memcpy(&zend_seq_collection_object_handlers, &std_object_handlers, sizeof(zend_object_handlers)); + zend_seq_collection_object_handlers.clone_obj = NULL; + zend_seq_collection_object_handlers.compare = seq_collection_compare; + zend_seq_collection_object_handlers.do_operation = seq_collection_do_operation; + + zend_ce_dict_collection = register_class_DictCollection(); + zend_ce_dict_collection->interface_gets_implemented = zend_implement_collection; + + memcpy(&zend_dict_collection_object_handlers, &std_object_handlers, sizeof(zend_object_handlers)); + zend_dict_collection_object_handlers.clone_obj = NULL; + zend_dict_collection_object_handlers.compare = dict_collection_compare; +} + +void zend_collection_add_interfaces(zend_class_entry *ce) +{ + uint32_t num_interfaces_before = ce->num_interfaces; + + ce->num_interfaces++; + + ZEND_ASSERT(!(ce->ce_flags & ZEND_ACC_RESOLVED_INTERFACES)); + + ce->interface_names = erealloc(ce->interface_names, sizeof(zend_class_name) * ce->num_interfaces); + + switch (ce->collection_data_structure) { + case ZEND_COLLECTION_SEQ: + ce->interface_names[num_interfaces_before].name = zend_string_copy(zend_ce_seq_collection->name); + ce->interface_names[num_interfaces_before].lc_name = ZSTR_INIT_LITERAL("seqcollection", 0); + ce->default_object_handlers = &zend_seq_collection_object_handlers; + break; + case ZEND_COLLECTION_DICT: + ce->interface_names[num_interfaces_before].name = zend_string_copy(zend_ce_dict_collection->name); + ce->interface_names[num_interfaces_before].lc_name = ZSTR_INIT_LITERAL("dictcollection", 0); + ce->default_object_handlers = &zend_dict_collection_object_handlers; + break; + } + +} + +bool zend_collection_add_item(zend_object *object, zval *offset, zval *value); +void zend_collection_set_item(zend_object *object, zval *offset, zval *value); +int zend_collection_has_item(zend_object *object, zval *offset); +zval *zend_collection_read_item(zend_object *object, zval *offset); +void zend_collection_unset_item(zend_object *object, zval *offset); + +static void create_array_if_needed(zend_class_entry *ce, zend_object *object); + +static const char *get_data_structure_name(zend_class_entry *ce) +{ + if (ce->collection_data_structure == ZEND_COLLECTION_SEQ) { + return "sequence"; + } + if (ce->collection_data_structure == ZEND_COLLECTION_DICT) { + return "dictionary"; + } + return "unknown"; +} + +static int key_type_allowed(zend_class_entry *ce, zval *offset) +{ + ZEND_ASSERT(ce->ce_flags & ZEND_ACC_COLLECTION); + + if (Z_TYPE_P(offset) != IS_LONG && Z_TYPE_P(offset) != IS_STRING) { + zend_type_error( + "Key type %s is not allowed for %s %s", + ZSTR_VAL(ce->name), + offset ? zend_zval_type_name(offset) : zend_get_type_by_const(IS_NULL), + get_data_structure_name(ce) + ); + } + + if (ce->collection_key_type != Z_TYPE_P(offset)) { + zend_type_error( + "Key type %s of element does not match %s %s key type %s", + offset ? zend_zval_type_name(offset) : zend_get_type_by_const(IS_NULL), + ZSTR_VAL(ce->name), + get_data_structure_name(ce), + zend_get_type_by_const(ce->collection_key_type) + ); + return false; + } + + return true; +} + +static int collection_is_equal_function(zval *z1, zval *z2) +{ + zval result; + is_equal_function(&result, z1, z2); + + return Z_TYPE(result) == IS_TRUE ? 0 : 1; +} + +static int seq_collection_compare(zval *object1, zval *object2) +{ + zval *value_prop_object1, *value_prop_object2; + + if (!object1 || !object2) { + return ZEND_UNCOMPARABLE; + } + + if (Z_TYPE_P(object1) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(object1), zend_ce_seq_collection)) { + return 1; + } + if (Z_TYPE_P(object2) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(object2), zend_ce_seq_collection)) { + return 1; + } + + if (Z_OBJ_P(object1) == Z_OBJ_P(object2)) { + return 0; + } + + value_prop_object1 = zend_read_property_ex(Z_OBJCE_P(object1), Z_OBJ_P(object1), ZSTR_KNOWN(ZEND_STR_VALUE), true, NULL); + value_prop_object2 = zend_read_property_ex(Z_OBJCE_P(object2), Z_OBJ_P(object2), ZSTR_KNOWN(ZEND_STR_VALUE), true, NULL); + + if (!value_prop_object1 || !value_prop_object2) { + return 1; + } + + return (zend_hash_compare( + Z_ARRVAL_P(value_prop_object1), + Z_ARRVAL_P(value_prop_object2), + (compare_func_t) collection_is_equal_function, true + ) != 0); +} + +static void seq_copy_add_elements(zend_object *clone, zval *other) +{ + zend_class_entry *ce = clone->ce; + zval *value_prop; + zval *element; + + value_prop = zend_read_property_ex(ce, Z_OBJ_P(other), ZSTR_KNOWN(ZEND_STR_VALUE), true, NULL); + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(value_prop), element) { + if (!zend_collection_add_item(clone, NULL, element)) { + return; + } + } ZEND_HASH_FOREACH_END(); +} + +static zend_result seq_collection_do_operation_add_impl(uint8_t opcode, zval *result, zval *op1, zval *op2) +{ + zend_object *clone; + + clone = zend_objects_clone_obj(Z_OBJ_P(op1)); + + seq_copy_add_elements(clone, op2); + + ZVAL_OBJ(result, clone); + + return SUCCESS; +} + +static zend_result seq_collection_do_operation(uint8_t opcode, zval *result, zval *op1, zval *op2) +{ + if (Z_TYPE_P(op1) != IS_OBJECT || Z_TYPE_P(op2) != IS_OBJECT) { + return FAILURE; + } + + if (!instanceof_function(Z_OBJCE_P(op1), zend_ce_seq_collection) || !instanceof_function(Z_OBJCE_P(op2), zend_ce_seq_collection)) { + return FAILURE; + } + + switch (opcode) { + case ZEND_ADD: + return seq_collection_do_operation_add_impl(opcode, result, op1, op2); + } + return FAILURE; +} + +static int dict_collection_compare(zval *object1, zval *object2) +{ + zval *value_prop_object1, *value_prop_object2; + + if (!object1 || !object2) { + return ZEND_UNCOMPARABLE; + } + + if (Z_TYPE_P(object1) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(object1), zend_ce_dict_collection)) { + return 1; + } + if (Z_TYPE_P(object2) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(object2), zend_ce_dict_collection)) { + return 1; + } + + if (Z_OBJ_P(object1) == Z_OBJ_P(object2)) { + return 0; + } + + value_prop_object1 = zend_read_property_ex(Z_OBJCE_P(object1), Z_OBJ_P(object1), ZSTR_KNOWN(ZEND_STR_VALUE), true, NULL); + value_prop_object2 = zend_read_property_ex(Z_OBJCE_P(object2), Z_OBJ_P(object2), ZSTR_KNOWN(ZEND_STR_VALUE), true, NULL); + + if (!value_prop_object1 || !value_prop_object2) { + return 1; + } + + return (zend_hash_compare( + Z_ARRVAL_P(value_prop_object1), + Z_ARRVAL_P(value_prop_object2), + (compare_func_t) collection_is_equal_function, false + ) != 0); +} + +static ZEND_NAMED_FUNCTION(zend_collection_seq_add_func) +{ + zval *value; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ZVAL(value) + ZEND_PARSE_PARAMETERS_END(); + + zend_collection_add_item(Z_OBJ_P(ZEND_THIS), NULL, value); + + RETURN_OBJ_COPY(Z_OBJ_P(ZEND_THIS)); +} + +static ZEND_NAMED_FUNCTION(zend_collection_seq_remove_func) +{ + zval *index; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ZVAL(index) + ZEND_PARSE_PARAMETERS_END(); + + zend_collection_unset_item(Z_OBJ_P(ZEND_THIS), index); + + RETURN_OBJ_COPY(Z_OBJ_P(ZEND_THIS)); +} + +static ZEND_NAMED_FUNCTION(zend_collection_seq_has_func) +{ + zval *index; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ZVAL(index) + ZEND_PARSE_PARAMETERS_END(); + + RETURN_BOOL(zend_collection_has_item(Z_OBJ_P(ZEND_THIS), index)); +} + +static ZEND_NAMED_FUNCTION(zend_collection_seq_get_func) +{ + zval *index, *value; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ZVAL(index) + ZEND_PARSE_PARAMETERS_END(); + + value = zend_collection_read_item(Z_OBJ_P(ZEND_THIS), index); + if (!value) { + return; + } + + RETURN_COPY_VALUE(value); +} + +static ZEND_NAMED_FUNCTION(zend_collection_seq_with_func) +{ + zval *value; + zend_object *clone; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ZVAL(value) + ZEND_PARSE_PARAMETERS_END(); + + clone = zend_objects_clone_obj(Z_OBJ_P(ZEND_THIS)); + + zend_collection_add_item(clone, NULL, value); + + RETURN_OBJ(clone); +} + +static ZEND_NAMED_FUNCTION(zend_collection_seq_without_func) +{ + zval *index; + zend_object *clone; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ZVAL(index) + ZEND_PARSE_PARAMETERS_END(); + + clone = zend_objects_clone_obj(Z_OBJ_P(ZEND_THIS)); + + zend_collection_unset_item(clone, index); + + RETURN_OBJ(clone); +} + +static ZEND_NAMED_FUNCTION(zend_collection_seq_set_func) +{ + zval *index; + zval *value; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_ZVAL(index) + Z_PARAM_ZVAL(value) + ZEND_PARSE_PARAMETERS_END(); + + if (!key_type_allowed(Z_OBJCE_P(ZEND_THIS), index)) { + return; + } + + if (!zend_collection_has_item(Z_OBJ_P(ZEND_THIS), index)) { + zend_throw_exception_ex( + spl_ce_OutOfBoundsException, 0, + "Index '%ld' does not exist in the %s sequence", + Z_LVAL_P(index), ZSTR_VAL(Z_OBJCE_P(ZEND_THIS)->name) + ); + } + + zend_collection_set_item(Z_OBJ_P(ZEND_THIS), index, value); + + RETURN_OBJ_COPY(Z_OBJ_P(ZEND_THIS)); +} + +static ZEND_NAMED_FUNCTION(zend_collection_seq_concat_func) +{ + zval *other; + zend_object *clone; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(other, zend_ce_seq_collection); + ZEND_PARSE_PARAMETERS_END(); + + clone = zend_objects_clone_obj(Z_OBJ_P(ZEND_THIS)); + + seq_copy_add_elements(clone, other); + + RETURN_OBJ(clone); +} + +static ZEND_NAMED_FUNCTION(zend_collection_seq_equals_func) +{ + zval *other; + zval *value_prop_us, *value_prop_other; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(other, zend_ce_seq_collection); + ZEND_PARSE_PARAMETERS_END(); + + if (Z_OBJ_P(ZEND_THIS) == Z_OBJ_P(other)) { + RETURN_TRUE; + } + + value_prop_us = zend_read_property_ex(Z_OBJCE_P(ZEND_THIS), Z_OBJ_P(ZEND_THIS), ZSTR_KNOWN(ZEND_STR_VALUE), true, NULL); + value_prop_other = zend_read_property_ex(Z_OBJCE_P(other), Z_OBJ_P(other), ZSTR_KNOWN(ZEND_STR_VALUE), true, NULL); + + if (!value_prop_us || !value_prop_other) { + RETURN_FALSE; + } + + RETURN_BOOL(zend_hash_compare(Z_ARRVAL_P(value_prop_us), Z_ARRVAL_P(value_prop_other), (compare_func_t) collection_is_equal_function, true) == 0); +} + +static ZEND_NAMED_FUNCTION(zend_collection_seq_map_func) +{ + zend_object *object = Z_OBJ_P(ZEND_THIS); + zend_class_entry *ce = Z_OBJCE_P(ZEND_THIS); + zend_fcall_info fci = empty_fcall_info; + zend_fcall_info_cache fci_cache = empty_fcall_info_cache; + zval *value_prop; + zend_string *type; + zval *operand; + zval retval; + zval args[1]; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_FUNC(fci, fci_cache) + Z_PARAM_STR(type) + ZEND_PARSE_PARAMETERS_END(); + + zend_class_entry *return_ce = zend_lookup_class(type); + if (!return_ce) { + zend_type_error( + "Type '%s' can not be fetched", + ZSTR_VAL(type) + ); + return; + } + + if (!instanceof_function(return_ce, zend_ce_seq_collection)) { + zend_type_error( + "Type '%s' must implement SeqCollection interface", + ZSTR_VAL(type) + ); + return; + } + + create_array_if_needed(ce, object); + value_prop = zend_read_property_ex(ce, object, ZSTR_KNOWN(ZEND_STR_VALUE), true, NULL); + + object_init_ex(return_value, return_ce); + Z_OBJCE_P(return_value)->collection_data_structure = return_ce->collection_data_structure; + + fci.retval = &retval; + fci.param_count = 1; + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(value_prop), operand) { + ZVAL_COPY(&args[0], operand); + + fci.params = args; + + if (zend_call_function(&fci, &fci_cache) == SUCCESS) { + zval_ptr_dtor(&args[0]); + + if (!zend_collection_add_item(Z_OBJ_P(return_value), NULL, &retval)) { + return; + } + } else { + zval_ptr_dtor(&args[0]); + + return; + } + + } ZEND_HASH_FOREACH_END(); +} + + +static ZEND_NAMED_FUNCTION(zend_collection_dict_add_func) +{ + zval *key, *value; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_ZVAL(key) + Z_PARAM_ZVAL(value) + ZEND_PARSE_PARAMETERS_END(); + + zend_collection_add_item(Z_OBJ_P(ZEND_THIS), key, value); + + RETURN_OBJ_COPY(Z_OBJ_P(ZEND_THIS)); +} + +static ZEND_NAMED_FUNCTION(zend_collection_dict_remove_func) +{ + zval *index; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ZVAL(index) + ZEND_PARSE_PARAMETERS_END(); + + zend_collection_unset_item(Z_OBJ_P(ZEND_THIS), index); + + RETURN_OBJ_COPY(Z_OBJ_P(ZEND_THIS)); +} + +static ZEND_NAMED_FUNCTION(zend_collection_dict_has_func) +{ + zval *index; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ZVAL(index) + ZEND_PARSE_PARAMETERS_END(); + + RETURN_BOOL(zend_collection_has_item(Z_OBJ_P(ZEND_THIS), index)); +} + +static ZEND_NAMED_FUNCTION(zend_collection_dict_get_func) +{ + zval *index, *value; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ZVAL(index) + ZEND_PARSE_PARAMETERS_END(); + + value = zend_collection_read_item(Z_OBJ_P(ZEND_THIS), index); + + RETURN_COPY_VALUE(value); +} + +static ZEND_NAMED_FUNCTION(zend_collection_dict_with_func) +{ + zval *index, *value; + zend_object *clone; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_ZVAL(index) + Z_PARAM_ZVAL(value) + ZEND_PARSE_PARAMETERS_END(); + + clone = zend_objects_clone_obj(Z_OBJ_P(ZEND_THIS)); + + zend_collection_add_item(clone, index, value); + + RETURN_OBJ(clone); +} + +static ZEND_NAMED_FUNCTION(zend_collection_dict_without_func) +{ + zval *index; + zend_object *clone; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ZVAL(index) + ZEND_PARSE_PARAMETERS_END(); + + clone = zend_objects_clone_obj(Z_OBJ_P(ZEND_THIS)); + + zend_collection_unset_item(clone, index); + + RETURN_OBJ(clone); +} + +static ZEND_NAMED_FUNCTION(zend_collection_dict_set_func) +{ + zval *index; + zval *value; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_ZVAL(index) + Z_PARAM_ZVAL(value) + ZEND_PARSE_PARAMETERS_END(); + + if (!key_type_allowed(Z_OBJCE_P(ZEND_THIS), index)) { + return; + } + + if (!zend_collection_has_item(Z_OBJ_P(ZEND_THIS), index)) { + switch (Z_TYPE_P(index)) { + case IS_LONG: + zend_throw_exception_ex( + spl_ce_OutOfBoundsException, 0, + "Index '%ld' does not exist in the %s dictionary", + Z_LVAL_P(index), ZSTR_VAL(Z_OBJCE_P(ZEND_THIS)->name) + ); + return; + case IS_STRING: + zend_throw_exception_ex( + spl_ce_OutOfBoundsException, 0, + "Index '%s' does not exist in the %s dictionary", + Z_STRVAL_P(index), ZSTR_VAL(Z_OBJCE_P(ZEND_THIS)->name) + ); + return; + } + } + + zend_collection_set_item(Z_OBJ_P(ZEND_THIS), index, value); + + RETURN_OBJ_COPY(Z_OBJ_P(ZEND_THIS)); +} + +static ZEND_NAMED_FUNCTION(zend_collection_dict_concat_func) +{ + zval *other; + zend_object *clone; + zend_class_entry *ce = Z_OBJCE_P(ZEND_THIS); + zval *value_prop; + zval *element; + zend_ulong int_key; + zend_string *key; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(other, zend_ce_dict_collection); + ZEND_PARSE_PARAMETERS_END(); + + clone = zend_objects_clone_obj(Z_OBJ_P(ZEND_THIS)); + + value_prop = zend_read_property_ex(ce, Z_OBJ_P(other), ZSTR_KNOWN(ZEND_STR_VALUE), true, NULL); + + ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(value_prop), int_key, key, element) { + zval zkey; + + if (key) { + ZVAL_STR(&zkey, key); + } else { + ZVAL_LONG(&zkey, int_key); + } + + if (!zend_collection_add_item(clone, &zkey, element)) { + return; + } + } ZEND_HASH_FOREACH_END(); + + RETURN_OBJ(clone); +} + +static ZEND_NAMED_FUNCTION(zend_collection_dict_equals_func) +{ + zval *other; + zval *value_prop_us, *value_prop_other; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(other, zend_ce_dict_collection); + ZEND_PARSE_PARAMETERS_END(); + + if (Z_OBJ_P(ZEND_THIS) == Z_OBJ_P(other)) { + RETURN_TRUE; + } + + value_prop_us = zend_read_property_ex(Z_OBJCE_P(ZEND_THIS), Z_OBJ_P(ZEND_THIS), ZSTR_KNOWN(ZEND_STR_VALUE), true, NULL); + value_prop_other = zend_read_property_ex(Z_OBJCE_P(other), Z_OBJ_P(other), ZSTR_KNOWN(ZEND_STR_VALUE), true, NULL); + + if (!value_prop_us || !value_prop_other) { + RETURN_FALSE; + } + + RETURN_BOOL(zend_hash_compare(Z_ARRVAL_P(value_prop_us), Z_ARRVAL_P(value_prop_other), (compare_func_t) collection_is_equal_function, false) == 0); +} + +static ZEND_NAMED_FUNCTION(zend_collection_dict_map_func) +{ + zend_object *object = Z_OBJ_P(ZEND_THIS); + zend_class_entry *ce = Z_OBJCE_P(ZEND_THIS); + zend_fcall_info fci = empty_fcall_info; + zend_fcall_info_cache fci_cache = empty_fcall_info_cache; + zval *value_prop; + zend_string *type; + zval *operand; + zend_ulong index; + zend_string *key; + zval retval; + zval args[2]; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_FUNC(fci, fci_cache) + Z_PARAM_STR(type) + ZEND_PARSE_PARAMETERS_END(); + + zend_class_entry *return_ce = zend_lookup_class(type); + if (!return_ce) { + zend_type_error( + "Type '%s' can not be fetched", + ZSTR_VAL(type) + ); + return; + } + + if (!instanceof_function(return_ce, zend_ce_dict_collection)) { + zend_type_error( + "Type '%s' must implement DictCollection interface", + ZSTR_VAL(type) + ); + return; + } + + create_array_if_needed(ce, object); + value_prop = zend_read_property_ex(ce, object, ZSTR_KNOWN(ZEND_STR_VALUE), true, NULL); + + object_init_ex(return_value, return_ce); + Z_OBJCE_P(return_value)->collection_data_structure = return_ce->collection_data_structure; + + fci.retval = &retval; + fci.param_count = 2; + + ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(value_prop), index, key, operand) { + ZVAL_COPY(&args[0], operand); + if (key != NULL) { + ZVAL_STR_COPY(&args[1], key); + } else { + ZVAL_LONG(&args[1], index); + } + + fci.params = args; + + if (zend_call_function(&fci, &fci_cache) == SUCCESS) { + bool ok = zend_collection_add_item(Z_OBJ_P(return_value), &args[1], &retval); + + zval_ptr_dtor(&args[0]); + zval_ptr_dtor(&args[1]); + + if (!ok) { + return; + } + } else { + zval_ptr_dtor(&args[0]); + zval_ptr_dtor(&args[1]); + + return; + } + + } ZEND_HASH_FOREACH_END(); +} + + +static void zend_collection_register_func(zend_class_entry *ce, zend_known_string_id name_id, zend_internal_function *zif) +{ + zend_string *name = ZSTR_KNOWN(name_id); + zif->type = ZEND_INTERNAL_FUNCTION; + zif->module = EG(current_module); + zif->scope = ce; + zif->T = ZEND_OBSERVER_ENABLED; + if (EG(active)) { // at run-time + ZEND_MAP_PTR_INIT(zif->run_time_cache, zend_arena_calloc(&CG(arena), 1, zend_internal_run_time_cache_reserved_size())); + } else { + ZEND_MAP_PTR_NEW(zif->run_time_cache); + } + + if (!zend_hash_add_ptr(&ce->function_table, name, zif)) { + zend_error_noreturn(E_COMPILE_ERROR, "Cannot redeclare %s::%s()", ZSTR_VAL(ce->name), ZSTR_VAL(name)); + } +} + +#define REGISTER_FUNCTION(name, php_handler, arginfo, argc) \ + { \ + zend_internal_function *zif_function = zend_arena_calloc(&CG(arena), sizeof(zend_internal_function), 1); \ + zif_function->handler = (php_handler); \ + zif_function->function_name = ZSTR_KNOWN((name)); \ + zif_function->fn_flags = fn_flags; \ + zif_function->num_args = (argc); \ + zif_function->required_num_args = (argc); \ + zif_function->arg_info = (zend_internal_arg_info *) ((arginfo) + 1); \ + zend_collection_register_func(ce, (name), zif_function); \ + } + + +void zend_collection_register_funcs(zend_class_entry *ce) +{ + const uint32_t fn_flags = ZEND_ACC_PUBLIC|ZEND_ACC_HAS_RETURN_TYPE|ZEND_ACC_ARENA_ALLOCATED; + + switch (ce->collection_data_structure) { + case ZEND_COLLECTION_SEQ: + REGISTER_FUNCTION(ZEND_STR_ADD, zend_collection_seq_add_func, arginfo_class_SeqCollection_add, 1); + REGISTER_FUNCTION(ZEND_STR_REMOVE, zend_collection_seq_remove_func, arginfo_class_SeqCollection_remove, 1); + REGISTER_FUNCTION(ZEND_STR_HAS, zend_collection_seq_has_func, arginfo_class_SeqCollection_has, 1); + REGISTER_FUNCTION(ZEND_STR_GET, zend_collection_seq_get_func, arginfo_class_SeqCollection_get, 1); + REGISTER_FUNCTION(ZEND_STR_WITH, zend_collection_seq_with_func, arginfo_class_SeqCollection_with, 1); + REGISTER_FUNCTION(ZEND_STR_WITHOUT, zend_collection_seq_without_func, arginfo_class_SeqCollection_without, 1); + REGISTER_FUNCTION(ZEND_STR_SET, zend_collection_seq_set_func, arginfo_class_SeqCollection_set, 2); + REGISTER_FUNCTION(ZEND_STR_CONCAT, zend_collection_seq_concat_func, arginfo_class_SeqCollection_concat, 1); + REGISTER_FUNCTION(ZEND_STR_EQUALS, zend_collection_seq_equals_func, arginfo_class_SeqCollection_equals, 1); + REGISTER_FUNCTION(ZEND_STR_MAP, zend_collection_seq_map_func, arginfo_class_SeqCollection_map, 2); + break; + case ZEND_COLLECTION_DICT: + REGISTER_FUNCTION(ZEND_STR_ADD, zend_collection_dict_add_func, arginfo_class_DictCollection_add, 2); + REGISTER_FUNCTION(ZEND_STR_REMOVE, zend_collection_dict_remove_func, arginfo_class_DictCollection_remove, 1); + REGISTER_FUNCTION(ZEND_STR_HAS, zend_collection_dict_has_func, arginfo_class_DictCollection_has, 1); + REGISTER_FUNCTION(ZEND_STR_GET, zend_collection_dict_get_func, arginfo_class_DictCollection_get, 1); + REGISTER_FUNCTION(ZEND_STR_WITH, zend_collection_dict_with_func, arginfo_class_DictCollection_with, 2); + REGISTER_FUNCTION(ZEND_STR_WITHOUT, zend_collection_dict_without_func, arginfo_class_DictCollection_without, 1); + REGISTER_FUNCTION(ZEND_STR_SET, zend_collection_dict_set_func, arginfo_class_DictCollection_set, 2); + REGISTER_FUNCTION(ZEND_STR_CONCAT, zend_collection_dict_concat_func, arginfo_class_DictCollection_concat, 1); + REGISTER_FUNCTION(ZEND_STR_EQUALS, zend_collection_dict_equals_func, arginfo_class_DictCollection_equals, 1); + REGISTER_FUNCTION(ZEND_STR_MAP, zend_collection_dict_map_func, arginfo_class_DictCollection_map, 2); + break; + } +} + +void zend_collection_register_props(zend_class_entry *ce) +{ + ce->ce_flags |= ZEND_ACC_NO_DYNAMIC_PROPERTIES; + + zval name_default_value; + ZVAL_UNDEF(&name_default_value); + zend_type name_type = ZEND_TYPE_INIT_CODE(IS_ARRAY, 0, 0); + zend_declare_typed_property(ce, ZSTR_KNOWN(ZEND_STR_VALUE), &name_default_value, ZEND_ACC_PUBLIC | ZEND_ACC_READONLY, NULL, name_type); +} + +static void create_array_if_needed(zend_class_entry *ce, zend_object *object) +{ + zval *value_prop = zend_read_property_ex(ce, object, ZSTR_KNOWN(ZEND_STR_VALUE), true, NULL); + + if (Z_TYPE_P(value_prop) == IS_ARRAY) { + return; + } + + zval new_array; + + array_init(&new_array); + + zend_update_property_ex(ce, object, ZSTR_KNOWN(ZEND_STR_VALUE), &new_array); + + zval_ptr_dtor(&new_array); +} + + +static void seq_add_item(zend_object *object, zval *value) +{ + zend_class_entry *ce = object->ce; + zval rv; + zval *value_prop; + + create_array_if_needed(ce, object); + value_prop = zend_read_property_ex(ce, object, ZSTR_KNOWN(ZEND_STR_VALUE), true, &rv); + SEPARATE_ARRAY(value_prop); + zval_add_ref(value); + add_next_index_zval(value_prop, value); +} + +static void seq_set_item(zend_object *object, zend_long index, zval *value) +{ + zend_class_entry *ce = object->ce; + zval rv; + zval *value_prop; + + create_array_if_needed(ce, object); + value_prop = zend_read_property_ex(ce, object, ZSTR_KNOWN(ZEND_STR_VALUE), true, &rv); + SEPARATE_ARRAY(value_prop); + zval_add_ref(value); + add_index_zval(value_prop, index, value); +} + +static void dict_add_or_set_item(zend_object *object, zval *offset, zval *value) +{ + zend_class_entry *ce = object->ce; + zval rv; + zval *value_prop; + + if (!key_type_allowed(ce, offset)) { + return; + } + + switch (ce->collection_key_type) { + case IS_LONG: { + create_array_if_needed(ce, object); + value_prop = zend_read_property_ex(ce, object, ZSTR_KNOWN(ZEND_STR_VALUE), true, &rv); + SEPARATE_ARRAY(value_prop); + zval_add_ref(value); + add_index_zval(value_prop, Z_LVAL_P(offset), value); + break; + } + case IS_STRING: { + create_array_if_needed(ce, object); + value_prop = zend_read_property_ex(ce, object, ZSTR_KNOWN(ZEND_STR_VALUE), true, &rv); + SEPARATE_ARRAY(value_prop); + zval_add_ref(value); + add_assoc_zval_ex(value_prop, Z_STRVAL_P(offset), Z_STRLEN_P(offset), value); + break; + } + } +} + + +bool zend_collection_add_item(zend_object *object, zval *offset, zval *value) +{ + zend_class_entry *ce = object->ce; + ZEND_ASSERT(ce->ce_flags & ZEND_ACC_COLLECTION); + + if (offset && ce->collection_data_structure == ZEND_COLLECTION_SEQ) { + zend_value_error("Specifying an offset for sequence collections is not allowed"); + return false; + } + + if (!offset && ce->collection_data_structure == ZEND_COLLECTION_DICT) { + zend_value_error("Specifying an offset for dictionary collections is required"); + return false; + } + + if (!zend_check_type(&ce->collection_item_type, value, NULL, ce, true, true)) { + zend_string *type_str = zend_type_to_string(ce->collection_item_type); + zend_type_error( + "Value type %s does not match %s collection item type %s", + ZSTR_VAL(ce->name), + zend_zval_type_name(value), + ZSTR_VAL(type_str) + ); + zend_string_release(type_str); + return false; + } + + switch (ce->collection_data_structure) { + case ZEND_COLLECTION_SEQ: + seq_add_item(object, value); + break; + case ZEND_COLLECTION_DICT: + dict_add_or_set_item(object, offset, value); + break; + } + + return true; +} + +void zend_collection_set_item(zend_object *object, zval *offset, zval *value) +{ + zend_class_entry *ce = object->ce; + ZEND_ASSERT(ce->ce_flags & ZEND_ACC_COLLECTION); + + if (Z_TYPE_P(offset) != IS_LONG && ce->collection_data_structure == ZEND_COLLECTION_SEQ) { + zend_value_error("Specifying a non-integer offset for sequence collections is not allowed"); + return; + } + + if (!((Z_TYPE_P(offset) == IS_LONG || Z_TYPE_P(offset) == IS_STRING)) && ce->collection_data_structure == ZEND_COLLECTION_DICT) { + zend_value_error("Specifying a non-integer/non-string offset for sequence collections is not allowed"); + return; + } + + if (!zend_check_type(&ce->collection_item_type, value, NULL, ce, 0, false)) { + zend_string *type_str = zend_type_to_string(ce->collection_item_type); + zend_type_error( + "Value type %s does not match %s collection item type %s", + ZSTR_VAL(ce->name), + zend_zval_type_name(value), + ZSTR_VAL(type_str) + ); + zend_string_release(type_str); + return; + } + + + + switch (ce->collection_data_structure) { + case ZEND_COLLECTION_SEQ: + seq_set_item(object, Z_LVAL_P(offset), value); + break; + case ZEND_COLLECTION_DICT: + dict_add_or_set_item(object, offset, value); + break; + } +} + +int zend_collection_has_item(zend_object *object, zval *offset) +{ + zval rv; + zval *value_prop; + zend_class_entry *ce = object->ce; + + if (!key_type_allowed(ce, offset)) { + return false; + } + + value_prop = zend_read_property_ex(ce, object, ZSTR_KNOWN(ZEND_STR_VALUE), true, &rv); + + if (Z_TYPE_P(offset) == IS_STRING) { + return zend_hash_find(HASH_OF(value_prop), Z_STR_P(offset)) != NULL; + } else { + return zend_hash_index_find(HASH_OF(value_prop), Z_LVAL_P(offset)) != NULL; + } + + return false; +} + +zval *zend_collection_read_item(zend_object *object, zval *offset) +{ + zval rv; + zval *value_prop, *value; + zend_class_entry *ce = object->ce; + + if (!key_type_allowed(ce, offset)) { + return NULL; + } + + value_prop = zend_read_property_ex(ce, object, ZSTR_KNOWN(ZEND_STR_VALUE), true, &rv); + + if (Z_TYPE_P(offset) == IS_STRING) { + value = zend_hash_find(HASH_OF(value_prop), Z_STR_P(offset)); + } else { + value = zend_hash_index_find(HASH_OF(value_prop), Z_LVAL_P(offset)); + } + + return value; +} + +void zend_collection_unset_item(zend_object *object, zval *offset) +{ + zval rv; + zval *value_prop; + zend_class_entry *ce = object->ce; + + if (!key_type_allowed(ce, offset)) { + return; + } + + value_prop = zend_read_property_ex(ce, object, ZSTR_KNOWN(ZEND_STR_VALUE), true, &rv); + SEPARATE_ARRAY(value_prop); + + // TODO: Should unset throw when an item with key 'offset' does not exist? + + if (Z_TYPE_P(offset) == IS_STRING) { + zend_hash_del(HASH_OF(value_prop), Z_STR_P(offset)); + } else { + zend_array *new_array; + zval new_zval; + + zend_hash_index_del(HASH_OF(value_prop), Z_LVAL_P(offset)); + + if (!HT_IS_WITHOUT_HOLES(HASH_OF(value_prop))) { + new_array = zend_array_to_list(HASH_OF(value_prop)); + ZVAL_ARR(&new_zval, new_array); + Z_PROP_FLAG_P(value_prop) |= IS_PROP_REINITABLE; + zend_update_property_ex(ce, object, ZSTR_KNOWN(ZEND_STR_VALUE), &new_zval); + Z_PROP_FLAG_P(value_prop) &= IS_PROP_REINITABLE; + } + } +} diff --git a/Zend/zend_collection.h b/Zend/zend_collection.h new file mode 100644 index 0000000000000..26100fb198ea2 --- /dev/null +++ b/Zend/zend_collection.h @@ -0,0 +1,48 @@ +/* + +----------------------------------------------------------------------+ + | Zend Engine | + +----------------------------------------------------------------------+ + | Copyright (c) Zend Technologies Ltd. (http://www.zend.com) | + +----------------------------------------------------------------------+ + | This source file is subject to version 2.00 of the Zend license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | http://www.zend.com/license/2_00.txt. | + | If you did not receive a copy of the Zend license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@zend.com so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: Derick Rethans | + +----------------------------------------------------------------------+ +*/ + +#ifndef ZEND_COLLECTION_H +#define ZEND_COLLECTION_H + +#include "zend.h" + +#include + +BEGIN_EXTERN_C() + +extern ZEND_API zend_class_entry *zend_ce_seq_collection; +extern ZEND_API zend_class_entry *zend_ce_dict_collection; + +extern ZEND_API zend_object_handlers zend_seq_collection_object_handlers; +extern ZEND_API zend_object_handlers zend_dict_collection_object_handlers; + +void zend_register_collection_ce(void); +void zend_collection_add_interfaces(zend_class_entry *ce); + +void zend_collection_register_handlers(zend_class_entry *ce); +void zend_collection_register_props(zend_class_entry *ce); +void zend_collection_register_funcs(zend_class_entry *ce); + +void zend_collection_add_item(zend_object *object, zval *offset, zval *value); +int zend_collection_has_item(zend_object *object, zval *offset); +zval *zend_collection_read_item(zend_object *object, zval *offset); +void zend_collection_unset_item(zend_object *object, zval *offset); + +END_EXTERN_C() + +#endif /* ZEND_COLLECTION_H */ diff --git a/Zend/zend_collection.stub.php b/Zend/zend_collection.stub.php new file mode 100644 index 0000000000000..28361b0c0a90e --- /dev/null +++ b/Zend/zend_collection.stub.php @@ -0,0 +1,67 @@ +attributes = NULL; ce->enum_backing_type = IS_UNDEF; ce->backed_enum_table = NULL; + ce->collection_key_type = IS_UNDEF; if (nullify_handlers) { ce->constructor = NULL; @@ -8946,12 +8948,73 @@ static void zend_compile_enum_backing_type(zend_class_entry *ce, zend_ast *enum_ zend_type_release(type, 0); } +static void zend_compile_collection_data_structure(zend_class_entry *ce, zend_ast *collection_data_structure_ast) +{ + zval *struct_type = zend_ast_get_zval(collection_data_structure_ast); + + ZEND_ASSERT(Z_TYPE_P(struct_type) == IS_LONG); + ZEND_ASSERT(Z_LVAL_P(struct_type) == ZEND_COLLECTION_SEQ || Z_LVAL_P(struct_type) == ZEND_COLLECTION_DICT); + + switch (Z_LVAL_P(struct_type)) { + case ZEND_COLLECTION_SEQ: + if (ce->collection_key_type != IS_UNDEF) { + zend_error_noreturn(E_COMPILE_ERROR, "Collection sequences may not have a key type defined"); + } + ce->collection_key_type = IS_LONG; + ce->collection_data_structure = ZEND_COLLECTION_SEQ; + break; + + case ZEND_COLLECTION_DICT: + if (ce->collection_key_type == IS_UNDEF) { + zend_error_noreturn(E_COMPILE_ERROR, "Collection dictionaries must have a key type defined"); + } + ce->collection_data_structure = ZEND_COLLECTION_DICT; + break; + } +} + +static void zend_compile_collection_key_type(zend_class_entry *ce, zend_ast *collection_key_type_ast) +{ + ZEND_ASSERT(ce->ce_flags & ZEND_ACC_COLLECTION); + zend_type type = zend_compile_typename(collection_key_type_ast); + uint32_t type_mask = ZEND_TYPE_PURE_MASK(type); + if (ZEND_TYPE_IS_COMPLEX(type) || (type_mask != MAY_BE_LONG && type_mask != MAY_BE_STRING)) { + zend_string *type_string = zend_type_to_string(type); + zend_error_noreturn(E_COMPILE_ERROR, + "Collection key type must be int or string, %s given", + ZSTR_VAL(type_string)); + } + if (type_mask == MAY_BE_LONG) { + ce->collection_key_type = IS_LONG; + } else { + ZEND_ASSERT(type_mask == MAY_BE_STRING); + ce->collection_key_type = IS_STRING; + } + zend_type_release(type, 0); +} + +static void zend_compile_collection_item_type(zend_class_entry *ce, zend_ast *collection_item_type_ast) +{ + ZEND_ASSERT(ce->ce_flags & ZEND_ACC_COLLECTION); + zend_type type = zend_compile_typename(collection_item_type_ast); + + if (ZEND_TYPE_FULL_MASK(type) & (MAY_BE_VOID|MAY_BE_NEVER|MAY_BE_CALLABLE)) { + zend_string *str = zend_type_to_string(type); + zend_error_noreturn(E_COMPILE_ERROR, + "Collection item type cannot have type %s", ZSTR_VAL(str)); + } + + ce->collection_item_type = type; +} + static void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel) /* {{{ */ { zend_ast_decl *decl = (zend_ast_decl *) ast; zend_ast *extends_ast = decl->child[0]; zend_ast *implements_ast = decl->child[1]; + zend_ast *collection_data_structure_ast = decl->child[1]; zend_ast *stmt_ast = decl->child[2]; + zend_ast *collection_types_list_ast = decl->child[4]; zend_ast *enum_backing_type_ast = decl->child[4]; zend_string *name, *lcname; zend_class_entry *ce = zend_arena_alloc(&CG(arena), sizeof(zend_class_entry)); @@ -9032,7 +9095,7 @@ static void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel) zend_compile_attributes(&ce->attributes, decl->child[3], 0, ZEND_ATTRIBUTE_TARGET_CLASS, 0); } - if (implements_ast) { + if (implements_ast && !(ce->ce_flags & ZEND_ACC_COLLECTION)) { zend_compile_implements(implements_ast); } @@ -9044,6 +9107,29 @@ static void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel) zend_enum_register_props(ce); } + if (ce->ce_flags & ZEND_ACC_COLLECTION) { + zend_ast_list *list = zend_ast_get_list(collection_types_list_ast); + + switch (list->children) { + case 1: + zend_compile_collection_item_type(ce, list->child[0]); + break; + case 2: + zend_compile_collection_key_type(ce, list->child[0]); + zend_compile_collection_item_type(ce, list->child[1]); + break; + default: + zend_error_noreturn(E_COMPILE_ERROR, "Invalid collection data types signature"); + break; + } + + /* The data structure is compiled after the key/value types so it can do heuristics */ + zend_compile_collection_data_structure(ce, collection_data_structure_ast); + + zend_collection_add_interfaces(ce); + zend_collection_register_props(ce); + } + zend_compile_stmt(stmt_ast); /* Reset lineno for final opcodes and errors */ diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index c7e31877b5cd2..05622569075d3 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -262,7 +262,7 @@ typedef struct _zend_oparray_context { /* Virtual property without backing storage | | | */ #define ZEND_ACC_VIRTUAL (1 << 9) /* | | X | */ /* | | | */ -/* Class Flags (unused: 30,31) | | | */ +/* Class Flags (unused: 31) | | | */ /* =========== | | | */ /* | | | */ /* Special class types | | | */ @@ -270,6 +270,7 @@ typedef struct _zend_oparray_context { #define ZEND_ACC_TRAIT (1 << 1) /* X | | | */ #define ZEND_ACC_ANON_CLASS (1 << 2) /* X | | | */ #define ZEND_ACC_ENUM (1 << 28) /* X | | | */ +#define ZEND_ACC_COLLECTION (1 << 30) /* X | | | */ /* | | | */ /* Class linked with parent, interfaces and traits | | | */ #define ZEND_ACC_LINKED (1 << 3) /* X | | | */ diff --git a/Zend/zend_default_classes.c b/Zend/zend_default_classes.c index 7ab9b8325a39a..31be93ade90c1 100644 --- a/Zend/zend_default_classes.c +++ b/Zend/zend_default_classes.c @@ -28,6 +28,7 @@ #include "zend_weakrefs.h" #include "zend_enum.h" #include "zend_fibers.h" +#include "zend_collection.h" ZEND_API void zend_register_default_classes(void) { @@ -40,4 +41,5 @@ ZEND_API void zend_register_default_classes(void) zend_register_attribute_ce(); zend_register_enum_ce(); zend_register_fiber_ce(); + zend_register_collection_ce(); } diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 602bb3b0e79f5..e7a95a6802884 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -1190,7 +1190,7 @@ static zend_always_inline bool zend_check_type_slow( * because this case is already checked at compile-time. */ } -static zend_always_inline bool zend_check_type( +zend_always_inline bool zend_check_type( zend_type *type, zval *arg, void **cache_slot, zend_class_entry *scope, bool is_return_type, bool is_internal) { diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h index fe854f305150c..8a0a40a768338 100644 --- a/Zend/zend_execute.h +++ b/Zend/zend_execute.h @@ -93,6 +93,7 @@ ZEND_API ZEND_COLD void ZEND_FASTCALL zend_object_released_while_assigning_to_pr ZEND_API ZEND_COLD void ZEND_FASTCALL zend_cannot_add_element(void); ZEND_API bool zend_verify_scalar_type_hint(uint32_t type_mask, zval *arg, bool strict, bool is_internal_arg); +ZEND_API bool zend_check_type(zend_type *type, zval *arg, void **cache_slot, zend_class_entry *scope, bool is_return_type, bool is_internal); ZEND_API ZEND_COLD void zend_verify_arg_error( const zend_function *zf, const zend_arg_info *arg_info, uint32_t arg_num, zval *value); ZEND_API ZEND_COLD void zend_verify_return_error( diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index b43544766b848..10f3adbc5d60f 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -30,6 +30,7 @@ #include "zend_attributes.h" #include "zend_constants.h" #include "zend_observer.h" +#include "zend_collection.h" ZEND_API zend_class_entry* (*zend_inheritance_cache_get)(zend_class_entry *ce, zend_class_entry *parent, zend_class_entry **traits_and_interfaces) = NULL; ZEND_API zend_class_entry* (*zend_inheritance_cache_add)(zend_class_entry *ce, zend_class_entry *proto, zend_class_entry *parent, zend_class_entry **traits_and_interfaces, HashTable *dependencies) = NULL; @@ -3508,6 +3509,12 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string zend_enum_register_funcs(ce); } + if (ce->ce_flags & ZEND_ACC_COLLECTION) { + /* Only register builtin collection methods during inheritance to avoid persisting them + * in opcache. */ + zend_collection_register_funcs(ce); + } + if (parent) { if (!(parent->ce_flags & ZEND_ACC_LINKED)) { add_dependency_obligation(ce, parent); diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index 5423d40185766..5c696f2dbe92f 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -164,6 +164,8 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %token T_TRAIT "'trait'" %token T_INTERFACE "'interface'" %token T_ENUM "'enum'" +%token T_COLLECTION_SEQ "'collection(Seq)'" +%token T_COLLECTION_DICT "'collection(Dict)'" %token T_EXTENDS "'extends'" %token T_IMPLEMENTS "'implements'" %token T_NAMESPACE "'namespace'" @@ -279,6 +281,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %type attribute_decl attribute attributes attribute_group namespace_declaration_name %type match match_arm_list non_empty_match_arm_list match_arm match_arm_cond_list %type enum_declaration_statement enum_backing_type enum_case enum_case_expr +%type collection_declaration_statement collection_type_list %type function_name non_empty_member_modifiers %type property_hook property_hook_list optional_property_hook_list hooked_property property_hook_body %type optional_parameter_list @@ -387,6 +390,7 @@ attributed_statement: | trait_declaration_statement { $$ = $1; } | interface_declaration_statement { $$ = $1; } | enum_declaration_statement { $$ = $1; } + | collection_declaration_statement { $$ = $1; } ; top_statement: @@ -656,6 +660,20 @@ enum_case_expr: | '=' expr { $$ = $2; } ; +collection_declaration_statement: + T_COLLECTION_SEQ { $$ = CG(zend_lineno); } + T_STRING '<' collection_type_list '>' backup_doc_comment '{' class_statement_list '}' + { $$ = zend_ast_create_decl(ZEND_AST_CLASS, ZEND_ACC_COLLECTION|ZEND_ACC_FINAL, $2, $7, zend_ast_get_str($3), NULL, zend_ast_create_zval_from_long(ZEND_COLLECTION_SEQ), $9, NULL, $5); } + | T_COLLECTION_DICT { $$ = CG(zend_lineno); } + T_STRING '<' collection_type_list '>' backup_doc_comment '{' class_statement_list '}' + { $$ = zend_ast_create_decl(ZEND_AST_CLASS, ZEND_ACC_COLLECTION|ZEND_ACC_FINAL, $2, $7, zend_ast_get_str($3), NULL, zend_ast_create_zval_from_long(ZEND_COLLECTION_DICT), $9, NULL, $5); } +; + +collection_type_list: + collection_type_list T_DOUBLE_ARROW type_expr { $$ = zend_ast_list_add($1, $3); } + | type_expr { $$ = zend_ast_create_list(1, ZEND_AST_NAME_LIST, $1); } +; + extends_from: %empty { $$ = NULL; } | T_EXTENDS class_name { $$ = $2; } diff --git a/Zend/zend_language_scanner.l b/Zend/zend_language_scanner.l index 4551d26a17e79..303f3511dfd91 100644 --- a/Zend/zend_language_scanner.l +++ b/Zend/zend_language_scanner.l @@ -1562,6 +1562,15 @@ OPTIONAL_WHITESPACE_OR_COMMENTS ({WHITESPACE}|{MULTI_LINE_COMMENT}|{SINGLE_LINE_ RETURN_TOKEN_WITH_IDENT(T_ENUM); } +"collection(Seq)" { + RETURN_TOKEN_WITH_IDENT(T_COLLECTION_SEQ); +} + +"collection(Dict)" { + RETURN_TOKEN_WITH_IDENT(T_COLLECTION_DICT); +} + + "extends" { RETURN_TOKEN_WITH_IDENT(T_EXTENDS); } diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c index 4c096d26b1b7b..7abd5f1bc9ac7 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -26,6 +26,7 @@ #include "zend_objects_API.h" #include "zend_object_handlers.h" #include "zend_interfaces.h" +#include "zend_collection.h" #include "zend_exceptions.h" #include "zend_closures.h" #include "zend_compile.h" @@ -1182,6 +1183,10 @@ ZEND_API zval *zend_std_read_dimension(zend_object *object, zval *offset, int ty return NULL; } return rv; + } else if (zend_class_implements_interface(ce, zend_ce_seq_collection)) { + return zend_collection_read_item(object, offset); + } else if (zend_class_implements_interface(ce, zend_ce_dict_collection)) { + return zend_collection_read_item(object, offset); } else { zend_bad_array_access(ce); return NULL; @@ -1205,6 +1210,10 @@ ZEND_API void zend_std_write_dimension(zend_object *object, zval *offset, zval * zend_call_known_instance_method_with_2_params(funcs->zf_offsetset, object, NULL, &tmp_offset, value); OBJ_RELEASE(object); zval_ptr_dtor(&tmp_offset); + } else if (zend_class_implements_interface(ce, zend_ce_seq_collection)) { + zend_collection_add_item(object, offset, value); + } else if (zend_class_implements_interface(ce, zend_ce_dict_collection)) { + zend_collection_add_item(object, offset, value); } else { zend_bad_array_access(ce); } @@ -1232,6 +1241,10 @@ ZEND_API int zend_std_has_dimension(zend_object *object, zval *offset, int check } OBJ_RELEASE(object); zval_ptr_dtor(&tmp_offset); + } else if (zend_class_implements_interface(ce, zend_ce_seq_collection)) { + return zend_collection_has_item(object, offset); + } else if (zend_class_implements_interface(ce, zend_ce_dict_collection)) { + return zend_collection_has_item(object, offset); } else { zend_bad_array_access(ce); return 0; @@ -1418,6 +1431,10 @@ ZEND_API void zend_std_unset_dimension(zend_object *object, zval *offset) /* {{{ zend_call_known_instance_method_with_1_params(funcs->zf_offsetunset, object, NULL, &tmp_offset); OBJ_RELEASE(object); zval_ptr_dtor(&tmp_offset); + } else if (zend_class_implements_interface(ce, zend_ce_seq_collection)) { + zend_collection_unset_item(object, offset); + } else if (zend_class_implements_interface(ce, zend_ce_dict_collection)) { + zend_collection_unset_item(object, offset); } else { zend_bad_array_access(ce); } diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index 0d1d8b6bf528f..85fc5b57ac986 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -439,6 +439,9 @@ ZEND_API void destroy_zend_class(zval *zv) if (ce->backed_enum_table) { zend_hash_release(ce->backed_enum_table); } + if (ZEND_TYPE_IS_SET(ce->collection_item_type)) { + zend_type_release(ce->collection_item_type, 0); + } if (ce->default_properties_table) { zval *p = ce->default_properties_table; zval *end = p + ce->default_properties_count; diff --git a/Zend/zend_string.h b/Zend/zend_string.h index ad6c5e1ee38bf..aed9a134c94c7 100644 --- a/Zend/zend_string.h +++ b/Zend/zend_string.h @@ -633,6 +633,16 @@ EMPTY_SWITCH_DEFAULT_CASE() _(ZEND_STR_AUTOGLOBAL_REQUEST, "_REQUEST") \ _(ZEND_STR_COUNT, "count") \ _(ZEND_STR_SENSITIVEPARAMETER, "SensitiveParameter") \ + _(ZEND_STR_ADD, "add") \ + _(ZEND_STR_REMOVE, "remove") \ + _(ZEND_STR_HAS, "has") \ + _(ZEND_STR_GET, "get") \ + _(ZEND_STR_SET, "set") \ + _(ZEND_STR_WITH, "with") \ + _(ZEND_STR_WITHOUT, "without") \ + _(ZEND_STR_CONCAT, "concat") \ + _(ZEND_STR_EQUALS, "equals") \ + _(ZEND_STR_MAP, "map") \ _(ZEND_STR_CONST_EXPR_PLACEHOLDER, "[constant expression]") \ _(ZEND_STR_DEPRECATED, "Deprecated") \ _(ZEND_STR_SINCE, "since") \ diff --git a/configure.ac b/configure.ac index 0abaf4a4f185b..701a31b2ab96a 100644 --- a/configure.ac +++ b/configure.ac @@ -1714,6 +1714,7 @@ PHP_ADD_SOURCES([Zend], m4_normalize([ zend_builtin_functions.c zend_call_stack.c zend_closures.c + zend_collection.c zend_compile.c zend_constants.c zend_cpuinfo.c diff --git a/ext/tokenizer/tokenizer_data.c b/ext/tokenizer/tokenizer_data.c index cdaaddecd7bfa..988f299ff03f9 100644 --- a/ext/tokenizer/tokenizer_data.c +++ b/ext/tokenizer/tokenizer_data.c @@ -102,6 +102,8 @@ char *get_token_type_name(int token_type) case T_TRAIT: return "T_TRAIT"; case T_INTERFACE: return "T_INTERFACE"; case T_ENUM: return "T_ENUM"; + case T_COLLECTION_SEQ: return "T_COLLECTION_SEQ"; + case T_COLLECTION_DICT: return "T_COLLECTION_DICT"; case T_EXTENDS: return "T_EXTENDS"; case T_IMPLEMENTS: return "T_IMPLEMENTS"; case T_NAMESPACE: return "T_NAMESPACE"; diff --git a/ext/tokenizer/tokenizer_data.stub.php b/ext/tokenizer/tokenizer_data.stub.php index 81e4e92626f37..07e0b78e42db1 100644 --- a/ext/tokenizer/tokenizer_data.stub.php +++ b/ext/tokenizer/tokenizer_data.stub.php @@ -387,6 +387,16 @@ * @cvalue T_ENUM */ const T_ENUM = UNKNOWN; +/** + * @var int + * @cvalue T_COLLECTION_SEQ + */ +const T_COLLECTION_SEQ = UNKNOWN; +/** + * @var int + * @cvalue T_COLLECTION_DICT + */ +const T_COLLECTION_DICT = UNKNOWN; /** * @var int * @cvalue T_EXTENDS diff --git a/ext/tokenizer/tokenizer_data_arginfo.h b/ext/tokenizer/tokenizer_data_arginfo.h index 0fc1f219619dd..ea1bab01cbd68 100644 --- a/ext/tokenizer/tokenizer_data_arginfo.h +++ b/ext/tokenizer/tokenizer_data_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: b7a13b3b242bd535f62a7d819eb0df751efb54ed */ + * Stub hash: 59da19fc9926e153c9fc6d99ec0a7c5feb744566 */ static void register_tokenizer_data_symbols(int module_number) { @@ -80,6 +80,8 @@ static void register_tokenizer_data_symbols(int module_number) REGISTER_LONG_CONSTANT("T_TRAIT", T_TRAIT, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_INTERFACE", T_INTERFACE, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_ENUM", T_ENUM, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("T_COLLECTION_SEQ", T_COLLECTION_SEQ, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("T_COLLECTION_DICT", T_COLLECTION_DICT, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_EXTENDS", T_EXTENDS, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_IMPLEMENTS", T_IMPLEMENTS, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("T_NAMESPACE", T_NAMESPACE, CONST_PERSISTENT); diff --git a/win32/build/config.w32 b/win32/build/config.w32 index c4a617a2e16ec..4440bf5299363 100644 --- a/win32/build/config.w32 +++ b/win32/build/config.w32 @@ -240,7 +240,7 @@ ADD_SOURCES("Zend", "zend_language_parser.c zend_language_scanner.c \ zend_default_classes.c zend_execute.c zend_strtod.c zend_gc.c zend_closures.c zend_weakrefs.c \ zend_float.c zend_string.c zend_generators.c zend_virtual_cwd.c zend_ast.c \ zend_inheritance.c zend_smart_str.c zend_cpuinfo.c zend_observer.c zend_system_id.c \ - zend_enum.c zend_fibers.c zend_atomic.c zend_hrtime.c zend_frameless_function.c zend_property_hooks.c"); + zend_collection.c zend_enum.c zend_fibers.c zend_atomic.c zend_hrtime.c zend_frameless_function.c zend_property_hooks.c"); ADD_SOURCES("Zend\\Optimizer", "zend_optimizer.c pass1.c pass3.c optimize_func_calls.c block_pass.c optimize_temp_vars_5.c nop_removal.c compact_literals.c zend_cfg.c zend_dfg.c dfa_pass.c zend_ssa.c zend_inference.c zend_func_info.c zend_call_graph.c zend_dump.c escape_analysis.c compact_vars.c dce.c sccp.c scdf.c"); var PHP_ASSEMBLER = PATH_PROG({