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

Tdbm Laravel Package

thecodingmachine/tdbm

TDBM (The Database Machine) is a fast, lightweight PHP ORM that generates beans and DAOs directly from your database schema. A simple, strongly typed alternative to Doctrine with no annotations and an explicit, easy-to-learn API.

Deep Wiki
Context7

Getting Started

TDBM is a database-first ORM: you don't write entities and mappings, you point it at an existing schema and it generates bean classes (one per table) plus DAO classes (one per table) with typed getters/setters and ready-made finders. That inverts the usual Doctrine/Eloquent workflow, and it's the whole reason to pick it.

Minimal path to first value:

composer require thecodingmachine/tdbm doctrine/dbal
// tdbm.php (project root) — consumed by the CLI
use Doctrine\DBAL\DriverManager;
use TDBM\Configuration;

$connection = DriverManager::getConnection(['url' => getenv('DATABASE_URL')]);

return new Configuration(
    $connection,
    'App\Bean',   // generated bean namespace
    'App\DAO',    // generated DAO namespace
    $cache        // any PSR-16 SimpleCache
);
vendor/bin/tdbm generate        # writes App\Bean\* and App\DAO\* from the live schema

Then:

$tdbm = new TDBMService($configuration);
$userDao = $tdbm->getDao(App\Bean\User::class);

$user = $userDao->getById(42);
$active = $userDao->findByStatus('active');   // magic finder, returns a ResultIterator

Where to look first, in this order:

  1. The generated DAO classes — their @method docblock is your API reference. TDBM lists every magic findBy* / findOneBy* / countBy* it has synthesised from your columns and foreign keys.
  2. TDBM\Configuration and TDBMService (connection, namespaces, cache, getDao()).
  3. TDBM\Dao\AbstractDao (getById, findAll, createFilter, save, delete).
  4. TDBM\Filter\Filter and TDBM\ResultIterator.

First real use case: retrofitting an admin/back-office or internal tool onto a legacy MySQL/Postgres schema you inherited. Twenty tables become queryable in an afternoon, with no YAML, no attribute plumbing, and no argument about whether the DB or the code is the source of truth — the DB wins, by design.

Implementation Patterns

Wire one TDBMService per connection in your DI container (Symfony service definition, Laravel binding, plain PSR-11 factory). It's cheap to construct; the expensive part is metadata, which the PSR-16 cache absorbs. Inject DAOs, not the service, into your application services — UserDao is a clean, mockable dependency.

Querying: three tiers, in increasing power.

// 1. Generated magic finders — zero cost, covers 80% of reads
$userDao->getById($id);
$userDao->findByEmail('[email protected]');
$userDao->findOneByEmail('[email protected]');              // single bean or null
$userDao->findByCountry($countryBean);          // FK-aware: accepts the bean
$userDao->findByStatus('active');               // ResultIterator, countable, iterable

// 2. Filter — for anything with an ORDER BY, LIMIT or a second predicate
$filter = $userDao->createFilter();
$filter->where('status = :status', ['status' => 'active'])
       ->orderBy('created_at DESC')
       ->limit(0, 50);
$users = $userDao->findByFilter($filter);

// 3. Hand-written SQL through the DBAL connection for reporting/joins
//    TDBM does not try to be a query builder for analytics.

Magic finders are compiled from the schema, so their names drift when columns are renamed — treat regeneration diffs as an API diff and let static analysis catch the breakages.

Persistence is explicit, not unit-of-work. There is no identity map and no change tracking. Mutate the bean, then call save(); there's no flush.

$user = new User();
$user->setEmail('[email protected]');
$userDao->save($user);          // INSERT; autoincrement PK is written back onto the bean

$user->setStatus('banned');
$userDao->save($user);          // UPDATE by primary key

$userDao->delete($user);

Wrap multi-bean writes in a DBAL transaction, and save parents before children so FK constraints are satisfiable. For many-to-many, the join table gets its own generated DAO/bean pair — manipulate it explicitly rather than expecting collection magic.

