waad/truffle
Truffle is a Laravel package that adds simple, flexible search to your Eloquent models. Define searchable fields, build expressive queries, and filter results with a clean, chainable API—no external search engine or complex setup required.
Truffle's job is narrow: it lets Eloquent models talk to an in-memory SQLite connection instead of your real MySQL/Postgres one. Practically, that means tests (and throwaway prototype scripts) can boot a schema, run factories, and assert against models without a database server, a docker-compose service, or a CI service container.
Minimal steps
composer require waad/truffle --dev
php artisan vendor:publish --tag=truffle-config # check README for the exact tag
truffle connection (driver sqlite, database :memory:) is registered and where you can see the trait/hook the package exposes.dd(DB::connection('truffle')->getDriverName()); // "sqlite"
config/truffle.php, then the package src/ for the trait or connection resolver it ships. Read that source before the README — at 51 stars and zero dependents, the source is the documentation.First use case — migrate one model and one test, not the whole app:
// app/Models/Invoice.php
protected $connection = 'truffle'; // or use the package trait, if provided
// tests/Feature/InvoiceTest.php
protected function setUp(): void
{
parent::setUp();
$this->artisan('migrate', [
'--database' => 'truffle',
'--path' => 'database/migrations/truffle',
])->run();
}
public function test_it_totals_lines(): void
{
$invoice = Invoice::factory()->hasLines(3)->create();
$this->assertSame(3, $invoice->lines()->count());
}
If that green bar appears, you've validated the whole premise in one test. Then decide whether to widen adoption or stop.
Opt-in per model, not globally. Set $connection on the handful of models you actually test in isolation. A global Model::setConnectionResolver() swap is tempting and will eventually poison a test that needed the real DB (JSON columns, generated columns, whereRaw with MySQL functions).
Schema bootstrap belongs in a base TestCase, not in every test.
abstract class TruffleTestCase extends TestCase
{
protected function setUp(): void
{
parent::setUp();
foreach (glob(database_path('migrations/truffle/*.php')) as $file) {
(require $file)->up();
}
}
protected function tearDown(): void
{
foreach (array_reverse(glob(database_path('migrations/truffle/*.php'))) as $file) {
(require $file)->down();
}
parent::tearDown();
}
}
Keeping a separate, hand-written migration set for the in-memory schema is more work than reusing your app migrations, but it lets you drop things SQLite can't express (fulltext indexes, jsonb, partial indexes) instead of fighting them. Start by reusing database/migrations with --path; fork only when a migration fails.
Pest. A beforeEach/afterEach pair or a uses(TruffleTestCase::class) at the top of the file is the cleanest distribution mechanism — it makes the in-memory decision visible in the test file rather than hidden in phpunit.xml.
Factories just work if the model's $connection is set, because factory()->create() resolves the connection from the model. Seeders do not — $this->seed() runs against the default connection; call DB::connection('truffle')->table(...) or set $connection on the seeder.
Relationships: keep the whole graph on one connection. Lazy/eager loading across connections works (separate queries), but whereHas, withCount, has(), and any join compile into a subquery on the parent's connection and will fail or silently return wrong results. If Invoice is truffle-connected, InvoiceLine must be too.
Don't mix and match in one test. A test that writes User to MySQL and Invoice to truffle is a test that can't be reasoned about, and DB::transaction() inside it will only wrap the default connection.
CLI integration, when you need it:
php artisan migrate --database=truffle # only useful for a *file* sqlite db
php artisan db:seed --database=truffle
Note that these are only meaningful when the database is a file, not :memory: — see the gotcha below.
CI payoff. Removing the mysql:8 service container and the health-check wait loop is usually 30–90 seconds per job and one fewer flaky failure mode. That's the real business case for evaluating this package.
:memory: connection name is a separate database. DB::connection('truffle') and DB::connection('truffle_read') do not share storage. Use one connection name, or file::memory:?cache=shared (which reintroduces cross-test bleed). This is the single most common source of "why is my table missing?"$this->artisan('migrate', ['--database' => 'truffle']) does work in tests because Laravel runs it in-process; php artisan migrate --database=truffle from a terminal does not, because the PDO dies with the process. Anything that shells out — queue workers, php artisan serve, Dusk, Process::run() — will never see your in-memory data.RefreshDatabase transacts the default connection only. Set protected $connectionsToTransact = ['truffle']; (and remember it migrates the default connection, which you probably don't want). For truffle, DatabaseMigrations or manual up()/down() is usually the right fit; :memory: already dies at process exit, so you may not need transaction wrapping at all.config:cache freezes the connection array. If you register the connection at runtime in a service provider, cached config in CI will drop it. Publish the config file and keep the definition there.lockForUpdate()/sharedLock() are no-ops, LIKE is case-insensitive for ASCII, ALTER TABLE support is limited, JSON functions differ, strict typing is off. Tests that pass here can still fail in production — that's an accepted trade, but don't let truffle become the only place a query is exercised. Keep one scheduled job on the real DB.setUpBeforeClass, ids drift and assertDatabaseCount gets brittle. Prefer a fresh up() per test; the cost is milliseconds.DB::table('invoices'), DB::select(...), and DB::transaction(fn () => ...) all hit the default connection unless you chain ->connection('truffle'). Grep for DB:: in code under test before you trust the result.DB::connection('truffle')->listen(fn ($q) => dump($q->sql, $q->bindings)); and DB::connection('truffle')->getSchemaBuilder()->getTables() tell you almost everything. ->pretend() is useful for asserting a query compiles under SQLite's grammar before you run it.Connection::resolverFor('truffle', ...) if you want a custom connection class; a trait on your base model for getConnectionName() if you want env-driven switching (TRUFFLE=1); and a Model::resolveConnection override in a service provider for a global, opt-out swap.TruffleTestCase and base trait, and keep a --database=mysql toggle in CI so ripping the package out is a one-file change rather than a refactor.How can I help you explore Laravel packages today?