What's New in 2.1 and 2.0
MikoORM 2.1.0 adds parallel async queries and built-in database clients on top of 2.0.0, a correctness and security release. Normal (sync) methods behave the same in 2.0 and 2.1.
2.1.0
Async queries
Every read has an ...Async() version that returns a Future. On MySQL / MariaDB, PostgreSQL and SQL Server independent queries run in parallel on extra connections, so three queries of one second take about one second.
use Miko\Core\Async\Async;
[$users, $orderCount, $revenue, $rates] = Async::all([
User::where('Active', 1)->with('roles')->getAsync(),
Order::query()->countAsync(),
Order::query()->whereYear('Date', 2026)->sumAsync('Total'),
$http->getAsync('https://api.example.com/rates'), // HTTP requests join the same wait
]);
- ORM builder:
getAsync(),firstAsync(),findAsync(),findManyAsync(),countAsync(),existsAsync(),sumAsync(),avgAsync(),minAsync(),maxAsync(),valueAsync(),pluckAsync(),paginateAsync();Model::allAsync(),User::countAsync(). - Relations:
$user->posts()->getAsync(); eager loading insidegetAsync()loads one level of relations in parallel. - Table builder (
DB::table()),DB::queryAsync(),DB::firstAsync(),DB::scalarAsync(),RawQuery::getAsync(). HttpClient::getAsync(),postAsync(),downloadAsync()... can be awaited together with queries.
Details: Async Queries.
No extension to install
Miko has its own clients for the MySQL, PostgreSQL and SQL Server (TDS) protocols, written in PHP. They are used when mysqli / pgsql are not loaded, and always for SQL Server (pdo_sqlsrv has no async API). Results, types and error codes are the same as PDO, and so is the speed. Details: Built-in Clients.
| Database | Async queries | Client |
|---|---|---|
| MySQL / MariaDB | parallel | mysqli when loaded, otherwise built-in |
| PostgreSQL | parallel | pgsql when loaded, otherwise built-in |
| SQL Server | parallel | built-in TDS client |
| SQLite | one by one on the main connection | - |
SQL Server tested live
unique()on a nullable column becomes a filtered unique index, so several NULL rows are allowed, as on the other databases.BulkOperations::upsert()usesMERGE ... WITH (HOLDLOCK).- int / float columns come back as PHP int / float (bigint, decimal and money stay strings, like
pdo_sqlsrv). - New
encryptandtrust_server_certificateconnection options for ODBC Driver 18. Connection::paginate()works for SQL that ends with ORDER BY.
Other changes
- Repeated named parameters (
:n ... :n) are no longer replaced inside string literals. - Query log entries have an
asyncflag; slow async queries are marked[async]in the log file. - PostgreSQL
sslmode,sslrootcert,sslcert,sslkeyconnection keys. - Test suite: 101 tests on SQLite, MariaDB, MySQL 8 (with and without
mysqli), PostgreSQL 18 (with and withoutpgsql) and SQL Server 2022.
2.0.0
2.0 rewrote the parts that returned wrong results or were unsafe. It requires PHP 8.1+ and has breaking changes - see the Upgrade Guide.
Correctness
- Relations return the right rows.
hasManyno longer returns the whole table,belongsTo/hasOneno longer return the first row; eager loading maps every parent. belongsToManyworks with pivot data ($role->pivot,withPivot()),attach(),detach(),sync(),updateExistingPivot().$model->update([...])updates that row only. Mass writes (update,delete,restore,forceDelete,increment) need an explicit query:User::where(...)->update([...]).- Reading a property never runs a library method (
$model->deleteused to delete the row). - Mass assignment protection is enforced by
new Model($data)andcreate(); the primary key is never mass assignable. - Global scopes and soft deletes cannot be bypassed with
orWhere();withTrashed(),onlyTrashed(),withoutGlobalScope()work. Transaction::run()catches everyThrowable, uses the model connection and savepoints for nested calls.$castsare applied on read and write,json_encode($model)works,count()withgroupBy()is correct.
Security
- Every column, table and operator is validated and quoted for the driver; values are always bound. Expressions go through
selectRaw(),whereRaw(),orderByRaw(). whereLike()escapes%and_in the value.FormCrypt/Cryptouse authenticatedv2:payloads (AES-256-CBC + HMAC-SHA256); there is no built-in fallback key.Security::getClientIp()trusts forwarding headers only fromTRUSTED_PROXIES.- HTTP clients accept only
http:///https://URLs (also after redirects) and reject header injection. - SQL binding values are masked in log files.
New features
- Grammar per driver: LIMIT/OFFSET, date functions, savepoints, DDL for MySQL, PostgreSQL, SQLite and SQL Server;
Schema::table()to alter tables. - Nested and constrained eager loading:
with('posts.comments'),with(['posts' => fn($q) => ...]),load(),loadMissing(), default$with. - Query builder:
whereNot(),whereKey(),whereStartsWith(),whereTime(),unless(),addSelect(),reorder(),forPage(),cursor(),each(),single(), local scopes on the builder. BulkOperations::upsert()for every driver,DB::table(),DB::transaction().HttpClientrewrite: keep-alive, parallelpool(), retries with backoff, streamed downloads;XmlClientandSoapClientwith real timeouts.Security::rateLimitByIp()(APCu),QueryLoggerfed by every query.
Faster
exists()runsSELECT 1 ... LIMIT 1;count()drops ORDER BY / LIMIT.whereDate()/whereYear()compile to ranges that can use an index.- One connection per request instead of one per model class; one
INIT_COMMANDper MySQL connection. - Bulk inserts are chunked to the driver parameter limit;
save()skips the UPDATE when nothing changed. HttpClientreuses its cURL handle: five sequential calls took 15 ms instead of 40 ms locally.