/ Upgrade Guide

Upgrade Guide (1.x to 2.x)

2.0 fixed many 1.x behaviours that returned wrong rows or were unsafe, so some code has to change. 2.1 adds async queries and changes nothing else for existing code (except SQL Server number types, below). Test your application against a copy of the database before you switch.


Checklist

  1. PHP 8.1 or newer.
  2. Primary key default is now Id (was id).
  3. Timestamps are opt-in: add use HasTimestamps; to models that have CreatedDate / UpdatedDate.
  4. List mass-assignable columns in $fillable (or use $guarded).
  5. Replace expressions in select() / where() / orderBy() with the *Raw() methods.
  6. Replace User::fresh() / User::refresh() (migration helpers) with User::recreateTable().
  7. Set ENCRYPTION_KEY in .env and re-encrypt old FormCrypt / Crypto data.
  8. Review HttpClient calls that used the $async parameter.

Breaking changes

1.x2.x
PHP 8.0PHP 8.1+
$primaryKey = 'id'$primaryKey = 'Id'
Timestamps always onuse HasTimestamps; to enable (const CREATED_AT / UPDATED_AT to rename)
create() / new Model($data) ignored $fillable$fillable / $guarded enforced, primary key always guarded; forceCreate() / forceFill() skip the check
Relation keys guessed as user_id{Model}{PrimaryKey}, e.g. UserId (user_id when the primary key is id)
select('CONCAT(a,b)'), where('DATE(x)', ...)selectRaw(), whereRaw(), whereDate(); COUNT/SUM/AVG/MIN/MAX(col) and col AS alias still work
$model->update([...]) could update the whole tableupdates that row only; mass updates need a query: User::where(...)->update([...])
$model->delete (property) ran the methodproperty access only loads relations declared in your model
User::fresh() / User::refresh() (migration)User::recreateTable(); fresh() / refresh() now reload a model
Query delete() hard-deleted soft-delete modelssoft deletes; forceDelete() removes rows
whereLike() passed % / _ throughescaped; whereLike($col, $pattern, false) for a raw pattern
latest() used CreateDateCreatedDate (or the model's created-at column)
Query\QueryBuilder::getBindings() returned a listnamed bindings [':q1_0' => value]
autoload registered error handlers and ../Cargo/bootstrap.phpopt-in MIKO_REGISTER_ERROR_HANDLERS, no Cargo include
FormCrypt fallback key, no MACENCRYPTION_KEY required, v2: payloads; decryptLegacy() reads 1.x data
Security::getClientIp() trusted X-Forwarded-Foronly from TRUSTED_PROXIES / setTrustedProxies()
HttpClient::get($url, $headers, $async)get($url, $headers, $query); parallel calls with pool() or getAsync()
HttpClient::request(..., $async, $multipart)request($method, $url, $body, $headers, $options) with query, form, multipart, sink, timeout, retries
HttpClient::post($url) sent []sends an empty body; post/put/patch default $data = null
ORM\ConnectionPool::acquire() waited 30 s when fullthrows immediately
DbContext::ensureCreated() printed table namesreturns them; Migrator::ensureDatabaseExists() is silent, seeders print on the CLI only
Config/Database.php persistent onpersistent off by default; MySQL lc_time_names opt-in (DB_LC_TIME_NAMES); migrations table __migrations
SQL Server returned int columns as stringsint / float come back as int / float (2.1); bigint, decimal and money stay strings

One shared connection

Models, Transaction, DB and the builders use the same default connection, resolved in this order:

  1. ConnectionResolver::setDefault($connection) (or a DbContext you created),
  2. the last DbConfig::...()->connect(),
  3. the default connection of Config/Database.php (.env).

So a Transaction::run() now really contains the model writes made inside it.


Re-encrypting old data

use Miko\Security\FormCrypt;

$plain = FormCrypt::decryptLegacy($row['Token']);   // 1.x payload
if ($plain !== false) {
    DB::execute('UPDATE Tokens SET Token = ? WHERE Id = ?', [FormCrypt::encrypt($plain), $row['Id']]);
}

Crypto::decryptLegacy($data, $key) does the same for Crypto values.


Running the tests on your server

The library ships its test suite. Run it against an empty database before upgrading production:

php tests/run.php                                   # SQLite in a temp folder
MIKO_TEST_DRIVER=mysql MIKO_TEST_HOST=127.0.0.1 MIKO_TEST_PORT=3306 MIKO_TEST_DATABASE=miko_test \
MIKO_TEST_USERNAME=root MIKO_TEST_PASSWORD=secret php tests/run.php
php -d extension=soap tests/http.php                # HTTP clients

The database must exist; the suite drops its miko_t_* tables at the end. The full list of changes is in CHANGELOG.md.