From af86f63feeb0325c5e223822a7f8c7af2d949a68 Mon Sep 17 00:00:00 2001 From: Maxim Smakouz Date: Mon, 22 Nov 2021 18:28:59 +0200 Subject: [PATCH 1/5] Add `ORM::with`. Marked other `with*` methods as deprecated (#257) --- src/ORM.php | 38 ++++++++++++++++------- tests/ORM/ORMTest.php | 26 ++++++++++++++++ tests/ORM/RelationWithColumnAliasTest.php | 1 - 3 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/ORM.php b/src/ORM.php index 44afc4bf8..73bb0f7ce 100644 --- a/src/ORM.php +++ b/src/ORM.php @@ -180,14 +180,12 @@ public function make(string $role, array $data = [], int $node = Node::NEW) } /** - * @inheritdoc + * @deprecated since Cycle ORM v1.8, this method will be removed in future releases. + * Use method {@see with} instead. */ public function withFactory(FactoryInterface $factory): ORMInterface { - $orm = clone $this; - $orm->factory = $factory; - - return $orm; + return $this->with(null, $factory); } /** @@ -199,14 +197,12 @@ public function getFactory(): FactoryInterface } /** - * @inheritdoc + * @deprecated since Cycle ORM v1.8, this method will be removed in future releases. + * Use method {@see with} instead. */ public function withSchema(SchemaInterface $schema): ORMInterface { - $orm = clone $this; - $orm->schema = $schema; - - return $orm; + return $this->with($schema); } /** @@ -222,12 +218,30 @@ public function getSchema(): SchemaInterface } /** - * @inheritdoc + * @deprecated since Cycle ORM v1.8, this method will be removed in future releases. + * Use method {@see with} instead. */ public function withHeap(HeapInterface $heap): ORMInterface { + return $this->with(null, null, $heap); + } + + public function with( + ?SchemaInterface $schema = null, + ?FactoryInterface $factory = null, + ?HeapInterface $heap = null + ): ORMInterface { $orm = clone $this; - $orm->heap = $heap; + + if ($schema !== null) { + $orm->schema = $schema; + } + if ($factory !== null) { + $orm->factory = $factory; + } + if ($heap !== null) { + $orm->heap = $heap; + } return $orm; } diff --git a/tests/ORM/ORMTest.php b/tests/ORM/ORMTest.php index 262929cc9..b18134df8 100644 --- a/tests/ORM/ORMTest.php +++ b/tests/ORM/ORMTest.php @@ -11,6 +11,8 @@ namespace Cycle\ORM\Tests; +use Cycle\ORM\Factory; +use Cycle\ORM\Heap\Heap; use Cycle\ORM\Mapper\Mapper; use Cycle\ORM\Schema; use Cycle\ORM\Tests\Fixtures\User; @@ -70,6 +72,30 @@ public function testORMClone(): void $this->assertNotSame($orm, $this->orm); } + public function testORMCloneWithSchema(): void + { + $orm = $this->orm->with(new Schema([])); + + $this->assertNotSame($orm, $this->orm); + $this->assertNotSame($orm->getSchema(), $this->orm->getSchema()); + } + + public function testORMCloneWithFactory(): void + { + $orm = $this->orm->with(null, new Factory($this->dbal)); + + $this->assertNotSame($orm, $this->orm); + $this->assertNotSame($orm->getFactory(), $this->orm->getFactory()); + } + + public function testORMCloneWithHeap(): void + { + $orm = $this->orm->with(null, null, new Heap()); + + $this->assertNotSame($orm, $this->orm); + $this->assertNotSame($orm->getHeap(), $this->orm->getHeap()); + } + public function testORMGetByRole(): void { $this->assertNull($this->orm->get('user', ['id' => 1], false)); diff --git a/tests/ORM/RelationWithColumnAliasTest.php b/tests/ORM/RelationWithColumnAliasTest.php index e90d3ac1c..15dc9c510 100644 --- a/tests/ORM/RelationWithColumnAliasTest.php +++ b/tests/ORM/RelationWithColumnAliasTest.php @@ -8,7 +8,6 @@ */ declare(strict_types=1); -declare(strict_types=1); namespace Cycle\ORM\Tests; From 842bce3af605b93f4ec1d343bb47ca22fe282cb9 Mon Sep 17 00:00:00 2001 From: Pavel Buchnev Date: Sun, 28 Nov 2021 11:12:41 +0300 Subject: [PATCH 2/5] Adds test for transaction that checks heap leaks after rollbacks (#264) --- .github/workflows/ci-mysql.yml | 4 +- .github/workflows/ci-pgsql.yml | 4 +- .github/workflows/main.yml | 11 +- tests/ORM/Driver/MySQL/TransactionTest.php | 10 + tests/ORM/Driver/Postgres/TransactionTest.php | 10 + .../ORM/Driver/SQLServer/TransactionTest.php | 10 + tests/ORM/Driver/SQLite/TransactionTest.php | 10 + tests/ORM/Fixtures/TransactionTestMapper.php | 45 ++++ tests/ORM/TransactionTest.php | 238 ++++++++++++++++++ tests/generate.php | 7 +- 10 files changed, 334 insertions(+), 15 deletions(-) create mode 100644 tests/ORM/Driver/MySQL/TransactionTest.php create mode 100644 tests/ORM/Driver/Postgres/TransactionTest.php create mode 100644 tests/ORM/Driver/SQLServer/TransactionTest.php create mode 100644 tests/ORM/Driver/SQLite/TransactionTest.php create mode 100644 tests/ORM/Fixtures/TransactionTestMapper.php create mode 100644 tests/ORM/TransactionTest.php diff --git a/.github/workflows/ci-mysql.yml b/.github/workflows/ci-mysql.yml index 4379f436e..1a78c5691 100644 --- a/.github/workflows/ci-mysql.yml +++ b/.github/workflows/ci-mysql.yml @@ -51,7 +51,7 @@ jobs: key: ${{ env.key }} - name: Cache extensions - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ${{ steps.cache-env.outputs.dir }} key: ${{ steps.cache-env.outputs.key }} @@ -70,7 +70,7 @@ jobs: run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV - name: Cache dependencies installed with composer - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ${{ env.COMPOSER_CACHE_DIR }} key: php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('**/composer.json') }} diff --git a/.github/workflows/ci-pgsql.yml b/.github/workflows/ci-pgsql.yml index c443bfc4e..fb8c1027b 100644 --- a/.github/workflows/ci-pgsql.yml +++ b/.github/workflows/ci-pgsql.yml @@ -54,7 +54,7 @@ jobs: key: ${{ env.key }} - name: Cache extensions - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ${{ steps.cache-env.outputs.dir }} key: ${{ steps.cache-env.outputs.key }} @@ -73,7 +73,7 @@ jobs: run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV - name: Cache dependencies installed with composer - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ${{ env.COMPOSER_CACHE_DIR }} key: php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('**/composer.json') }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0c6d8bb3a..5bf11cc76 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,7 +13,7 @@ jobs: id: composer-cache run: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Restore Composer Cache - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -23,10 +23,11 @@ jobs: - name: Check CS run: vendor/bin/spiral-cs check src tests test: - needs: lint + needs: sqlite name: Test PHP ${{ matrix.php-versions }} with Code Coverage runs-on: ubuntu-latest strategy: + fail-fast: false matrix: php-versions: ['7.2', '7.3', '7.4', '8.0'] steps: @@ -48,7 +49,7 @@ jobs: id: composer-cache run: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Restore Composer Cache - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -66,10 +67,10 @@ jobs: file: ./coverage.xml sqlite: - needs: lint name: SQLite PHP ${{ matrix.php-versions }} runs-on: ubuntu-latest strategy: + fail-fast: false matrix: php-versions: ['7.2', '7.3', '7.4', '8.0'] steps: @@ -86,7 +87,7 @@ jobs: id: composer-cache run: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Restore Composer Cache - uses: actions/cache@v1 + uses: actions/cache@v2 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} diff --git a/tests/ORM/Driver/MySQL/TransactionTest.php b/tests/ORM/Driver/MySQL/TransactionTest.php new file mode 100644 index 000000000..441583ed4 --- /dev/null +++ b/tests/ORM/Driver/MySQL/TransactionTest.php @@ -0,0 +1,10 @@ +id == '3') { + return new class () implements CommandInterface { + public function isReady(): bool + { + return true; + } + + public function isExecuted(): bool + { + return false; + } + + public function execute() + { + throw new \Exception('Something went wrong'); + } + + public function complete() + { + } + + public function rollBack() + { + } + }; + } + + return parent::queueDelete($entity, $node, $state); + } +} diff --git a/tests/ORM/TransactionTest.php b/tests/ORM/TransactionTest.php new file mode 100644 index 000000000..2036183af --- /dev/null +++ b/tests/ORM/TransactionTest.php @@ -0,0 +1,238 @@ +makeTable('user', ['id' => 'primary', 'email' => 'string', 'balance' => 'float',]); + $this->getDatabase()->table('user')->insertMultiple( + ['email', 'balance'], + [ + ['hello@world.com', 100], + ['another@world.com', 200], + ['test@world.com', 300], + ] + ); + + $this->orm = $this->withSchema( + new Schema([ + User::class => [ + Schema::ROLE => 'user', + Schema::MAPPER => TransactionTestMapper::class, + Schema::DATABASE => 'default', + Schema::TABLE => 'user', + Schema::PRIMARY_KEY => 'id', + Schema::COLUMNS => ['id', 'email', 'balance'], + Schema::TYPECAST => ['id' => 'int', 'balance' => 'int'], + Schema::SCHEMA => [], + Schema::RELATIONS => [], + ], + ]) + ); + } + + public function testTransactionRollbackShouldResetEntityState() + { + $t = new Transaction($this->orm); + + $s = new Select($this->orm, User::class); + + $u1 = $s->wherePK(1)->fetchOne(); + $u1->balance = 150; + + $s = new Select($this->orm, User::class); + $u2 = $s->wherePK(2)->fetchOne(); + $u2->balance = 250; + + $s = new Select($this->orm, User::class); + $u4 = $s->wherePK(3)->fetchOne(); + + $u = new User(); + $u->email = 'foo@site.com'; + $u->balance = 300; + + $t->persist($u1); + $t->delete($u2); + $t->persist($u); + $t->delete($u4); + + $t1 = clone $t; + $t2 = clone $t1; + + try { + $this->captureWriteQueries(); + $t->run(); + } catch (\Exception $e) { + $this->assertNumWrites(3); + $this->assertNull($u->id); + } + + try { + $this->captureWriteQueries(); + $t1->run(); + } catch (\Exception $e) { + $this->assertNumWrites(3); + $this->assertNull($u->id); + } + + try { + $this->captureWriteQueries(); + $t2->run(); + } catch (\Exception $e) { + $this->assertNumWrites(3); + $this->assertNull($u->id); + } + + $this->orm->getHeap()->clean(); + } + + public function testRollbackDatabaseTransactionAfterRunORMTransaction() + { + $dbal = $this->orm->getFactory()->database(); + + $u = (new Select($this->orm, User::class))->wherePK(1)->fetchOne(); + $u->balance = 150; + + $u3 = (new Select($this->orm, User::class))->wherePK(2)->fetchOne(); + + $newU = new User(); + $newU->email = 'foo@site.com'; + $newU->balance = 300; + + $dbal->begin(); + + try { + $t = new Transaction($this->orm); + + $t->persist($u); + $t->persist($newU); + + $t->run(); + + $t->delete($u3); + + $newU->balance = 350; + $t->persist($newU); + + $t->run(); + + throw new \Exception('Something went wrong outside transaction'); + + $dbal->commit(); + } catch (\Throwable $e) { + $this->assertSame('Something went wrong outside transaction', $e->getMessage()); + $dbal->rollback(); + } + + $this->orm->getHeap()->clean(); + + $this->assertSame(100, (new Select($this->orm, User::class))->wherePK(1)->fetchOne()->balance); + $this->assertNotNull((new Select($this->orm, User::class))->wherePK(2)->fetchOne()); + $this->assertNull((new Select($this->orm, User::class))->wherePK($newU->id)->fetchOne()); + } + + public function testRollbackDatabaseTransactionDuringRunORMTransaction() + { + $dbal = $this->orm->getFactory()->database(); + + $u = (new Select($this->orm, User::class))->wherePK(1)->fetchOne(); + $u->balance = 150; + + $u3 = (new Select($this->orm, User::class))->wherePK(3)->fetchOne(); + + $newU = new User(); + $newU->email = 'foo@site.com'; + $newU->balance = 300; + + $dbal->begin(); + + try { + $t = new Transaction($this->orm); + + $t->persist($u); + $t->persist($newU); + + $t->run(); + + $this->assertSame( + 150, + (new Select($this->orm->withHeap(new Heap()), User::class))->wherePK(1)->fetchOne()->balance + ); + + // For user with ID 3 Mapper should throw an exception + $t->delete($u3); + + $newU->balance = 350; + $t->persist($newU); + + $t->run(); + + $this->fail('Exception should be thrown.'); + + $dbal->commit(); + } catch (\Throwable $e) { + $this->assertSame('Something went wrong', $e->getMessage()); + $dbal->rollback(); + } + + $this->orm->getHeap()->clean(); + + $this->assertSame(100, (new Select($this->orm, User::class))->wherePK(1)->fetchOne()->balance); + $this->assertNotNull((new Select($this->orm, User::class))->wherePK(3)->fetchOne()); + $this->assertNull((new Select($this->orm, User::class))->wherePK($newU->id)->fetchOne()); + } + + public function testCommitDatabaseTransactionAfterORMTransaction() + { + $dbal = $this->orm->getFactory()->database(); + + $dbal->begin(); + + $u = (new Select($this->orm, User::class))->wherePK(1)->fetchOne(); + $u->balance = 150; + + $u2 = (new Select($this->orm, User::class))->wherePK(2)->fetchOne(); + + $newU = new User(); + $newU->email = 'foo@site.com'; + $newU->balance = 300; + + $t = new Transaction($this->orm); + + $t->persist($u); + $t->persist($newU); + + $t->run(); + + $t->delete($u2); + + $newU->balance = 350; + $t->persist($newU); + + $t->run(); + + $dbal->commit(); + + $this->orm->getHeap()->clean(); + + $this->assertSame(150, (new Select($this->orm, User::class))->wherePK(1)->fetchOne()->balance); + $this->assertNull((new Select($this->orm, User::class))->wherePK(2)->fetchOne()); + $this->assertSame(350, (new Select($this->orm, User::class))->wherePK($newU->id)->fetchOne()->balance); + } +} diff --git a/tests/generate.php b/tests/generate.php index 28ab431d5..606a50909 100644 --- a/tests/generate.php +++ b/tests/generate.php @@ -58,12 +58,7 @@ $filename, sprintf( ' Date: Sun, 28 Nov 2021 13:43:46 +0300 Subject: [PATCH 3/5] Better compatibility between `ConstrainInterface` and `ScopeInterface` (#271) Force load ConstrainInterface when ScopeInterface is loaded --- src/Select/ScopeInterface.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Select/ScopeInterface.php b/src/Select/ScopeInterface.php index 673085091..7ddc82e3d 100644 --- a/src/Select/ScopeInterface.php +++ b/src/Select/ScopeInterface.php @@ -1,5 +1,7 @@ Date: Mon, 6 Dec 2021 21:53:23 +0300 Subject: [PATCH 4/5] Add the STI discriminator autoadding in the Schema (#278) --- src/Schema.php | 5 + .../TableInheritanceWithoutTypeColumnTest.php | 10 ++ .../TableInheritanceWithoutTypeColumnTest.php | 10 ++ .../TableInheritanceWithoutTypeColumnTest.php | 10 ++ .../TableInheritanceWithoutTypeColumnTest.php | 10 ++ .../TableInheritanceWithoutTypeColumnTest.php | 156 ++++++++++++++++++ 6 files changed, 201 insertions(+) create mode 100644 tests/ORM/Driver/MySQL/TableInheritanceWithoutTypeColumnTest.php create mode 100644 tests/ORM/Driver/Postgres/TableInheritanceWithoutTypeColumnTest.php create mode 100644 tests/ORM/Driver/SQLServer/TableInheritanceWithoutTypeColumnTest.php create mode 100644 tests/ORM/Driver/SQLite/TableInheritanceWithoutTypeColumnTest.php create mode 100644 tests/ORM/TableInheritanceWithoutTypeColumnTest.php diff --git a/src/Schema.php b/src/Schema.php index 4af26c074..d69173e21 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -141,6 +141,11 @@ protected function normalize(array $schema): array $aliases[$item[self::ENTITY]] = $role; } + // Single Table Inheritance marker + if (isset($item[self::CHILDREN]) && !isset($item[self::COLUMNS]['_type'])) { + $item[self::COLUMNS]['_type'] = '_type'; + } + unset($item[self::ROLE]); $result[$role] = $item; } diff --git a/tests/ORM/Driver/MySQL/TableInheritanceWithoutTypeColumnTest.php b/tests/ORM/Driver/MySQL/TableInheritanceWithoutTypeColumnTest.php new file mode 100644 index 000000000..d18108e0a --- /dev/null +++ b/tests/ORM/Driver/MySQL/TableInheritanceWithoutTypeColumnTest.php @@ -0,0 +1,10 @@ +makeTable('user', [ + 'id' => 'primary', + '_type' => 'string,nullable', + 'email' => 'string', + 'balance' => 'float', + 'permissions' => 'string,nullable', + ]); + + $this->getDatabase()->table('user')->insertMultiple( + ['_type', 'email', 'balance', 'permissions'], + [ + ['user', 'hello@world.com', 100, ''], + ['admin', 'another@world.com', 200, '*'], + [null, 'third@world.com', 300, ''], + ] + ); + + $this->orm = $this->withSchema(new Schema([ + User::class => [ + Schema::ROLE => 'user', + Schema::CHILDREN => [ + 'admin' => Admin::class, + ], + Schema::MAPPER => Mapper::class, + Schema::DATABASE => 'default', + Schema::TABLE => 'user', + Schema::PRIMARY_KEY => 'id', + Schema::COLUMNS => ['id', 'email', 'balance', 'permissions'], + Schema::SCHEMA => [], + Schema::RELATIONS => [], + ], + Admin::class => [Schema::ROLE => User::class], + ])); + } + + public function testFetchData(): void + { + $selector = new Select($this->orm, User::class); + + $this->assertEquals([ + [ + 'id' => 1, + '_type' => 'user', + 'email' => 'hello@world.com', + 'balance' => 100.0, + 'permissions' => '', + ], + [ + 'id' => 2, + '_type' => 'admin', + 'email' => 'another@world.com', + 'balance' => 200.0, + 'permissions' => '*', + ], + [ + 'id' => 3, + '_type' => null, + 'email' => 'third@world.com', + 'balance' => 300.0, + 'permissions' => '', + ], + ], $selector->fetchData()); + } + + public function testIterate(): void + { + $selector = new Select($this->orm, User::class); + [$a, $b, $c] = $selector->orderBy('id')->fetchAll(); + + $this->assertInstanceOf(User::class, $a); + $this->assertNotInstanceOf(Admin::class, $a); + $this->assertInstanceOf(Admin::class, $b); + $this->assertInstanceOf(User::class, $c); + $this->assertNotInstanceOf(Admin::class, $c); + + $this->assertSame('*', $b->permissions); + } + + public function testStoreNormalAndInherited(): void + { + $u = new User(); + $u->email = 'user@email.com'; + $u->balance = 100; + + $a = new Admin(); + $a->email = 'admin@email.com'; + $a->balance = 400; + $a->permissions = '~'; + + $tr = new Transaction($this->orm); + $tr->persist($u); + $tr->persist($a); + $tr->run(); + + $selector = new Select($this->orm->withHeap(new Heap()), User::class); + $this->assertInstanceOf(User::class, $selector->wherePK(4)->fetchOne()); + + $selector = new Select($this->orm->withHeap(new Heap()), User::class); + $this->assertNotInstanceOf(Admin::class, $selector->wherePK(4)->fetchOne()); + + $selector = new Select($this->orm->withHeap(new Heap()), User::class); + $this->assertInstanceOf(Admin::class, $selector->wherePK(5)->fetchOne()); + } + + public function testGetFromRepositoryWithEmptyHeap(): void + { + $u = new User(); + $u->email = 'user@email.com'; + $u->balance = 100; + + $a = new Admin(); + $a->email = 'admin@email.com'; + $a->balance = 400; + $a->permissions = '~'; + + $tr = new Transaction($this->orm); + $tr->persist($u); + $tr->persist($a); + $tr->run(); + unset($u, $a); + + $this->orm->getHeap()->clean(); + + $u = $this->orm->get(User::class, ['id' => 4]); + self::assertInstanceOf(User::class, $u); + $a = $this->orm->get(User::class, ['id' => 5]); + self::assertInstanceOf(Admin::class, $a); + } +} From 4018f8a00570e6c11bc42e388046e1b25a5a2f42 Mon Sep 17 00:00:00 2001 From: Aleksei Gagarin Date: Mon, 6 Dec 2021 18:53:43 +0000 Subject: [PATCH 5/5] Apply fixes from StyleCI --- .../TableInheritanceWithoutTypeColumnTest.php | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/tests/ORM/TableInheritanceWithoutTypeColumnTest.php b/tests/ORM/TableInheritanceWithoutTypeColumnTest.php index f43ae5711..63cbae5d9 100644 --- a/tests/ORM/TableInheritanceWithoutTypeColumnTest.php +++ b/tests/ORM/TableInheritanceWithoutTypeColumnTest.php @@ -26,10 +26,10 @@ public function setUp(): void parent::setUp(); $this->makeTable('user', [ - 'id' => 'primary', - '_type' => 'string,nullable', - 'email' => 'string', - 'balance' => 'float', + 'id' => 'primary', + '_type' => 'string,nullable', + 'email' => 'string', + 'balance' => 'float', 'permissions' => 'string,nullable', ]); @@ -43,18 +43,18 @@ public function setUp(): void ); $this->orm = $this->withSchema(new Schema([ - User::class => [ - Schema::ROLE => 'user', - Schema::CHILDREN => [ + User::class => [ + Schema::ROLE => 'user', + Schema::CHILDREN => [ 'admin' => Admin::class, ], - Schema::MAPPER => Mapper::class, - Schema::DATABASE => 'default', - Schema::TABLE => 'user', + Schema::MAPPER => Mapper::class, + Schema::DATABASE => 'default', + Schema::TABLE => 'user', Schema::PRIMARY_KEY => 'id', - Schema::COLUMNS => ['id', 'email', 'balance', 'permissions'], - Schema::SCHEMA => [], - Schema::RELATIONS => [], + Schema::COLUMNS => ['id', 'email', 'balance', 'permissions'], + Schema::SCHEMA => [], + Schema::RELATIONS => [], ], Admin::class => [Schema::ROLE => User::class], ])); @@ -66,24 +66,24 @@ public function testFetchData(): void $this->assertEquals([ [ - 'id' => 1, - '_type' => 'user', - 'email' => 'hello@world.com', - 'balance' => 100.0, + 'id' => 1, + '_type' => 'user', + 'email' => 'hello@world.com', + 'balance' => 100.0, 'permissions' => '', ], [ - 'id' => 2, - '_type' => 'admin', - 'email' => 'another@world.com', - 'balance' => 200.0, + 'id' => 2, + '_type' => 'admin', + 'email' => 'another@world.com', + 'balance' => 200.0, 'permissions' => '*', ], [ - 'id' => 3, - '_type' => null, - 'email' => 'third@world.com', - 'balance' => 300.0, + 'id' => 3, + '_type' => null, + 'email' => 'third@world.com', + 'balance' => 300.0, 'permissions' => '', ], ], $selector->fetchData());