Active Record ORM, Scaffold controls, and the prado-cli activerecord command for the PRADO PHP Framework.
A record class maps one table to one PHP class: each public property is a column, finder() methods read rows into objects, save() and delete() write them back, and a static $RELATIONS array declares HAS_MANY, HAS_ONE, BELONGS_TO, and MANY_TO_MANY relations that load eagerly with with<Relation>() or lazily on access. The Scaffold controls render list, edit, search, and detail views for any record class. prado-activerecord is the Prado\Data\ActiveRecord package that shipped inside the framework through PRADO 4.3, moved into its own repository for PRADO 4.4 with the same namespaces and class names.
- PHP 8.1 or later
- PRADO 4.3.3 or later (
pradosoft/prado) - PDO extension and a PDO driver for your database
On PRADO 4.3.x the framework still bundles its own ActiveRecord copy; the migration guide explains which copy loads. PRADO 4.4 removes the bundled copy.
Within a PRADO application:
composer require pradosoft/prado-activerecordApplications upgrading from the ActiveRecord bundled with the PRADO framework: see the migration guide. Release notes are in the changelog. The full documentation is under docs/.
Any database that PRADO's Prado\Data layer supports. The Scaffold input builders and activerecord/generate-all cover:
| Driver | PDO DSN prefix |
|---|---|
| MySQL / MariaDB | mysql: |
| PostgreSQL | pgsql: |
| SQLite | sqlite: |
| Firebird / InterBase | firebird: |
| SQL Server | sqlsrv: |
| Oracle | oci: |
| IBM DB2 | ibm: |
The database test suites run against SQLite, MySQL, PostgreSQL, Microsoft SQL Server, Firebird, Oracle, and IBM DB2, one CI job each.
Register TActiveRecordConfig as an application module in application.xml (or application.php). The module configures the default TActiveRecordManager, which every record uses unless it overrides getDbConnection().
Reference a TDataSourceConfig module by its id:
<modules>
<module id="db" class="Prado\Data\TDataSourceConfig">
<database ConnectionString="mysql:host=localhost;dbname=myapp"
Username="user" Password="secret"/>
</module>
<module id="activerecord" class="Prado\Data\ActiveRecord\TActiveRecordConfig"
ConnectionID="db" EnableCache="true"/>
</modules>Embed the <database> element directly inside TActiveRecordConfig:
<modules>
<module id="activerecord" class="Prado\Data\ActiveRecord\TActiveRecordConfig">
<database ConnectionString="sqlite:protected/data/myapp.db"/>
</module>
</modules>In PRADO 4.4 the package can also be named by its Composer id, <module id="pradosoft/prado-activerecord" ConnectionID="db"/>; the class comes from the package's extra.prado.bootstrap.
| Property | Type | Description |
|---|---|---|
ConnectionID |
string | ID of a TDataSourceConfig module to share its connection |
EnableCache |
bool | Cache table metadata in the PRADO application cache (default false) |
ManagerClass |
string | TActiveRecordManager subclass to use (default TActiveRecordManager) |
GatewayClass |
string | TActiveRecordGateway subclass to use (default TActiveRecordGateway) |
InvalidFinderResult |
Null or Exception |
What an unknown dynamic finder (findByNoSuchColumn()) returns or throws (default Null) |
CREATE TABLE users (
username VARCHAR(40) NOT NULL PRIMARY KEY,
email VARCHAR(80),
first_name VARCHAR(40),
last_name VARCHAR(40)
);use Prado\Data\ActiveRecord\TActiveRecord;
class UserRecord extends TActiveRecord
{
const TABLE = 'users';
public $username;
public $email;
public $first_name;
public $last_name;
public static function finder($className = __CLASS__)
{
return parent::finder($className);
}
}const TABLE names the table. A record class may define a static table() method instead. Each column needs a public property of the same name, or an entry in the static $COLUMN_MAPPING array that maps the column to a property. Overriding finder() lets UserRecord::finder() return a finder for the right class.
$user = UserRecord::finder()->findByPk('admin');
$users = UserRecord::finder()->findAll('last_name = ?', 'Dalton');
$some = UserRecord::finder()->findAll('email LIKE :mail', [':mail' => '%@example.com']);
$count = UserRecord::finder()->count('first_name = ?', 'Joe');
// dynamic finders: columns joined by And / Or
$joe = UserRecord::finder()->findByFirst_NameAndLast_Name('Joe', 'Dalton');
$all = UserRecord::finder()->findAllByEmail(null);
// criteria object
$criteria = new Prado\Data\ActiveRecord\TActiveRecordCriteria('email IS NOT NULL');
$criteria->OrdersBy['last_name'] = 'asc';
$criteria->Limit = 20;
$criteria->Offset = 40;
$page = UserRecord::finder()->findAll($criteria);
$user = new UserRecord();
$user->username = 'joe';
$user->email = 'joe@example.com';
$user->save(); // INSERT; later calls on the same object UPDATE
$user->email = 'joe@dalton.example';
$user->save();
$user->delete();
UserRecord::finder()->deleteByPk('averel');
UserRecord::finder()->deleteAll('last_name = ?', 'Dalton');Finder methods, parameter binding forms, and TActiveRecordCriteria are documented in docs/finders.md.
class TeamRecord extends TActiveRecord
{
const TABLE = 'teams';
public $name;
public $location;
public $players = [];
public static $RELATIONS = [
'players' => [self::HAS_MANY, PlayerRecord::class],
];
}
class PlayerRecord extends TActiveRecord
{
const TABLE = 'players';
public $player_id;
public $age;
public $team_name;
public $team;
public static $RELATIONS = [
'team' => [self::BELONGS_TO, TeamRecord::class],
'skills' => [self::MANY_TO_MANY, SkillRecord::class, 'player_skills'],
];
}
$teams = TeamRecord::finder()->withPlayers()->findAll(); // eager load
$player = PlayerRecord::finder()->withTeam()->withSkills()->findByPk(1);Foreign keys are read from the table metadata. A third array element names the foreign key column or, for MANY_TO_MANY, the association table. See docs/relations.md for lazy loading, self references, and saving related objects.
<com:TScaffoldListView ID="list" RecordClass="UserRecord" />
<com:TScaffoldEditView RecordClass="UserRecord" />
<com:TScaffoldSearch ListViewID="list" />
<com:TScaffoldView RecordClass="UserRecord" />TScaffoldView combines list, edit, and search in one control. The controls publish their own style.css (light and dark color schemes) and pick an input builder per database driver. See docs/scaffold.md.
With a TActiveRecordConfig module configured, prado-cli gains the activerecord action:
# one record class for one table
prado-cli activerecord/generate users Application.database.UserRecord
# one record class per table, with optional soap properties, overwrite, class prefix and suffix
prado-cli activerecord/generate-all Application.database soap overwrite "" Record<output> is a PRADO dot namespace that must resolve inside the application directory. TActiveRecordConfig::init() registers the action with the shell application; without the module the action is absent from prado-cli help. See docs/cli.md.
composer install| Command | Description |
|---|---|
composer fix |
Apply cs-fixer style fixes to src/ |
composer stan |
PHPStan static analysis |
composer test |
PHPUnit unit suite (no database required) |
composer unittest |
Alias for composer test |
composer db-sqlite |
Database cases over a SQLite file built from scripts/sqlite/database.sql, plus the relation tests over the bundled SQLite files |
composer db-mysql |
Database cases over MySQL |
composer db-pgsql, db-sqlsrv, db-firebird, db-oracle, db-ibm |
The same cases over PostgreSQL, SQL Server, Firebird, Oracle, and IBM DB2 |
composer dbtest |
Unit + SQLite + MySQL in one PHPUnit run |
composer fulltest |
Pre-commit check: php -l + cs-fixer dry-run + phpstan + unit tests |
Run composer fix before composer fulltest when the style check fails; fulltest runs cs-fixer in dry-run (check-only) mode.
vendor/bin/phpunit --testsuite unit --filter TScaffoldInputBaseTest
vendor/bin/phpunit --testsuite db-sqlite --filter test_has_manyThe database cases (finders, criteria, counts, deletes, dynamic finders, record save and delete, auto-increment keys, events) are written once as abstract classes under tests/unit/Data/ActiveRecord/DbSpecific/Cases/. Each driver directory runs every case through its connection trait, which names the PradoUnit::setup*Connection() helper, the database, the connection settings, and the features the driver lacks; a test that needs a missing feature is skipped with the reason. The schema for each driver is tests/unit/Data/ActiveRecord/scripts/<driver>/database.sql, loaded in CI by .github/scripts/setup-<driver>.sh. A test class whose server or table is missing fails its first test and skips the rest.
The db-sqlite suite builds its database from the SQLite script in the system temporary directory and needs only pdo_sqlite; the relation tests read the committed files in tests/unit/Data/ActiveRecord/sqlite/.
The db-mysql suite connects to mysql:host=localhost;dbname=prado_unitest as prado_unitest / prado_unitest. Create the database and user, then load the MySQL script. The other servers use the same credentials as the PRADO framework tests: PostgreSQL prado_unitest / prado_unitest on port 5432 (create the database with the C locale, which the ordering tests expect), SQL Server prado_unitest on port 1433, Firebird SYSDBA / masterkey with FIREBIRD_DB_PATH, Oracle prado_unitest on ORACLE_SERVICE_NAME (default FREEPDB1), and IBM DB2 through DB2_USER, DB2_PASSWORD, and DB2_DATABASE.
Run composer fulltest. All four steps must pass before committing:
find src -name '*.php' | xargs php -l: syntax checkvendor/bin/php-cs-fixer fix --dry-run src/: style checkvendor/bin/phpstan analyse --memory-limit=512M: static analysisvendor/bin/phpunit --testsuite unit: unit tests
- MySQL: put the class in
tests/unit/Data/ActiveRecord/DbSpecific/Mysql/, usePradoUnitDataConnectionTrait, returntruefromgetIsForActiveRecord(), and list the tables ingetTestTables(). Add any new table toscripts/mysql/database.sql. - SQLite: put the class in
DbSpecific/Sqlite/and give its records agetDbConnection()override that opens a file undertests/unit/Data/ActiveRecord/sqlite/.
src/
├── Data/ActiveRecord/
│ ├── TActiveRecordConfig.php — PRADO module; bootstraps the extension
│ ├── TActiveRecord.php — base record: finders, save/delete, relations, events
│ ├── TActiveRecordManager.php — singleton: connection, cache, gateway, InvalidFinderResult
│ ├── TActiveRecordGateway.php — builds and runs SQL through TDataGatewayCommand
│ ├── TActiveRecordCriteria.php — Condition, Parameters, OrdersBy, Limit, Offset
│ ├── TActiveRecordChangeEventParameter.php
│ ├── TActiveRecordInvalidFinderResult.php
│ ├── Exceptions/ — TActiveRecordException, TActiveRecordConfigurationException, messages.txt
│ ├── Relations/ — TActiveRecordRelation, HasMany, HasOne, BelongsTo, HasManyAssociation, RelationContext
│ └── Scaffold/ — TScaffoldBase, ListView, EditView, Search, View, templates, style.css
│ └── InputBuilder/ — TScaffoldInputBase, TScaffoldInputCommon, one T<Driver>ScaffoldInput per driver
└── Shell/Actions/
└── TActiveRecordAction.php — prado-cli activerecord/generate and generate-all
UserRecord::finder()->findByPk($id)
→ TActiveRecord::getRecordManager()->getRecordGateway()
→ TActiveRecordGateway::findRecordsByPks()
→ TDataGatewayCommand builds and executes the SQL on the record's DbConnection
→ TActiveRecord::populateObject() fills a new instance from the row
BSD-3-Clause. See LICENSE.
- Wei Zhuo — original Active Record and Scaffold implementation
- Fabio Bas — PRADO 4 maintenance
- Brad Anderson — shell command, Firebird and DB2 support, extension extraction and maintenance