Code Generation (phorm_generator)¶
phorm_generator is a build_runner plugin that reads your @Schema annotated classes and generates:
- SQL
CREATE TABLEstatement with indexes _$PhormClassNameMixinmixin with automatictoJson(),toString()andcopyWith()_$PhormClassNameFromJson()helperClassNameservice class (e.g.Users) with static CRUD methods and type-safe columns
Setup¶
Commands¶
# One-time build
dart run build_runner build
# Build and automatically resolve conflicts (recommended during development)
dart run build_runner build --delete-conflicting-outputs
# Watch mode — rebuilds on file changes
dart run build_runner watch --delete-conflicting-outputs
Anatomy of a Generated File¶
For a file lib/models/user.dart with part 'user.sql.g.dart';, the generator produces lib/models/user.sql.g.dart containing:
mixin _$PhormUserMixin {
// toJson — automatic TOP-LEVEL serialization
Map<String, dynamic> toJson() => _$PhormUserToJson(this as User);
// toString — automatic implementation for debugging
@override
String toString() => _$PhormUserToString(this as User);
// copyWith — immutable update pattern
User copyWith({
String? id,
String? firstName,
...
}) => User(
id: id ?? (this as User).id,
firstName: firstName ?? (this as User).firstName,
...
);
// Timestamps are automatically mixed in if enabled
DateTime? createdAt;
DateTime? updatedAt;
DateTime? deletedAt;
}
Generated Service Class (Users)¶
This class is the primary API for your model.
class Users {
// Type-safe columns for queries
static const id = PhormColumn<String>('id');
static const firstName = PhormColumn<String>('first_name');
...
// Static CRUD methods
static Future<int> insert(User item) => ...;
static Future<User?> read(Object id) => ...;
static PhormQuery<User> where(PhormCondition c) => ...;
...
}
Generated fromJson Helper¶
User _$PhormUserFromJson(Map<String, dynamic> json) => User(
id: json['id'] as String,
firstName: json['first_name'] as String,
isActive: (json['is_active'] as int?) == 1,
createdAt: json['created_at'] != null
? DateTime.parse(json['created_at'] as String)
: null,
...
);
Generated Row Binder (automatic, since generator 1.6.0)¶
Alongside fromJson, the generator emits a positional row binder
(_$PhormUserRowBinder) and wires it into the generated Table via
rowBinder:. The runtime uses it automatically on the fast read path:
column indices are resolved once per result set and fields are read by
position instead of building a map per row — measurably faster on large
reads.
You never interact with it directly — just re-run build_runner after
upgrading; your Users.readAll() / query calls are unchanged. Models
generated by older generator versions (or hand-written Tables without a
rowBinder) keep working through fromJson. Generic models don't get a
binder and also fall back to fromJson.
Generated Table Configuration¶
final usersTable = Table<User>(
name: 'users',
schema: '''
CREATE TABLE users (
id TEXT PRIMARY KEY,
first_name TEXT NOT NULL,
...
created_at TEXT NOT NULL,
updated_at TEXT,
deleted_at TEXT
);
CREATE UNIQUE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_name ON users(first_name, last_name);
''',
fromJson: _$PhormUserFromJson,
primaryKey: 'id', // Resolved from @ID annotation (e.g. 'custom_id')
paranoid: true,
timestamps: true,
relationships: [
HasMany(model: Order, foreignKey: 'user_id'),
],
columns: ['id', 'first_name', 'last_name', 'email', ...],
);
How to Use the Generated Code¶
// Your model file
part 'user.sql.g.dart';
@Schema(
tableName: 'users',
paranoid: true,
)
class User extends Model with _$PhormUserMixin {
@ID()
final String id;
@Column()
final String firstName;
User({
required this.id,
required this.firstName,
});
factory User.fromJson(Map<String, dynamic> json) => _$PhormUserFromJson(json);
}
// Service usage (No manual setup needed!)
import 'package:phorm_sqlite/phorm_sqlite.dart';
// 1. Querying
final users = await Users.where(Users.firstName.eq('John')).get();
// 2. CRUD
await Users.insert(newUser);
final user = await Users.readOne('id123');
Automatic Timestamp Fields¶
timestamps: true injects fields into the generated mixin¶
The generator adds created_at, updated_at to the SQL schema and injects DateTime? createdAt / DateTime? updatedAt into the generated _$PhormClassNameMixin automatically. You do not need to declare them manually — they are accessible directly on your model via the mixin.
If paranoid: true is enabled, it also adds:
DateTime? deletedAt
These fields are automatically handled in toJson() and fromJson(), so you can access them directly on your model instances without any manual declaration.
Note
If you want to customize these fields (e.g., add extra annotations or use a different name), you can still declare them manually in your model class. The generator will detect your manual declaration and won't generate a duplicate field in the mixin.
Generator Control Flags¶
You can disable specific generated code parts:
@Schema(
tableName: 'users',
useToJson: false, // Don't generate _$PhormUserToJson
useFromJson: false, // Don't generate _$PhormUserFromJson
useCopyWith: false, // Don't generate copyWith
)
class User extends Model with _$PhormUserMixin { ... }
This is useful when you have custom serialization logic that conflicts with generated code.
Common Issues¶
part 'file.sql.g.dart' not found¶
Run the generator:
Generated file is outdated¶
The generator does not automatically detect changes in non-annotated files (like referenced model classes). Rebuild explicitly when you change relationship target models.
Conflicting outputs error¶
_$PhormUserMixin not found¶
Make sure:
- The file has
part 'user.sql.g.dart'; - The class has
with _$PhormUserMixin(capitalSQ, capitalF, capitalU) - The generator has been run successfully
Note
The generated mixin name follows the pattern: _$Phorm + ClassName + Mixin.
For class MyModel → _$PhormMyModelMixin
For class UserProfile → _$PhormUserProfileMixin
Custom SQL Functions Code Generation (@SqlFunc)¶
phorm_generator also provides an automatic code generator for your custom SQLite functions, eliminating all boilerplate (such as manual registry creation, column extensions, and argument casting).
1. Annotate Top-Level Dart Functions¶
Write regular Dart functions containing your custom SQLite function logic and annotate them with @SqlFunc:
// lib/models/custom_functions.dart
import 'package:phorm/phorm.dart';
part 'custom_functions.fn.g.dart';
@SqlFunc(name: 'TO_SLUG')
String? toSlug(String? val) {
if (val == null) return null;
return val.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]+'), '-');
}
@SqlFunc(name: 'DOUBLE')
int? doubleValue(int? val) {
if (val == null) return null;
return val * 2;
}
2. Generate¶
Run build_runner. The generator creates a standalone .fn.g.dart file (e.g. custom_functions.fn.g.dart):
Note
The unique .fn.g.dart extension prevents output conflicts with other builders like source_gen:combining_builder.
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'custom_functions.dart';
// Custom SQL function registrations
final customSqlFunctions = [
SqlFunction.custom(
name: 'TO_SLUG',
argumentCount: 1,
function: (args) {
return toSlug(args[0] as String?);
},
),
SqlFunction.custom(
name: 'DOUBLE',
argumentCount: 1,
function: (args) {
return doubleValue(args[0] as int?);
},
),
];
// Type-safe column extensions for custom SQL functions
extension toSlugPhormColumnExtension on PhormColumn<String> {
/// Applies the custom SQL function `TO_SLUG` to this column.
PhormColumn<String> toSlug() {
return sqlFunction<String>('TO_SLUG');
}
}
extension doubleValuePhormColumnExtension on PhormColumn<int> {
/// Applies the custom SQL function `DOUBLE` to this column.
PhormColumn<int> doubleValue() {
return sqlFunction<int>('DOUBLE');
}
}
3. Register Custom Functions in Database¶
Provide customSqlFunctions when opening your database:
final db = DB.autoVersion(
databaseName: 'path_to_db.db',
customFunctions: customSqlFunctions,
tables: [usersTable],
);
4. Query Type-Safely¶
The generated extension methods allow calling your custom SQL functions directly on matching PhormColumn instances:
// Type-safe query!
final users = await Users.where(
Users.firstName.toSlug().eq('jane-smith'),
).get();
final doubledUsers = await Users.where(
Users.age.doubleValue().gt(50),
).get();
If you try to call .doubleValue() on a PhormColumn<String>, Dart will produce a compile-time error!
Advanced Features & Code Generation Details¶
The phorm_generator produces highly optimized, clean, and warning-free Dart code by employing smart static analysis.
1. Smart Validation Code Generation¶
To keep the generated files lightweight, validation methods (_$validate[ClassName]) are generated dynamically:
- If a model class has no validators defined on any of its fields, the generator completely omits the helper
_$validate[ClassName]function and its execution call insidetoJson(). - This ensures that generated files stay clean and strictly relevant, avoiding any unused validation boilerplate.
2. Elimination of Unused Helper Utilities¶
The generator performs a static scan of the class attributes and relationships to keep the output pristine:
- The JSON decoder helper
_$PhormDecodeJsonis omitted if it isn't referenced by any custom deserialization rules. - Unnecessary
_$PhormToJsonValuehelper declarations are excluded when no complex type conversions or collection fields are present in the schema.
3. Explicit Type Arguments for Generic Models¶
For generic model classes (e.g. class ApiResponse<T>), the generated Pluralized Service (e.g., class ApiResponses) uses explicit type arguments:
This ensures complete type safety and avoids compiler warnings (The generic type 'ApiResponse
4. Overriding Column Names vs Global Strategies¶
When a @Schema defines a global column naming strategy (e.g., columnNaming: ColumnNamingStrategy.snakeCase), specific fields can still be overridden using a per-field level configuration:
Direct columnName overrides have the highest priority and are strictly preserved exactly as defined. This allows seamless mapping of backend payload keys to local properties while maintaining global naming strategy conventions.
Tip
Best Practice for API Integration: If your application communicates with backend APIs or other external services, it is highly recommended to establish unified property naming conventions across your frontend models, database schemas, and backend payloads. Aligning these names beforehand minimizes manual mapping boilerplate, simplifies code maintenance, and prevents any property-naming confusion.