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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 68 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ final class AddressMapper extends RecordMapper
*/
final class UserMapper extends EntityRecordMapper
{
/** @var LazyBuffer<string, Address> */
private LazyBuffer $addressLoader;

public function __construct(
private readonly AddressRepository $addressRepository,
) {
Expand All @@ -96,25 +99,88 @@ final class UserMapper extends EntityRecordMapper
$this->string('name', fn(User $entity) => $entity->name);
$this->date('date_of_birth', fn(User $entity) => $entity->dateOfBirth);
$this->integer('address_id', fn(User $entity) => $entity->address->id);

$this->addressLoader = lazyBufferOf(Address::class, function (array $ids) {
return listOf($this->addressRepository->loadAllByIds($ids))
->indexedBy(fn(Address $address) => $address->id)
->entries();
});
}

public function map(array $row): User
#[Override] public function map(array $row): User
{
return new User(
id: $row['id'],
name: $row['name'],
dateOfBirth: new DateTime($row['date_of_birth']),
address: lazyOf(Address::class, fn() => $this->addressRepository->loadById($row['address_id'])),
address: $this->addressLoader->lazyOf($row['address_id']),
);
}
}

```

### Nullable Support

For nullable fields, use the `->nullable(...)` method on column definitions. This allows you to gracefully handle `NULL`
values in your database rows.

### Foreign keys and objects

The orm does not resolve foreign keys and objects automatically,
instead you have to inject the object repository and load as needed:

```php
return new User(
...,
/** lazy load foreign object */
address: lazyOf(Address::class, fn() => $addressRepository->loadById($row['address_id'])),
/** eager load foreign object */
address: $addressRepository->loadById($row['address_id']),
);
```

### Lazy loading

Records can be lazy loaded with `lazyOf` function, which receives an initializer and loads the entry only when any of its properties are called:
```php
/** lazy object */
$address = lazyOf(
/** The class to be loaded */
Address::class,
/** The object initializer */
fn() => $addressRepository->loadById($addressId),
);

/** loaded object */
$street = $address->street;
```

### Solving N+1 problems

N+1 can be solved with `lazyBufferOf` function, it manages buffered and loaded records,
all buffered records are loaded at once when any of the entries are loaded,
and all loaded records are returned right away without additional loader calls

```php
$addressLoader = lazyBufferOf(
/** The class to be loaded */
Address::class,
/** The object list loader */
function (array $bufferedIds) {
listOf($this->addressRepository->loadAllByIds($ids))
->indexedBy(fn(Address $address) => $address->id)
->entries()
},
);

/** lazy object */
$address = $addressLoader->lazyOf($addressId),

/** loaded object */
$street = $address->street;
```

## 🗃️ Repositories

This library also provides base repository classes that you can extend to perform actual database operations.
Expand Down
41 changes: 21 additions & 20 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions tests/Fixtures/AddressRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Tcds\Io\Orm\Connection\Connection;
use Tcds\Io\Orm\RecordMapper;
use Tcds\Io\Orm\RecordRepository;
use Traversable;

/**
* @extends RecordRepository<Address>
Expand All @@ -23,4 +24,13 @@ public function loadById(int $id): Address
{
return $this->selectOneWhere(['id' => $id]) ?? throw new Exception('Address not found');
}

/**
* @param list<string> $ids
* @return Traversable<Address>
*/
public function loadAllByIds(array $ids): Traversable
{
return $this->selectManyByQuery('SELECT * FROM addresses WHERE id IN (:ids)', ['ids' => $ids]);
}
}
12 changes: 11 additions & 1 deletion tests/Fixtures/UserMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@

use DateTime;
use Override;
use Tcds\Io\Generic\LazyBuffer;
use Tcds\Io\Orm\EntityRecordMapper;

/**
* @extends EntityRecordMapper<User, int>
*/
final class UserMapper extends EntityRecordMapper
{
/** @var LazyBuffer<string, Address> */
private LazyBuffer $addressLoader;

public function __construct(
private readonly AddressRepository $addressRepository,
) {
Expand All @@ -21,6 +25,12 @@ public function __construct(
$this->string('name', fn(User $entity) => $entity->name);
$this->date('date_of_birth', fn(User $entity) => $entity->dateOfBirth);
$this->integer('address_id', fn(User $entity) => $entity->address->id);

$this->addressLoader = lazyBufferOf(Address::class, function (array $ids) {
return listOf($this->addressRepository->loadAllByIds($ids))
->indexedBy(fn(Address $address) => $address->id)
->entries();
});
}

#[Override] public function map(array $row): User
Expand All @@ -29,7 +39,7 @@ public function __construct(
id: $row['id'],
name: $row['name'],
dateOfBirth: new DateTime($row['date_of_birth']),
address: lazyOf(Address::class, fn() => $this->addressRepository->loadById($row['address_id'])),
address: $this->addressLoader->lazyOf($row['address_id']),
);
}
}
6 changes: 3 additions & 3 deletions tests/Unit/EntityRecordMapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ private function setupLoadAddress(int $userId, Address $address): void
{
$this->addressRepository
->expects($this->once())
->method('loadById')
->with($userId)
->willReturn($address);
->method('loadAllByIds')
->with([$userId])
->willReturn(listOf($address));
}
}