scandio/troba
Troba is a lightweight Laravel package from Scandio for adding database-backed search to Eloquent models. It provides a simple searchable trait and query helpers so you can find records without an external search service, keeping setup minimal and search logic close to your models.
Troba is a standalone PDO wrapper with annotation-mapped entities and a fluent query builder — it is not a Laravel package and has no service provider, facade, or Eloquent integration. Treat it as a library you bolt on, not a dependency you require casually.
Before anything else, check the hard constraints:
composer.json declares PHP ~5.5. That means no PHP 7.x/8.x support: no scalar type hints in its internals, no Throwable handling, and almost certainly no PHP 8 compatibility (dynamic properties, PDO::ATTR_* deprecations, curly brace string offsets). If your app is Laravel 8+ (PHP 7.3+ minimum, realistically 8.1+), you cannot run this in-process without a PHP 5.5/5.6 container.composer.json, never dev-master, and vendor-lock it.Where to look first, in order:
README.md — for the bootstrap/configuration contract (connection setup and entity base class).tests/) — this is the real documentation for an 11-star archived library. Look for entity fixtures and query-builder assertions; they show exact annotation syntax and expected SQL.table, column, id, relation tags) versus what the README claims.NULL/IN.Minimal bootstrap: give troba a live PDO handle (reuse Laravel's so transactions are shared), point it at a config object or static config, then map one existing table to a plain entity class with annotations on properties. Do a single read first:
// conceptual shape — confirm exact class/method names against the repo
$pdo = DB::connection()->getPdo();
// configure troba's entity manager / config with $pdo
$user = $em->find(User::class, 42); // or via the static facade
$active = $em->findBy(User::class, ['status' => 'active']);
First use case to pick: read-only access to a legacy table you don't want to model in Eloquent — reporting, an admin lookup, an import source. Do not start with writes; persistence/flush semantics in an archived, undocumented library are where you'll lose days.
Wrap it behind repositories, immediately. The single highest-value pattern: never let EntityManager/EQM calls leak into controllers or jobs. One repository class per aggregate, returning your own DTOs or Eloquent models. This is also your migration escape hatch — when you replace troba with Eloquent or Doctrine, you rewrite the repository bodies only.
final class LegacyOrderRepository
{
public function __construct(private EntityManager $em) {}
public function findOpen(int $customerId): array
{
return array_map(
fn ($row) => OrderDto::fromRow($row),
$this->em->findBy(Order::class, ['customer_id' => $customerId, 'status' => 'open'])
);
}
}
Bind the manager as a singleton. Construct it once in a service provider with the shared PDO handle, and inject it. Avoid the static facade in application code — statics make tests order-dependent and parallel test runs flaky (global config + global connection).
$this->app->singleton(EntityManager::class, fn () => new EntityManager(
DB::connection()->getPdo(),
config('troba')
));
Share Laravel's PDO instance. If troba opens its own connection, DB::transaction() will not roll back troba's writes, and you'll get phantom rows in tests and half-committed data in production. Pass DB::connection()->getPdo() and use DB::transaction() as the single transaction boundary. If troba wraps its own transactions, keep them strictly inside the Laravel one and rely on savepoints.
Read path: builder → query → hydration. The idiomatic flow is a fluent builder producing a query object, executing against PDO, and hydrating into entities (or a raw array result if you want to skip hydration overhead). For reporting, prefer the array/row result — entity hydration is the expensive part and buys you nothing if you immediately serialise to JSON.
Relations: go eager by hand. Annotation-declared relations are typically lazy, and an archived library will not have a convincing N+1 solution. In list endpoints, issue a batched query for the related IDs (WHERE id IN (...)) and stitch in PHP. Budget for this explicitly; it's the most common performance trap.
Map legacy schemas explicitly. Use the column annotation to bridge snake_case DB columns to camelCase properties rather than renaming columns or adding accessors. Keep one entity class per table, no inheritance cleverness — the metadata parser is the least-tested part of the codebase.
Test against SQLite in-memory with schema fixtures. Troba's SQL generation is driver-sensitive (identifier quoting, LIMIT syntax). Run the bulk of your tests on SQLite for speed, plus a small suite on MySQL/Postgres for the driver-specific paths. Wrap each test in a transaction and rollBack() in tearDown.
Migration strategy: introduce the repository boundary and DTOs on day one, then replace repository internals with Eloquent one aggregate at a time. Because troba entities are dumb data holders, DTO construction is cheap and the swap stays mechanical.
use in the annotation string typically yields a default-mapped entity or an empty result set, not an exception. When a query returns nothing, dump the resolved metadata (table name, column map, primary key) before debugging the SQL.opcache.save_comments=0) break entity metadata in production only. Verify this setting and, if there's a metadata cache, warm it during deploy.int, bool, and DateTime columns come back as strings unless you cast. Cast in the DTO/from-row step, not in the entity, so the mapping stays obvious.IN () with an empty array is a classic builder bug in small libraries — it emits invalid SQL or matches everything. Guard the call site.IS NULL vs = NULL. Fluent builders frequently translate ['col' => null] into col = NULL, which never matches. Use the explicit isNull/notNull method if it exists, or raw SQL fragments.order, key, group) may work on MySQL and fail on Postgres or vice versa. Check what the builder emits for your production driver, not your test driver.phpunit --parallel will interleave state. Run these tests serially, or refactor config into the injected manager instance.$sql and $params before delegating. That five-line class is the single best investment you'll make in this dependency.PATCHES.md, and pin the upstream commit you diffed against. Upstream will never accept a PR.DB::select() with DTO mapping covers every use case here with maintained code and PHP 8 support. Troba is justified only for maintaining an existing PHP 5.5 codebase that already depends on it, and even then the correct roadmap is extraction, not expansion.How can I help you explore Laravel packages today?