Custom logic lives in the DAO. Add your own methods (findActiveAdmins(), promoteToAdmin()) to the generated DAO class. This is the intended extension point and matches TDBM's active-record-ish philosophy: query logic next to the table it concerns. Verify your generator version's overwrite behaviour on the first regenerate (see below) and commit generated code so the diff is visible.

Workflow discipline: schema changes come first.

write SQL / Doctrine Migrations / Phinx migration
  → apply to dev DB
  → vendor/bin/tdbm generate
  → clear metadata cache
  → commit generated beans + DAOs alongside the migration

TDBM manages no schema at all. Pairing it with Doctrine Migrations purely as a migration runner is a common, sane combination — you get versioned DDL without adopting the ORM.

Testing: TDBM has no fixture/seeding layer, so seed with raw SQL (or your migration stack's fixtures) against a real database — SQLite in-memory for speed if your generated code was produced from a compatible engine, otherwise a throwaway MySQL/Postgres container. No lazy loading means no hidden queries, which makes integration tests pleasantly deterministic.

Gotchas and Tips

It is not Doctrine. No lazy loading, no identity map, no dirty checking, no cascade, no flush(). If you're reaching for a relation graph three levels deep, you're writing joins by hand. Budget for that in estimation — teams that expect Eloquent/Doctrine ergonomics underestimate TDBM on object graphs and overestimate it everywhere else.

Regeneration and your custom code are the number-one operational risk. The generator rewrites DAO and bean files; depending on version it preserves existing method bodies and refreshes only the generated annotation/method blocks, or it clobbers. Pin this down on day one with a throwaway method, then keep generated files under version control and read the diff after every regenerate. Ideally run tdbm generate in CI against a schema-only database and fail the build if the diff is non-empty — that catches "someone changed the DB and forgot to regenerate" before it becomes a runtime findByOldColumnName fatal.

Stale metadata cache is the classic mystery bug. You rename a column, regenerate, and the app still queries the old name. Any code change to the schema or to the generated set must be paired with a cache clear (deploy step, cache:clear, or a cache namespace/key bump tied to a build hash). Use array cache in tests, APCu/filesystem in production.

save() writes every column. No partial updates: two concurrent requests both loading and saving the same bean means last-write-wins on all fields, silently. For anything with concurrent editors, either re-read inside a transaction, update via targeted SQL, or add a version column and enforce it yourself — TDBM won't.

Generation is bound to the database you generated from. Column types, enum handling, defaults, and platform quirks (MySQL TINYINT(1) → bool, Postgres native enums, unsigned ints) get baked into bean setters. Generate from the same engine and major version as production, or you'll ship type coercion bugs that only appear after deploy.

Awkward schemas get awkward APIs. Tables without a primary key lose getById(); composite keys produce multi-arg lookups; reserved words and columns like 2fa_enabled yield unlovely method names. TDBM's naming strategy is configurable in Configuration — worth ten minutes on a legacy schema rather than living with findByUser2faEnabled() forever.

Raw where() strings are raw SQL. Bind everything through the params array; the Filter accepts interpolated SQL and will happily pass an injection through. When debugging, log the compiled SQL and bound parameters from the filter rather than guessing — most "wrong results" reports are a mis-scoped where() or a missing orderBy on a paginated query.

Debugging is source-reading. With ~120 stars and a small maintainer base, expect to read vendor/thecodingmachine/tdbm/src/ rather than find a Stack Overflow answer. The upside: the codebase is compact and legible. The downside: no lazy-loading means an unbounded findAll() in a loop is a real N+1 generator — iterate ResultIterator deliberately and page with limit().

Budget for bus factor. Single-maintainer, niche adoption. Vendor the lockfile, keep the version pinned, and be prepared to fork if a DBAL major version moves past you. For a legacy-schema admin tool that's a fine trade; for the core of a ten-year product, weigh it against Doctrine + a mapping-generation step you control.

Extension points worth knowing: the PSR-16 cache (swap for Redis in a multi-node deploy), the configurable naming strategy, subclassing Filter for domain-specific query objects, and overriding any generated DAO method in place. Everything else — connection pooling, logging, transactions — comes from the DBAL connection you hand it, so configure those there.

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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core