/ What's New

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 inside getAsync() 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.

DatabaseAsync queriesClient
MySQL / MariaDBparallelmysqli when loaded, otherwise built-in
PostgreSQLparallelpgsql when loaded, otherwise built-in
SQL Serverparallelbuilt-in TDS client
SQLiteone 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() uses MERGE ... WITH (HOLDLOCK).
  • int / float columns come back as PHP int / float (bigint, decimal and money stay strings, like pdo_sqlsrv).
  • New encrypt and trust_server_certificate connection 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 async flag; slow async queries are marked [async] in the log file.
  • PostgreSQL sslmode, sslrootcert, sslcert, sslkey connection keys.
  • Test suite: 101 tests on SQLite, MariaDB, MySQL 8 (with and without mysqli), PostgreSQL 18 (with and without pgsql) 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. hasMany no longer returns the whole table, belongsTo / hasOne no longer return the first row; eager loading maps every parent.
  • belongsToMany works 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->delete used to delete the row).
  • Mass assignment protection is enforced by new Model($data) and create(); the primary key is never mass assignable.
  • Global scopes and soft deletes cannot be bypassed with orWhere(); withTrashed(), onlyTrashed(), withoutGlobalScope() work.
  • Transaction::run() catches every Throwable, uses the model connection and savepoints for nested calls.
  • $casts are applied on read and write, json_encode($model) works, count() with groupBy() 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 / Crypto use authenticated v2: payloads (AES-256-CBC + HMAC-SHA256); there is no built-in fallback key.
  • Security::getClientIp() trusts forwarding headers only from TRUSTED_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().
  • HttpClient rewrite: keep-alive, parallel pool(), retries with backoff, streamed downloads; XmlClient and SoapClient with real timeouts.
  • Security::rateLimitByIp() (APCu), QueryLogger fed by every query.

Faster

  • exists() runs SELECT 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_COMMAND per MySQL connection.
  • Bulk inserts are chunked to the driver parameter limit; save() skips the UPDATE when nothing changed.
  • HttpClient reuses its cURL handle: five sequential calls took 15 ms instead of 40 ms locally.