Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Think Orm Laravel Package

topthink/think-orm

ThinkORM is the PHP ORM behind ThinkPHP, offering ActiveRecord-style models, a fluent query builder, relationships, events, soft deletes, and multi-database support. Use it with ThinkPHP or standalone for expressive, efficient database access.

View on GitHub
Deep Wiki
Context7

Getting Started

topthink/think-orm is a standalone PHP ORM/query-builder that also powers ThinkPHP. It can be used outside the framework with a small bootstrap.

composer require topthink/think-orm

Minimal bootstrap (no ThinkPHP app needed):

use think\facade\Db;

Db::setConfig([
    'default'     => 'mysql',
    'connections' => [
        'mysql' => [
            'type'     => 'mysql',
            'hostname' => '127.0.0.1',
            'database' => 'app',
            'username' => 'root',
            'password' => '',
            'hostport' => '3306',
            'charset'  => 'utf8mb4',
            'prefix'   => '',
        ],
    ],
]);

$user = Db::name('user')->where('id', 1)->find();

Start with two entry points:

  1. think\facade\Db — query builder for reports, migrations, bulk operations, and anything where you want explicit SQL-like control.
  2. think\Model — active-record models for domain entities, relations, events, casts, and serialization.

First useful use case: replace hand-written PDO with Db::name('table') for reads and a single Model subclass for writes. Then add relations and eager loading.

Where to look first:

  • think\db\Query for available builder methods (where, whereIn, whereRaw, field, order, page, chunk, cursor, etc.).
  • think\Model and think\model\concern\* for attributes, casting, soft deletes, and serialization.
  • think\db\connector\* for connection-specific behavior.
  • ThinkPHP database docs apply almost directly to this package.

Implementation Patterns

Query builder for reads, models for writes

Use the builder for ad-hoc queries and reporting:

$rows = Db::name('orders')
    ->field('status, COUNT(*) AS total')
    ->whereBetween('created_at', [$start, $end])
    ->group('status')
    ->select();

Use models when behavior matters:

class User extends \think\Model
{
    protected $name = 'user';           // table without prefix
    protected $pk = 'id';
    protected $autoWriteTimestamp = 'datetime';

    protected $type = [
        'id'     => 'integer',
        'config' => 'json',
    ];

    public function posts()
    {
        return $this->hasMany(Post::class, 'user_id', 'id');
    }
}

$user = User::find(1);
$user->name = 'Alice';
$user->save();

Relations and eager loading

Avoid N+1 by using with() and withCount():

$users = User::with([
    'posts' => fn($q) => $q->where('published', 1)->limit(5),
])->withCount('posts')->select();

foreach ($users as $user) {
    echo $user->posts_count;
}

Use hasWhere() / has() when filtering parents by relation existence:

User::hasWhere('posts', ['published' => 1])->select();

Transactions

Prefer the closure form so rollback happens automatically:

Db::transaction(function () use ($data) {
    $order = Order::create($data);
    OrderItem::saveAll($data['items']);
    return $order;
});

For manual control use Db::startTrans(), Db::commit(), Db::rollback(). Nested transactions use savepoints where supported.

Batch jobs: chunk() and cursor()

chunk() is the standard approach for large updates, but sort by a stable, immutable key:

Db::name('user')->where('synced', 0)->chunk(500, function ($users) {
    foreach ($users as $user) {
        // process
    }
});

cursor() streams rows with lower memory but holds the connection open longer; don’t run concurrent queries on the same connection inside the loop.

Events and observers

Model lifecycle hooks are where you put cross-cutting behavior:

class User extends \think\Model
{
    protected $event = [
        'before_insert' => 'hashPassword',
        'after_delete'  => 'purgeCache',
    ];
}

Use observer classes for anything non-trivial. Keep events idempotent — they fire on save(), create(), delete(), and relation saves, but not on insertAll() / saveAll() in every version, so don’t rely on them for bulk paths.

JSON columns, soft deletes, serialization

protected $json = ['config'];
protected $jsonAssoc = true;

use \think\model\concern\SoftDelete;
protected $deleteTime = 'delete_time';
protected $defaultSoftDelete = null;

Then:

User::withTrashed()->find(1)->restore();
User::onlyTrashed()->select();
User::destroy(1); // soft delete

Hide or expose fields at serialization time:

$user->hidden(['password'])->toArray();
$user->append(['full_name']);

Pagination

In a framework context paginate() just works. Standalone, either bind a request/page source or drive it manually:

$page  = max(1, (int)($_GET['page'] ?? 1));
$size  = 20;
$total = Db::name('user')->count();
$rows  = Db::name('user')->page($page, $size)->select();

Use paginate($size, false, ['page' => $page]) when you want the Paginator object without the framework’s request resolver.

Gotchas and Tips

  • name() vs table(): Db::name('user') applies the configured table prefix; Db::table('user') does not. Mixing them is the most common source of “table not found” errors.
  • find() returns null when no row matches. Use findOrFail() if you want an exception; don’t call ->save() on a null result.
  • whereOr grouping: chained whereOr calls can leak out of an intended group. Use a closure: ->where(function ($q) { $q->where(...)->whereOr(...); }).
  • where('col', null) compiles to IS NULL. If you need = NULL semantics you’re probably looking for whereNull() / whereNotNull().
  • whereIn with an empty array typically compiles to a false condition (0 = 1). Guard against empty ID lists to avoid confusing SQL.
  • Timestamps: set $autoWriteTimestamp explicitly ('int', 'datetime', or 'timestamp'). A mismatch between int columns and datetime casting produces silently wrong values.
  • insertAll() / saveAll() skip model events and may skip automatic timestamps depending on version. For audit-critical writes, loop create() or set timestamps manually.
  • save() returning true does not always mean a query ran — an unchanged model may short-circuit. Use getChangedData() or check isExists() when behavior depends on it.
  • Empty updates: calling update([]) throws. Guard with if (empty($data)) return;.
  • chunk() with a mutable sort column: updating the field you ordered by can skip or duplicate rows. Order by primary key or a frozen column.
  • Query caching requires a configured cache handler. ->cache(true, 60) silently behaves differently or errors if no cache driver is bound; configure think\facade\Cache before relying on it.
  • Debugging SQL: $query->getLastSql() works per query instance; Db::listen(fn($sql, $time, $explain) => ...) gives global visibility. fetchSql(true) returns the SQL string instead of executing.
  • Long-running workers: connections are reused. Call Db::close() or reconnect between jobs to avoid stale connections, “server has gone away,” and transaction state leaking across requests.
  • Read/write splitting is configured through deploy, rw_separate, master_num, and slave_no. Reads may hit a replica immediately after a write; use ->master(true) when consistency matters.
  • Model field whitelisting: $field / $schema can block mass assignment. If a field silently won’t save, check the model’s allowed-field definitions before debugging the database.
  • Soft deletes affect every query on that model. Use withTrashed() for admin views and remember destroy() is not a hard delete unless you call force()->delete().
  • Raw SQL: always bind parameters — whereRaw('id > ?', [$id]) — never interpolate user input.
  • Composite primary keys are not a first-class model feature in the typical ThinkORM model flow; prefer query-builder updates or a surrogate key.
  • Config timing: call Db::setConfig() before the first query. If you change config at runtime, close the existing connection so the new settings are picked up.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo