Active Record
Pattern Overview
Active Record is an implementation of Martin Fowler's Active Record pattern:
"An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data."
— Martin Fowler, Patterns of Enterprise Application Architecture
Active Record is the M in MVC - the model - which is the layer of the system responsible for representing business data and logic. It facilitates the creation and use of business objects whose data requires persistent storage to a database.
Key Characteristics:
- Each Active Record instance represents a single row in the database
- The object carries both data and behavior
- Provides methods like
save(),delete(),get(),all(), etc. - Database operations are directly available on the domain object
When to use Active Record vs Repository/Data Mapper:
| Aspect | Active Record | Repository + Data Mapper |
|---|---|---|
| Complexity | Simple, straightforward | More complex, more layers |
| Best for | Simple domains, CRUD-heavy apps | Complex domain logic, DDD |
| Coupling | Domain objects know about database | Domain objects are pure |
| Learning curve | Lower | Higher |
| Testability | Harder (objects tied to DB) | Easier (dependency injection) |
💡 Tip: For simple applications or prototypes, Active Record is perfect. For complex domain logic or when following Domain-Driven Design principles, prefer Repository + Data Mapper.
How to Use Active Record
- Create a model and add the annotations to the class and properties ( see Getting Started with Models and Attributes)
- Add the
ActiveRecordtrait to your model - Initialize the Active Record with the
initializestatic method (need to do only once)
Example
#[TableAttribute(tableName: 'my_table')]
class MyClass
{
// Add the ActiveRecord trait to enable the Active Record
use ActiveRecord;
#[FieldAttribute(primaryKey: true)]
public ?int $id;
#[FieldAttribute(fieldName: "some_property")]
public ?int $someProperty;
}
If you have more than one database connection, you can define a default database connection:
// Set a default DBDriver
ORM::defaultDriver($dbDriver);
or call the initialize method with the database connection:
// Initialize the Active Record with a specific DBDriver
MyClass::initialize($dbDriver);
Using the Active Record
Once properly configured, you can use the Active Record pattern for database operations:
Insert a New Record
// Create a new instance
$myClass = MyClass::new();
$myClass->someProperty = 123;
$myClass->save();
// Or create with initial values
$myClass = MyClass::new(['someProperty' => 123]);
$myClass->save();
Retrieve a Record
$myClass = MyClass::get(1);
$myClass->someProperty = 456;
$myClass->save();
Query with Fluent API
The new fluent query API provides a more intuitive way to build and execute queries:
// Get first record matching criteria
$myClass = MyClass::where('someProperty = :value', ['value' => 123])
->first();
// Get first record or throw exception if not found
$myClass = MyClass::where('someProperty = :value', ['value' => 123])
->firstOrFail();
// Check if records exist
if (MyClass::where('someProperty > :min', ['min' => 100])->exists()) {
echo "Records found!";
}
// Get all matching records as array
$myClassList = MyClass::where('someProperty = :value', ['value' => 123])
->orderBy(['id DESC'])
->toArray();
// Chain multiple conditions
$myClass = MyClass::where('someProperty > :min', ['min' => 100])
->where('someProperty < :max', ['max' => 200])
->orderBy(['someProperty'])
->first();
// Build query step by step
$query = MyClass::newQuery()
->where('someProperty = :value', ['value' => 123])
->orderBy(['id DESC'])
->limit(0, 10);
$results = $query->toArray();
Complex Filtering (Legacy)
$myClassList = MyClass::filter((new IteratorFilter())->and('someProperty', Relation::EQUAL, 123));
foreach ($myClassList as $myClass) {
echo $myClass->someProperty;
}
Get All Records
// Get all records (paginated, default is page 0, limit 50)
$myClassList = MyClass::all();
// Get page 2 with 20 records per page
$myClassList = MyClass::all(2, 20);
Delete a Record
$myClass = MyClass::get(1);
$myClass->delete();
Refresh a Record
// Retrieve a record
$myClass = MyClass::get(1);
// do some changes in the database
// OR
// expect that the record was changed by another process
// Get the updated data from the database
$myClass->refresh();
Update a Model from Another Model or Array
// Get a record
$myClass = MyClass::get(1);
// Update from array
$myClass->fill(['someProperty' => 789]);
// Update from another model
$anotherModel = MyClass::new(['someProperty' => 789]);
$myClass->fill($anotherModel);
// Save changes
$myClass->save();
Convert to Array
$myClass = MyClass::get(1);
// Convert to array (excluding null values)
$array = $myClass->toArray();
// Convert to array (including null values)
$array = $myClass->toArray(true);
Using the Query Class
$query = MyClass::joinWith('other_table');
// do some query here
// ...
// Execute the query
$myClassList = MyClass::query($query);
Get Table Name
$tableName = MyClass::tableName();
Custom Mapper Configuration
By default, the Active Record uses the class attributes to discover the mapper configuration.
You can override this behavior by implementing the discoverClass method:
class MyClass
{
use ActiveRecord;
// Override the default mapper discovery
protected static function discoverClass(): string|Mapper
{
// Return a custom mapper
return new Mapper(
self::class,
'custom_table',
['id']
);
}
}
Advantages of Active Record
The Active Record pattern offers several advantages:
- Simplicity: It provides a simple, intuitive interface for database operations
- Encapsulation: Database operations are encapsulated within the model class
- Reduced Boilerplate: Eliminates the need for separate repository classes for basic operations
- Fluent Interface: Enables method chaining for a more readable code style
When to Use Active Record vs. Repository
Both patterns have their place in application development:
-
Use Active Record when:
- You prefer a simpler, more direct approach
- Your application has straightforward database operations
- You want to reduce the number of classes in your codebase
-
Use Repository when:
- You need more control over database operations
- Your application requires complex queries
- You prefer a more explicit separation between models and database operations
- You're implementing a domain-driven design approach