druidfi/mysqldump-php
Pure-PHP MySQL dump library that creates SQL backups without shelling out to mysqldump. It supports tables, views, triggers, compression, and configurable output, making portable database backups easy in Laravel and PHP applications.
druidfi/mysqldump-php is a maintained fork of ifsnop/mysqldump-php: a pure-PHP reimplementation of the mysqldump CLI that talks to MySQL over PDO. It matters because it removes the hard dependency on the mysqldump binary and shell exec() calls — useful on shared hosting, containerized PHP-FPM images without the MySQL client, and anywhere proc_open/exec is disabled.
Install and verify:
composer require druidfi/mysqldump-php
php -m | grep -E 'pdo_mysql' # only hard runtime requirement
First use case — dump to a file:
use Druidfi\Mysqldump\Mysqldump;
$dump = new Mysqldump(
'mysql:host=127.0.0.1;dbname=app',
'dbuser',
'secret',
['compress' => Mysqldump::GZIP] // or NONE, BZIP2, GZIPSTREAM
);
$dump->start(storage_path('backups/app-' . date('Ymd-His') . '.sql.gz'));
A Mysqldump instance takes (dsn, user, pass, settings array); start($file) writes the whole dump to disk. That four-line snippet is the 80% case for a Laravel scheduled backup command.
Where to look first: the settings array is the real API surface — it mirrors the mysqldump CLI flags (add-drop-table, include-tables, exclude-tables, no-data, where, skip-triggers, complete-insert, etc.). Read the option list in the README before writing any wrapper, and skim Mysqldump::__construct in src/Mysqldump.php to confirm exact keys for your version — the forks have drifted slightly over the years, so trust the source in vendor/, not blog posts.
Note on the namespace: this fork uses Druidfi\Mysqldump\Mysqldump. Older tutorials assume Ifsnop\Mysqldump\Mysqldump; the class API is largely compatible but the namespace is not — that alone accounts for a large share of "class not found" issues when upgrading.
start() writes to a path. When you don't want to touch local disk (containers, read-only filesystems), use the low-level trio start() / write() / end() with a temp stream, or write to php://output for a download endpoint:
$dump = new Mysqldump($dsn, $user, $pass, [
'compress' => Mysqldump::GZIPSTREAM,
]);
return response()->streamDownload(function () use ($dump) {
$dump->start('php://output');
}, 'backup.sql.gz', ['Content-Type' => 'application/gzip']);
For S3, dump to a tmpfile(), throw it into Storage::put, then fclose. Avoid buffering the whole dump in a PHP string — see gotchas.
The single most valuable pattern in a Laravel app is exporting one subset, not the whole DB:
$dump = new Mysqldump($dsn, $user, $pass, [
'include-tables' => ['users', 'invoices', 'invoice_lines'],
'exclude-tables' => ['audit_logs', 'sessions'],
'where' => 'tenant_id = 42 AND created_at >= "2024-01-01"',
'no-data' => ['sessions'], // schema only
'complete-insert' => true, // column names in INSERTs -> restore-safe
]);
where applies to every table in the dump, so pair it with include-tables and only target tables that actually have the column. Combine with complete-insert so the output survives schema drift if the target has extra columns.
For very large databases, or for fan-out to per-tenant files, list tables first with a plain PDO query against information_schema, then instantiate one dump per table:
$tables = DB::select(
'SELECT table_name FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_type = "BASE TABLE"'
);
foreach ($tables as $t) {
(new Mysqldump($dsn, $user, $pass, [
'include-tables' => [$t->table_name],
'compress' => Mysqldump::GZIP,
]))->start(storage_path("backups/{$t->table_name}.sql.gz"));
}
This gives you per-table retry granularity (a nice property when one giant table fails at 3 a.m.) and lets you skip audit_logs without a settings hack.
Wrap the dump in an Artisan command, guard it with withoutOverlapping(), and do retention pruning after a successful start() — not before:
Schedule::command('db:backup')->dailyAt('02:30')->withoutOverlapping();
Emit the exit code from the command based on the exception; the class throws Druidfi\Mysqldump\Exception (and PDO exceptions) on failure, so try/catch around start() is your success signal. Don't assume a file exists = a valid dump; a partial gzip file is a real failure mode. Optionally verify with a quick gzread() probe on the first bytes.
For CI, migrations, or seeding test fixtures:
['no-data' => true] // schema only — great for migration baselines
['no-create-info' => true] // data only — for seeding
no-create-info + complete-insert is the standard recipe for fixtures you intend to load into an already-migrated database.
The output is intended to be mysql-CLI compatible. Restore with the client (mysql db < backup.sql) rather than trying to parse it in PHP. If you must restore programmatically, split on ;\n carefully — triggers/procedures contain semicolons and will break naive splitters.
Memory is the first wall. The package builds each table's statements in memory before writing. Set 'memory-limit' => '1024M' (or similar) in settings for large tables, and prefer GZIPSTREAM over GZIP — the stream variant compresses incrementally instead of buffering the compressed blob. If a dump dies silently, raise memory_limit and check error_log; the process was almost certainly OOM-killed.
Consistency is best-effort, not InnoDB-consistent. This is not a drop-in replacement for mysqldump --single-transaction on a hot production database. There is no GTID/binlog coordinate captured, and cross-table consistency depends on transaction isolation you didn't set. For backups you intend to restore to a different system, either point it at a replica, take a logical snapshot under FLUSH TABLES WITH READ LOCK yourself, or accept that it's fine for per-tenant exports and dev fixtures but not for PITR.
No CREATE DATABASE / USE statements. The DSN's dbname is implicit in the connection, and the dump does not emit USE. Restore into an existing, pre-selected database (mysql mydb < backup.sql), or the output will land in whatever DB the client defaults to.
DEFINER clauses break restores across users. Triggers, views, procedures, and events are dumped with their DEFINER= user. Restoring to an environment where that user doesn't exist fails or (worse) silently drops the object. Either 'skip-triggers' => true for portable dumps, or post-process the SQL with a regex to strip /*!50017 DEFINER=`...`*/ clauses. Check the README for a skip-definer flag in your version — I'd verify against vendor/druidfi/mysqldump-php rather than assume.
Views must be created after tables. Views are dumped as CREATE VIEW, so on restore you must have base tables present first; and if add-drop-table is on, dropping a view that another view depends on will error. Dump and restore views separately, or use --skip-views and manage them via migrations.
Table and column name quoting. Identifiers are backticked by default. If you have tables with weird names (hyphens, reserved words), verify the escaping against your actual schema — a name that works in a SELECT may still trip a pattern match. include-tables/exclude-tables support % wildcards; exact-string matching silently fails if you typo one, and you'll get a valid but empty dump.
Charset drift causes mojibake and truncated rows. The connection charset controls what PDO returns; the dump does not add a global SET NAMES. If your app connects as utf8mb4 but this tool defaults differently, emoji and 4-byte characters get mangled. Add charset to the DSN (mysql:host=...;dbname=...;charset=utf8mb4) and confirm the dump's header, not just the restore, preserves it.
Timezones. TIMESTAMP columns are emitted in the session timezone. If the server's TZ differs from production's, restores shift times. Pass 'timezone' => '+00:00' (or the corresponding setting in your version) and be explicit.
where does not scope structure. With no-data it's ignored; with schema included, you still get every table's CREATE TABLE. And with multiple tables, a where that references a column absent from one table aborts the whole dump — scope to a single table with include-tables when using it.
Foreign keys and restore order. The dump wraps inserts with SET FOREIGN_KEY_CHECKS=0 only if add-locks/the FK option is enabled. If you emit per-table files (the chunked pattern), each file must independently disable FK checks, or restoring files out of order will fail. Prepend SET FOREIGN_KEY_CHECKS=0; to each restored chunk yourself.
Licensing. GPL-3.0 is copyleft. Running it server-side in a SaaS backend is generally fine (no distribution of the binary), but embedding it in a distributed product — a Laravel desktop app, an on-prem installer, a package you ship — obligates you to release source under GPL-3.0. Check with counsel before bundling; the safe pattern is a runtime composer require by the operator, not a vendored copy.
add-locks and add-drop-table default to true. For a dump you'll apply to a live table, DROP TABLE can be catastrophic if the file is truncated mid-write. For incremental exports, set 'add-drop-table' => false and prefer INSERT ... ON DUPLICATE KEY / truncate-then-insert patterns you control.
Extension points are thin. This isn't a framework package — there's no event bus or plugin system in the classic fork. If you need custom headers (e.g., a -- Set GTID comment) or post-processing, subclass Mysqldump and override the protected write()/header methods, or wrap start() and append to the file afterward. Keep the subclass outside vendor/ so composer update doesn't wipe it.
Debugging tip: set 'compress' => Mysqldump::NONE first. Grepping an uncompressed dump for SET NAMES, DEFINER, and the first INSERT answers 90% of "why did the restore fail" questions faster than reading the class.
How can I help you explore Laravel packages today?