PHP Data Access Libraries (ORM, ActiveRecord, Persistence)

2016-10-26 15:42:23
I have been looking for in-depths comparison between various libraries that simplify operations with data in PHP. Compare Doctrine vs Agile Data vs Eloquent vs Yii ORM vs Cake ORM vs Fuel ORM. For more information on comparison criteria, see: and
DoctrineAgile DataEloquentYii ORMCake ORMFuel ORMRed Bean PHPx
DescriptionPHP Framework for better Business Logic design and scalable database accessRedBeanPHP is an easy to use ORM for PHP. It's a Zero Config ORM lib that 'automagically' builds your database schema.
Github / Bitbucket / Sourceforge
Latest version2.5.51.1.x2.
How schema is defined?PHP, XML, YAMLmethod init() in Model class defines fields, relations, conditions, etc.Model class propertiesActive record magic properties.Automatically discovered on the fly.
Code generationYes schema-tool- Planned.Yes Web and console generator
Can be used in Any FrameworkYes composer / packagistYes Through composer/packagistYes Through Capsule- Planned in 2.1.xYes
Automated Cache- PlannedYesYes
Anti-patterns (purposely included)ORM.SRP in Model. Implements Scopes and Active Record.Eloquent is a God Class; Global functions; DB Conection is global;
DependenciesMedium: ±5 packages.Minimal: 2 packages: DSQL Query Builder (
Simplicity0 no rating5.0 2 ratings0 no rating5.0 2 ratings0 no rating0 no rating0 no rating0 no rating
Enterprise Compliance5.0 1 rating5.0 2 ratings3.0 1 rating4.5 2 ratings0 no rating0 no rating0 no rating0 no rating
Minimum PHP Version5.45.65.4
Basic Features
PDO SupportYes YesYes MySQL, SQLite, OthersYesYes
NoSQL Integrate Transparently- Supported, but drivers not provided.No Eloquent is built on Query Builder.Yes Avialable cross sql and nosql datababase relationsNo Build around plain queries.
How to invoke db-vendor-specific extensions?Yes DBALYes Provides access to persistence-layer actions (Queries)Yes Through Query Builder / RAW queries.Yes SQL - Query Builder.- Can add SQL code chunks.
Array as a persistenceYes CRUD operations supportedYes
RestAPI as a persistence- Planned- To extend BaseActiveRecordNo
JSON string as a persistence- In development- To extend BaseActiveRecordNo
Same model, multiple persistencesYes Same class. Different object.- (no docs)
Persistence Mapping (must support C,R,U,D)
Table Name MappingYes YesYes $model->table = 'my_table'Yes Same as model class name by defaultYes Using static method of active record. mapping no longer supported.
Field Name MappingYes Yes, (Annotations in PHP)Yes PHP: "name" = SQL: "first_name"- Through accessors/3rd party extensionYesNo mapping no longer supported.
Map Entity to SubQueryNo Must be physical tableYes "Table" can be "Expression" (or another Model)No Must be physical table.- table = new Expression(..)No
Map property to expressionNo Must be physical fieldYes $model->addExpression()No Must be physical table- (couldn't find docs)No
Map native types (DateTime)Yes $field->type = 'date'Yes Accessors and MutatorsYes Active record behaviorsYes
User-defined types (e.g. "23 USD")Yes Type classYes Through typecastingYes Accessors and MutatorsYes Active record behaviors
Map field of related Entity ('currency' = currency_id->Model Specific record onlyYes Maps into sub-query.- Relationship, but only single record.Yes $model->currency->nameNo
Map field to sub-query on related entity (Client.balance = Client->orders->sum(total))NoYes hasMany()->addField(..aggregate)No- (couldn't find docs)No
Entity to join multiple tables (adding new record populates both tables)Yes AssociationsYes $model->join('other_table')NoNo No native support.No
NoSQL (must be transparent to domain logic)
MongoDB support- PlannedNoYes
Neo4j- PlannedNoNoNo
MemCache- Planned- Through Caching backend, but not as ORM native persistence.No
Criteria, Scope, Conditions (Model will refuse to work with records that does not match criteria)
Support model-level criteriaYes- Global Scope (querying only)NoNo
Soft-deleteYes Extension- Easy to implement through conditions. See docs.Yes Trait and ScopeYes Events and behaviorsNo
Domain Model CriteriaYes $m->addCondition('gender','M')- Scopes, but not in domain model.No Only for specific query: User::find()->byEmail('...')->active()->one()No
Expression-based conditionYes $m->addCondition($m->expr('..'));Yes through Query BuilderNo Only for query: User::find()->andWhere('...')->all()No
Criteria Compliance (new record must match condition)Yes With reloading and rollbackNoNo
Dynamic criteriaYes $user->addCondition('is_expired', false)No Only Querying: User::find()->where(['a' => '1', 'b' => true])->all();No
Query Building (Convert Model into Query object for further SQL tweaking)
Convert Model into Query ObjectYes $q = $model->action('select');Yes Model is a Query object. See: Model::where()Yes \yii\db\ActiveRecord::find()No
Raw expressionYes new Expression('any sql')Yes DB::raw()Yes new \yii\db\Expression('...')Yes
Nested/Composite Queries/ExpressionsYes Expression can be in any part of queryNoYes Expression can be in any part of queryNo
Reference domain-model field in queryYes through Expressionable interfaceNo No fields. Only properties.No Field is class property.No
field, where, order, limitYes Expressions and Parametric valuesYesYes \yii\db\ActiveQueryNo
options, rollup, partition- Through custom templates, or expressions.- not sure.
UPDATE, DELETE, INSERT query buildingYesYesNo
REPLACE, TRUNCATEYes- Throught raw sql in \yii\db\Command- wipe(),
SHOW, DESCRIBE, CALL- Supported through Expressions or Custom query template- Throught raw sql in \yii\db\Command- Automatic schema alter / discovery.
ALTER, CREATE query buildingYes Addon:
User-defined query templateYes Templates for all queriesYes (please link doc)No
OR deep nested conditionsYes Through Object CompositionYesNo only raw query
Use Domain logic in multi-record update- Supported, but must map fields/values manually.NoNoNo
Data Fetching (different ways to retrieve data from database)
Get All Data (2x array)YesYesYes \yii\db\Query::all()Yes exportAll()
Associative Array (id=>value)No PlannedYes lists()
Single rowYes loadAnyYesYes \yii\db\Query::one()
Single Value- Through actionYesYes \yii\db\Query::scalar()
Single ColumnNo PlannedYesYes \yii\db\Query::column()
Bypass persistence mappingYes action('select')->getAll()- ?? (e..g prevent date from being transformed into Carbon)
Loading and Saving individual records
Load by IDYes load(id)Yes User::findOne(1)Yes
Load by other fieldYes loadBy('field', $value)Yes User::findOne(['id' => 1])No
Load if expressionYes loadBy($expression)Yes User::findOne('...')Yes
Load respects Scope (model-level condition)Yes All conditions added as "where" clause- User::find()->where(...)->andWhere(...)->one()No
Specify fields to loadYes onlyFields()No Fields load with SELECT *Yes User::find()->select([])->one()No
Save only sends dirty fieldsYesYesYes- (there is Dirty support, so I assuming it is supported)
Guarded / Fillable properties- Conditions and OnlyFields do same thing differently.Yes Prevents "create($_GET)" from messing up important fields.
Object Hierarchy Model
Admin extends User (is_admin=1)Yes Yes, recommended pattern. "User" class adds condition.No Both have to use "User" model.No Can be done as wrapper: User::findAdmin(..) or custom CDbCriteriaNo
Model that represent Orders of single User.Yes Yes, recommended pattern.No Both have to use "Order" model.NoNo
Extend model, add Join (Disjoined SubTypes)Yes Yes. Unlimited number of joins.NoNoNo
Extend model change tableYes Yes. Used to tweak 3rd party models.No
Relations/References (One model can relate to another model. NOT A TABLE JOIN)
Model can define relation to other ModelYes References are implemented as ObjectsYes RelationsYes RelationsYes
One-to-ManyYes $user->hasMany('Orders', new Order());YesYes
One-to-many traversal strategyReturns destination model instance with dynamically applied criteria (parent_id=123). No queries are executed. Inserting record into this model will ensure association with parent model.Returns array of objects that may either be pre-loaded with record data or only contain IDs (lazy-loaded).Uses classical eager/lazy loading approach. with() can help pre-fetch the data for related entities.pre-fetch and store objects in an array.
Lazy/Eager-loadingNo Anti-patternYesYesYes
One-to-OneYes $order->hasOne('user_id', new User());YesYes
Many-to-ManyYes $user->addCondition('vip', true)->ref('Orders');Yes relations() method of an entity can define.
Deep Many-to-Many traversal.NoYes $user->ref('Orders')->ref('Payments')->action('count'); No intermediate queries.- only with "to-one" references: $book->author->address->street; Executes multiple queries.- only with to-one references. Executes multiple queries.
Cross-Persistence traversalYes $user->hasMany('Log', new Log($mongodb));- No docs.No
Multi-persistence Mapping (some data from SQL, others MongoDB)
Multi-persistence joinNo- Planned.NoNo
UNION existing modelsNo- PlannedNoNo
Multi-persistence UNIONNoNo But you can merge raw dataNoNo
Multi-persistence value linking (current_sensor_value)- Through behaviour- Through 'afterLoad' hook.No
Behaviours / Hooks
Before/After operationYesYes
Hooks for C,R,U,D operationsYesYes
Persistence-specific hooks (to modify Query)Yes- (no docs)
User-defined hooksYesYes
Mapping strategyRecord dirty fields before persisting. Type-cast into JSON string and store in a single field of AuditLog Model. This model can be persisted anywhere, table, file, etc.
How to set-upAuditLog can be enabled for all models associated with a specific persistence. No further changes are necessary. Works out of the box.
Store old/new field valuesYes Yes in JSON and user-readable string.Yes
Date of creation, modification- No but easy to add through hook.Yes \yii\behaviors\TimestampBehavior
User/IP creation, modification- Easy to add through a hook.Yes \yii\behaviors\BlameableBehavior
Store incremental revisions- Easy to add through a hookYes
Revert action (undo)YesNoNo
Custom eventsYesYes
Custom fields, tableYes Remove default fields, use your own table, fields, formats.Yes
Store in CSV- PlannedNo
Access to record-specific historyYes $item->ref('AuditLog');- Through a custom query.
Access Control
Implementation StrategyDefine a special DB-hook. Every time new "Model" is initialized, extra "condition" will be added. This will make sure that you won't forget some important condition when traversing references. You can also restrict access to edit/delete through per-model hooks that can be globally set.
Single-system global access scopeNoYes Define "system_id" for all models that have this field defined - this will create global condition for all models making sure that user can work with records of one system at a time.NoNoNo
Meta-information (allows exploration by UI widgets)
Implementation StrategyAgile Data is designed to work with UI toolkits. Generic widgets can explore model fields and various meta-information about those fields. References are easy to identify and traverse allowing DropDown fields to populate values inside select.Eloquent is designed to work with MVC engines, where view is mandatory and would be defining how model properties fit into your HTML.Widgets must specify data types manually, while dataProvider is used for fetching the data stream.Beans support "meta" data actually contain data type or some user-data.
UI Widget can fetch list of Model FieldsYesNo Would have to manually specify fields.- \yii\grid\GridView, \yii\widgets\ListView
UI Widget can get Field type, default value, caption, hintYesNo- (no docs)
field storing reference to another Model can map into DropDownYes Widget can discover references and traverse to fetch list of drop-down options. Model defines "title_field" which is used by default.No Must manually specify object with lists() method.No
Like 0 2 0 0 0 0 0 0
  • 2016-10-18 22:08:00
    2016-10-26 15:42:23
  • Products
  • Compare Doctrine vs Agile Data vs Eloquent vs Yii ORM vs Cake ORM vs Fuel ORM vs x vs x
  • Public
  • Creative Commons License CC-BY-SA 3.0 / GNU FDL
    Manage backups


Leave a comment

Build comparison tables or lists about everything !

It's free and fast to publish data into original tables

Create a table