PHP’s ecosystem has produced several mature ORM libraries, each shaped by different philosophies and framework traditions. Laravel Eloquent powers the most popular PHP framework with an Active Record approach. Doctrine ORM brings enterprise-grade Data Mapper patterns to Symfony and standalone applications. Propel offers a code-generation approach with XML-defined schemas. Choosing among them involves trade-offs in architecture, performance, and team experience.

This in-depth comparison examines all three ORMs with real code examples, performance characteristics, and integration guidance.

Comparison Table

FeatureEloquent (Laravel)Doctrine ORMPropel
GitHub Stars34,793 (framework)10,1711,270
ArchitectureActive RecordData MapperActive Record + Code Gen
Schema DefinitionMigrations (PHP)Attributes/XML/YAMLXML Schema
Query LanguageEloquent BuilderDQL / QueryBuilder / raw SQLPropel Query API
MigrationsBuilt-inDoctrine MigrationsBuilt-in
Lazy LoadingYesYesYes
CachingQuery cacheResult/Metadata/Query cacheInstance pooling
Framework IntegrationNative to LaravelNative to SymfonyStandalone
Learning CurveLowModerate-HighModerate
LicenseMITMITMIT

Laravel Eloquent: The Active Record Powerhouse

Eloquent is the ORM bundled with Laravel, the most popular PHP framework with over 34,000 GitHub stars. Eloquent follows the Active Record pattern: each database table has a corresponding Model class, and each instance of that class represents a single row. This makes Eloquent incredibly intuitive for developers coming from Laravel’s ecosystem.

Eloquent’s fluent query builder allows complex queries without writing raw SQL. Relationships (hasMany, belongsTo, morphTo, etc.) are defined as methods on the model and automatically handle foreign key resolution. Eloquent also provides a rich set of features: accessors/mutators, model events, API resources for JSON transformation, and built-in soft deletes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php
// Eloquent: Active Record with relationship eager loading
$activeUsers = User::query()
    ->with('orders')
    ->where('status', 'active')
    ->orderByDesc('last_login_at')
    ->limit(10)
    ->get();

// Define a relationship on the model
class User extends Model
{
    public function orders(): HasMany
    {
        return $this->hasMany(Order::class);
    }
}

When to choose Eloquent: You are building a Laravel application, your team values rapid development, and the domain model aligns well with Active Record (one model per table, relatively straightforward relationships).

Doctrine ORM: The Enterprise Data Mapper

Doctrine ORM separates domain entities from persistence logic using the Data Mapper pattern. At 10,171 stars and deeply integrated with Symfony, Doctrine is the go-to ORM for enterprise PHP applications where domain purity matters. Entities are plain PHP objects — they know nothing about the database. Repositories handle persistence.

Doctrine’s DQL (Doctrine Query Language) is object-oriented SQL that operates on entities and relationships rather than tables and columns. Combined with the QueryBuilder and native SQL support, Doctrine provides flexibility at any abstraction level. Its metadata system supports PHP 8 attributes, XML, and YAML, though attributes are the modern standard.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
// Doctrine: Entity definition with PHP 8 attributes
#[Entity]
#[Table(name: 'users')]
class User
{
    #[Id, Column, GeneratedValue]
    private int $id;

    #[Column]
    private string $status;

    #[OneToMany(mappedBy: 'user', targetEntity: Order::class)]
    private Collection $orders;

    public function __construct()
    {
        $this->orders = new ArrayCollection();
    }
}

// Doctrine: DQL query with eager loading
$query = $entityManager->createQuery(
    'SELECT u, o FROM App\Entity\User u
     JOIN u.orders o
     WHERE u.status = :status
     ORDER BY u.lastLoginAt DESC'
)->setParameter('status', 'active')
 ->setMaxResults(10);

$users = $query->getResult();

When to choose Doctrine: You need a clean separation between domain logic and persistence, the project uses Symfony or a framework-agnostic architecture, and the team values testability without a database connection.

Propel: The Code Generation Approach

Propel takes a unique approach: you define your database schema in XML, and Propel generates all PHP model classes from that schema. This inversion of control — schema is the source of truth — appeals to teams that prefer database-first design. At 1,270 stars, Propel is the smallest of the three but remains actively maintained with a dedicated community.

Propel’s generated code includes model classes, query classes, and base classes that you can customize via inheritance. The query API is fluent and type-safe, generated directly from the schema. Propel also includes built-in behaviors (timestampable, sluggable, nested set) that add functionality to generated models without manual coding.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<!-- Propel: schema.xml defines the data model -->
<database name="app" defaultIdMethod="native">
  <table name="users">
    <column name="id" type="integer" required="true"
            primaryKey="true" autoIncrement="true"/>
    <column name="email" type="varchar" size="255" required="true"/>
    <column name="status" type="varchar" size="50" required="true"/>
    <column name="last_login_at" type="timestamp"/>
    <behavior name="timestampable"/>
  </table>
  <table name="orders">
    <column name="id" type="integer" required="true"
            primaryKey="true" autoIncrement="true"/>
    <column name="user_id" type="integer" required="true"/>
    <foreign-key foreignTable="users">
      <reference local="user_id" foreign="id"/>
    </foreign-key>
  </table>
