databases storage and caching
ORM Orientation: Doctrine ORM And Laravel Eloquent
An ORM, or object-relational mapper, maps database rows to PHP objects or models. It can make application code more expressive, but it does not remove the need to understand SQL, schema design, transactions, and query performance.
The two ORM names PHP developers most commonly meet are Doctrine ORM and Laravel Eloquent.
What An ORM Does
An ORM usually handles:
- Mapping tables to classes or models.
- Loading rows as objects.
- Saving object changes back to the database.
- Defining relationships such as user has many orders.
- Building SQL through methods instead of raw strings.
- Managing transactions and unit-of-work behaviour in some frameworks.
That convenience has a cost: hidden queries are easier to create.
Eloquent Shape
Laravel Eloquent uses active record style models. The model usually knows how to query and save itself.
<?php
declare(strict_types=1);
final class EloquentStyleExample
{
public function recentPaidOrders(): string
{
return "Order::query()->where('status', 'paid')->latest()->limit(20)->get()";
}
}
echo (new EloquentStyleExample())->recentPaidOrders() . PHP_EOL;
// Prints:
// Order::query()->where('status', 'paid')->latest()->limit(20)->get()
In real Laravel code, that chain builds and executes SQL. You still need indexes for the status and ordering pattern.
Doctrine ORM Shape
Doctrine ORM uses entities and an entity manager. It has a unit of work that tracks changes and flushes them to the database.
<?php
declare(strict_types=1);
final class DoctrineStyleExample
{
public function createUserFlow(): array
{
return ['new User()', 'entityManager->persist($user)', 'entityManager->flush()'];
}
}
print_r((new DoctrineStyleExample())->createUserFlow());
// Prints:
// entityManager->flush()
Doctrine can be very powerful, but developers must understand when SQL is actually executed.
Relationships And N+1 Queries
ORM relationships are convenient, but they can create N+1 query problems.
<?php
declare(strict_types=1);
function possibleQueriesForOrdersAndUsers(int $orders): int
{
return 1 + $orders;
}
echo possibleQueriesForOrdersAndUsers(30) . PHP_EOL;
// Prints:
// 31
If code loads 30 orders and then lazily loads the user for each order, it may run 31 queries. Use eager loading, joins, or explicit query design when listing related data.
When To Use Raw SQL Or Query Builders
Even in ORM projects, raw SQL or a query builder may be better for:
- Reports and aggregates.
- Large exports.
- Batch updates.
- Performance-critical list pages.
- Queries using database-specific features.
Using an ORM well includes knowing when not to force everything through it.
What To Check
Before moving on, make sure you can:
- Explain what an ORM maps.
- Distinguish Doctrine ORM from Doctrine DBAL.
- Recognise Eloquent's active record style.
- Recognise Doctrine's entity manager and flush style.
- Spot N+1 query risks.
- Explain why ORM code still needs schema and SQL knowledge.
Practice
Practice: Review ORM Query Risk
Write a short PHP example that explains what an ORM might hide.
Requirements
- Show an Eloquent-style query string.
- Show a Doctrine-style persist/flush flow.
- Calculate how many queries an N+1 relationship load could create.
- Name one case where raw SQL or a query builder may be better than an ORM.
- Explain why indexes still matter.
Show solution
This solution uses strings because the goal is to recognise patterns, not install a framework.
<?php
declare(strict_types=1);
function nPlusOneQueryCount(int $parentRows): int
{
return 1 + $parentRows;
}
$eloquent = "Order::query()->where('status', 'paid')->with('user')->latest()->limit(20)->get()";
$doctrine = ['new User()', 'entityManager->persist($user)', 'entityManager->flush()'];
echo $eloquent . PHP_EOL;
echo implode(' -> ', $doctrine) . PHP_EOL;
echo 'without eager loading, 20 orders may become ' . nPlusOneQueryCount(20) . ' queries' . PHP_EOL;
// Prints:
// Order::query()->where('status', 'paid')->with('user')->latest()->limit(20)->get()
// new User() -> entityManager->persist($user) -> entityManager->flush()
// without eager loading, 20 orders may become 21 queries
Raw SQL or a query builder may be better for reports, large exports, bulk updates, or database-specific features. Indexes still matter because the ORM ultimately runs SQL against real tables.