12 KiB
12 KiB
Model Package Specification
Overview
The Model package provides a robust data modeling system that matches Laravel's Eloquent functionality. It supports active record pattern, relationships, attribute casting, serialization, and model events while leveraging Dart's type system.
Related Documentation
- See Laravel Compatibility Roadmap for implementation status
- See Foundation Integration Guide for integration patterns
- See Testing Guide for testing approaches
- See Getting Started Guide for development setup
- See Events Package Specification for model events
Core Features
1. Base Model
/// Core model implementation
abstract class Model {
/// Model attributes
final Map<String, dynamic> _attributes = {};
/// Original attributes
final Map<String, dynamic> _original = {};
/// Changed attributes
final Set<String> _changes = {};
/// Model constructor
Model([Map<String, dynamic>? attributes]) {
fill(attributes ?? {});
}
/// Gets table name
String get table;
/// Gets primary key
String get primaryKey => 'id';
/// Gets fillable attributes
List<String> get fillable => [];
/// Gets guarded attributes
List<String> get guarded => ['id'];
/// Gets attribute value
dynamic operator [](String key) => getAttribute(key);
/// Sets attribute value
operator []=(String key, dynamic value) => setAttribute(key, value);
/// Gets an attribute
dynamic getAttribute(String key) {
return _attributes[key];
}
/// Sets an attribute
void setAttribute(String key, dynamic value) {
if (!_original.containsKey(key)) {
_original[key] = _attributes[key];
}
_attributes[key] = value;
_changes.add(key);
}
/// Fills attributes
void fill(Map<String, dynamic> attributes) {
for (var key in attributes.keys) {
if (_isFillable(key)) {
this[key] = attributes[key];
}
}
}
/// Checks if attribute is fillable
bool _isFillable(String key) {
if (guarded.contains(key)) return false;
if (fillable.isEmpty) return true;
return fillable.contains(key);
}
/// Gets changed attributes
Map<String, dynamic> getDirty() {
var dirty = <String, dynamic>{};
for (var key in _changes) {
dirty[key] = _attributes[key];
}
return dirty;
}
/// Checks if model is dirty
bool get isDirty => _changes.isNotEmpty;
/// Gets original attributes
Map<String, dynamic> getOriginal() => Map.from(_original);
/// Resets changes
void syncOriginal() {
_original.clear();
_original.addAll(_attributes);
_changes.clear();
}
/// Converts to map
Map<String, dynamic> toMap() => Map.from(_attributes);
/// Converts to JSON
String toJson() => jsonEncode(toMap());
}
2. Model Relationships
/// Has one relationship
class HasOne<T extends Model> extends Relationship<T> {
/// Foreign key
final String foreignKey;
/// Local key
final String localKey;
HasOne(Query<T> query, Model parent, this.foreignKey, this.localKey)
: super(query, parent);
@override
Future<T?> get() async {
return await query
.where(foreignKey, parent[localKey])
.first();
}
}
/// Has many relationship
class HasMany<T extends Model> extends Relationship<T> {
/// Foreign key
final String foreignKey;
/// Local key
final String localKey;
HasMany(Query<T> query, Model parent, this.foreignKey, this.localKey)
: super(query, parent);
@override
Future<List<T>> get() async {
return await query
.where(foreignKey, parent[localKey])
.get();
}
}
/// Belongs to relationship
class BelongsTo<T extends Model> extends Relationship<T> {
/// Foreign key
final String foreignKey;
/// Owner key
final String ownerKey;
BelongsTo(Query<T> query, Model child, this.foreignKey, this.ownerKey)
: super(query, child);
@override
Future<T?> get() async {
return await query
.where(ownerKey, parent[foreignKey])
.first();
}
}
3. Model Events
/// Model events mixin
mixin ModelEvents {
/// Event dispatcher
static EventDispatcherContract? _dispatcher;
/// Sets event dispatcher
static void setEventDispatcher(EventDispatcherContract dispatcher) {
_dispatcher = dispatcher;
}
/// Fires a model event
Future<bool> fireModelEvent(String event) async {
if (_dispatcher == null) return true;
var result = await _dispatcher!.dispatch('model.$event', this);
return result != false;
}
/// Fires creating event
Future<bool> fireCreatingEvent() => fireModelEvent('creating');
/// Fires created event
Future<bool> fireCreatedEvent() => fireModelEvent('created');
/// Fires updating event
Future<bool> fireUpdatingEvent() => fireModelEvent('updating');
/// Fires updated event
Future<bool> fireUpdatedEvent() => fireModelEvent('updated');
/// Fires deleting event
Future<bool> fireDeletingEvent() => fireModelEvent('deleting');
/// Fires deleted event
Future<bool> fireDeletedEvent() => fireModelEvent('deleted');
}
4. Model Query Builder
/// Model query builder
class Query<T extends Model> {
/// Database connection
final DatabaseConnection _connection;
/// Model instance
final T _model;
/// Query constraints
final List<String> _wheres = [];
final List<dynamic> _bindings = [];
final List<String> _orders = [];
int? _limit;
int? _offset;
Query(this._connection, this._model);
/// Adds where clause
Query<T> where(String column, [dynamic value]) {
_wheres.add('$column = ?');
_bindings.add(value);
return this;
}
/// Adds order by clause
Query<T> orderBy(String column, [String direction = 'asc']) {
_orders.add('$column $direction');
return this;
}
/// Sets limit
Query<T> limit(int limit) {
_limit = limit;
return this;
}
/// Sets offset
Query<T> offset(int offset) {
_offset = offset;
return this;
}
/// Gets first result
Future<T?> first() async {
var results = await get();
return results.isEmpty ? null : results.first;
}
/// Gets results
Future<List<T>> get() async {
var sql = _toSql();
var rows = await _connection.select(sql, _bindings);
return rows.map((row) => _hydrate(row)).toList();
}
/// Builds SQL query
String _toSql() {
var sql = 'select * from ${_model.table}';
if (_wheres.isNotEmpty) {
sql += ' where ${_wheres.join(' and ')}';
}
if (_orders.isNotEmpty) {
sql += ' order by ${_orders.join(', ')}';
}
if (_limit != null) {
sql += ' limit $_limit';
}
if (_offset != null) {
sql += ' offset $_offset';
}
return sql;
}
/// Hydrates model from row
T _hydrate(Map<String, dynamic> row) {
var instance = _model.newInstance() as T;
instance.fill(row);
instance.syncOriginal();
return instance;
}
}
Integration Examples
1. Basic Model Usage
// Define model
class User extends Model {
@override
String get table => 'users';
@override
List<String> get fillable => ['name', 'email'];
String get name => this['name'];
set name(String value) => this['name'] = value;
String get email => this['email'];
set email(String value) => this['email'] = value;
}
// Create user
var user = User()
..name = 'John Doe'
..email = 'john@example.com';
await user.save();
// Find user
var found = await User.find(1);
print(found.name); // John Doe
2. Relationships
class User extends Model {
// Has many posts
Future<List<Post>> posts() {
return hasMany<Post>('user_id').get();
}
// Has one profile
Future<Profile?> profile() {
return hasOne<Profile>('user_id').get();
}
}
class Post extends Model {
// Belongs to user
Future<User?> user() {
return belongsTo<User>('user_id').get();
}
}
// Use relationships
var user = await User.find(1);
var posts = await user.posts();
var profile = await user.profile();
3. Events
// Register event listener
Model.getEventDispatcher().listen<ModelCreated<User>>((event) {
var user = event.model;
print('User ${user.name} was created');
});
// Create user (triggers event)
var user = User()
..name = 'Jane Doe'
..email = 'jane@example.com';
await user.save();
Testing
void main() {
group('Model', () {
test('handles attributes', () {
var user = User()
..name = 'John'
..email = 'john@example.com';
expect(user.name, equals('John'));
expect(user.isDirty, isTrue);
expect(user.getDirty(), containsPair('name', 'John'));
});
test('tracks changes', () {
var user = User()
..fill({
'name': 'John',
'email': 'john@example.com'
});
user.syncOriginal();
user.name = 'Jane';
expect(user.isDirty, isTrue);
expect(user.getOriginal()['name'], equals('John'));
expect(user.name, equals('Jane'));
});
});
group('Relationships', () {
test('loads relationships', () async {
var user = await User.find(1);
var posts = await user.posts();
expect(posts, hasLength(greaterThan(0)));
expect(posts.first, isA<Post>());
});
});
}
Next Steps
- Implement core model features
- Add relationship types
- Add model events
- Add query builder
- Write tests
- Add benchmarks
Development Guidelines
1. Getting Started
Before implementing model features:
- Review Getting Started Guide
- Check Laravel Compatibility Roadmap
- Follow Testing Guide
- Use Foundation Integration Guide
- Review Events Package Specification
2. Implementation Process
For each model feature:
- Write tests following Testing Guide
- Implement following Laravel patterns
- Document following Getting Started Guide
- Integrate following Foundation Integration Guide
3. Quality Requirements
All implementations must:
- Pass all tests (see Testing Guide)
- Meet Laravel compatibility requirements
- Follow integration patterns (see Foundation Integration Guide)
- Support model events (see Events Package Specification)
4. Integration Considerations
When implementing model features:
- Follow patterns in Foundation Integration Guide
- Ensure Laravel compatibility per Laravel Compatibility Roadmap
- Use testing approaches from Testing Guide
- Follow development setup in Getting Started Guide
5. Performance Guidelines
Model system must:
- Handle large datasets efficiently
- Optimize relationship loading
- Support eager loading
- Cache query results
- Meet performance targets in Laravel Compatibility Roadmap
6. Testing Requirements
Model tests must:
- Cover all model operations
- Test relationships
- Verify events
- Check query building
- Follow patterns in Testing Guide
7. Documentation Requirements
Model documentation must:
- Explain model patterns
- Show relationship examples
- Cover event handling
- Include performance tips
- Follow standards in Getting Started Guide