</database>
1
2
3
4
5
6
7
8
<?php
// Propel: Generated query API with fluent interface
$users = UserQuery::create()
    ->filterByStatus('active')
    ->joinWith('Order')
    ->orderByLastLoginAt('desc')
    ->limit(10)
    ->find();

When to choose Propel: Database-first design philosophy, desire for generated type-safe query APIs, and projects where the XML schema serves as documentation for both developers and DBAs.

Integrating ORMs with PHP Application Servers

Modern PHP deployments increasingly use application servers like Swoole, RoadRunner, and FrankenPHP instead of the traditional PHP-FPM model. Each ORM handles persistent memory differently:

  • Eloquent benefits from Laravel Octane (RoadRunner/Swoole) with listener resetting between requests to prevent stale state.
  • Doctrine requires explicit $entityManager->clear() between requests in long-running processes to avoid memory leaks and identity map bloat.
  • Propel supports instance pooling that naturally fits persistent PHP workers, reducing per-request overhead.

For production deployments, consider our PHP application servers guide for detailed configuration patterns.

Performance Considerations

ORM performance varies by query type and data volume. Eloquent’s Active Record creates model instances for every row, which adds memory overhead for large result sets — use lazy() or chunk() for batch processing. Doctrine’s Unit of Work tracks every loaded entity, requiring clear() for bulk operations. Propel’s generated code has the least runtime overhead per query because the schema is baked into the generated classes.

For read-heavy workloads, consider supplementing any ORM with a lightweight query layer. Doctrine’s DBAL provides a thin SQL abstraction independent of the ORM. Eloquent allows raw queries via DB::select(). Propel includes a raw PDO connection. The most important performance optimization is understanding how your ORM generates queries — always profile with tools like Laravel Debugbar or Doctrine’s SQL Logger.

The relationship between your ORM, database, and application server creates a performance triangle. Eloquent with Octane on RoadRunner can match Doctrine’s throughput for most workloads when properly tuned. Propel’s zero-reflection approach gives it an edge in CPU-bound scenarios where every millisecond of request handling counts.

For self-hosted e-commerce platforms that use these ORMs in production, see our PHP e-commerce platforms comparison. For managing your own PHP package infrastructure, reference our Composer repository hosting guide.

Testing Strategies with ORMs

Testing code that depends on an ORM requires balancing speed (no real database) against fidelity (real database behavior). Each ORM ecosystem has evolved distinct patterns for this challenge.

Eloquent’s testing story is the most streamlined thanks to Laravel’s built-in test helpers. RefreshDatabase trait wraps each test in a transaction and rolls it back, providing a clean database state without manual teardown. For faster tests, DatabaseMigrations runs migrations once per test class. Eloquent also supports withoutEvents() and withoutObservers() to isolate model logic from side effects. When you need to assert that a specific SQL query was executed, Laravel’s query log (DB::listen()) captures every query for inspection.

Doctrine’s approach emphasizes repository testing with an in-memory SQLite database. The EntityManager can be configured with SQLite for unit tests, preserving the full ORM stack while avoiding external dependencies. For true unit tests, Doctrine’s entity classes are plain PHP objects — you can instantiate them directly, set properties, and test domain logic without any ORM involvement. The DoctrineTestBundle for Symfony provides entity manager resetting and schema creation in test setup.

Propel’s generated classes make testing straightforward: you can mock the generated query classes or use an in-memory SQLite connection string in your test configuration. Propel’s instance pooling can be disabled in tests to ensure clean state between test cases. Because Propel generates concrete query methods from the schema, mocking becomes a matter of subclassing generated query classes — no reflection or dynamic proxies needed.

A practical testing strategy regardless of ORM: use repository interfaces to abstract data access, test business logic with in-memory repositories, and reserve database integration tests for complex query logic and schema migrations.

FAQ

Q: Can I use Doctrine with Laravel instead of Eloquent? A: Yes, through the Laravel Doctrine package. However, you lose much of Laravel’s built-in conveniences (policies, notifications, queues) that assume Eloquent models. For most Laravel projects, Eloquent is the path of least resistance.

Q: Is Eloquent slower than Doctrine? A: Not inherently. Both generate efficient SQL for common patterns. Doctrine’s Data Mapper typically creates more objects per query (entity + proxy), but the difference is negligible for most applications. Benchmark your specific use case.

Q: Which ORM works best with PHP 8.x features? A: All three support PHP 8.x. Doctrine was first to adopt attributes (PHP 8.0) for metadata. Eloquent uses attributes for casting and relationships. Propel’s generated code is compatible with PHP 8.x.

Q: Can I use Propel outside of a framework? A: Yes. Propel is framework-agnostic and works as a standalone library. Its code generation step produces self-contained PHP classes that don’t require a framework.

Q: How do I choose between Active Record and Data Mapper? A: Active Record (Eloquent, Propel) maps one class to one table — simpler to learn and faster to prototype. Data Mapper (Doctrine) separates domain objects from persistence — better for complex domains with business logic that shouldn’t depend on database structure.


💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com