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

Laravel Database Trigger Laravel Package

ntimyeboah/laravel-database-trigger

Add database triggers to your Laravel migrations with a clean, fluent API. Create and drop triggers for insert, update, and delete events, keep trigger logic versioned alongside your schema, and support multiple database drivers without writing raw SQL by hand.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package as a dev dependency:

composer require ntimyeboah/laravel-database-trigger --dev

Because this is a migration-scaffolding tool, there is no service provider, config file, or facade to wire up. The first place to look is the artisan command it registers — run php artisan list | grep -i trigger to confirm the exact command name and signature (it varies between minor releases). You should see something like make:trigger.

Your first use case should be the classic audit-table pattern:

php artisan make:trigger UsersAuditInsert

Open the generated migration in database/migrations/. The scaffold gives you the migration shell and the boilerplate for a trigger. Fill in your CREATE TRIGGER … SQL in up() and the matching DROP TRIGGER … in down(), then run php artisan migrate. Confirm the trigger exists with SHOW TRIGGERS (MySQL/MariaDB) or SELECT * FROM information_schema.triggers (Postgres) before moving on.

Implementation Patterns

Scaffold-only lifecycle. Treat the package as a generator, not a runtime abstraction. Once the migration is generated, you own the SQL. That means you can edit the file freely, commit it like any other migration, and never touch the package API again. This is the cleanest pattern for a low-churn dependency.

One trigger per migration. The scaffold is easiest to review, roll back, and debug when each migration creates exactly one trigger. Name the migration after the trigger (create_users_audit_trigger), and mirror that name verbatim in the DROP TRIGGER in down().

Audit / history triggers. The most common use:

public function up(): void
{
    DB::unprepared(<<<'SQL'
        CREATE TRIGGER users_audit_insert
        AFTER INSERT ON users
        FOR EACH ROW
        INSERT INTO user_audit (user_id, action, created_at)
        VALUES (NEW.id, 'insert', NOW());
    SQL);
}

Do the same for UPDATE and DELETE in separate migrations.

Denormalization / counter caches. Triggers are a good fit for keeping comments_count on posts accurate without touching application code:

DB::unprepared('
    CREATE TRIGGER comments_after_insert
    AFTER INSERT ON comments
    FOR EACH ROW
    UPDATE posts SET comments_count = comments_count + 1 WHERE id = NEW.post_id;
');

Use BEFORE triggers when you need to mutate NEW.column before write; AFTER when the effect is downstream.

Pairing with Eloquent observers. If you already have observers, run a short spike comparing them. Observers are cheaper to test (no real DB required) but can be bypassed by bulk insert(), queue workers, or direct SQL. Triggers catch all of those. A common compromise: observers for app-level events, triggers for invariants that must never fail silently.

Tenant / multi-connection setups. Generate a separate migration per connection by passing --connection=tenant (or whatever flag the command exposes) and keeping the SQL identical. Never try to reuse one migration file across different SQL dialects.

Seeding from a known baseline. If you generate a trigger via make:trigger, always also write the corresponding down() in the same run. Do not leave a trigger un-droppable — CI environments that run migrate:fresh will leak triggers across test runs otherwise.

Gotchas and Tips

SQL is not portable. The command does not translate between MySQL, Postgres, SQL Server, or SQLite. DB::unprepared() passes the string straight to the driver. Write dialect-specific SQL, and use if (DB::connection()->getDriverName() === 'pgsql') guards inside up() if you need cross-DB support.

Use DB::unprepared(), not DB::statement(). Some drivers return unexpected result sets from CREATE TRIGGER; the packaged scaffold uses unprepared for this reason. If you switch to statement(), expect odd PDO warnings on some configs.

Triggers are invisible to the schema builder. Schema::hasTable() will not see them, and migrate:rollback will not drop them for you. The down() method you wrote is the only thing that removes the trigger. Test rollback explicitly: php artisan migrate:rollback --step=1 and check SHOW TRIGGERS.

There is no ->change() for triggers. Any modification means DROP TRIGGER …; CREATE TRIGGER …; in a new migration. Don't try to edit an already-run migration; Laravel won't re-run it, and the trigger name will collide on a fresh environment.

Naming and scope. Trigger names are unique per schema, not per table. Prefix them (users_audit_insert) to avoid collisions in large schemas. On MySQL, DELIMITER is not needed via DB::unprepared() when the trigger has a single statement — but if you include a BEGIN … END block, most drivers still accept it as one string; if you hit a syntax error, use DB::connection()->getPdo()->exec() with the delimiter stripped.

SQLite and testing. SQLite supports triggers with a different syntax. If your test suite uses :memory: SQLite but production is MySQL, you need parallel SQL paths in the migration or a RefreshDatabase shortcut. Many teams simply disable trigger migrations in tests via migrations => [] and assert behavior at the integration layer.

Permission requirements. CREATE TRIGGER needs elevated privileges on MySQL 8 and Postgres (the table owner, or TRIGGER grant). Local dev usually works; managed DBs (RDS, Aurora) frequently work too, but shared hosts may not. Verify early before writing a dozen migration files.

migrate:fresh ordering. If a trigger references another table, the referenced table must exist before the trigger migration runs. Laravel orders by migration timestamp — name your timestamp files so the audit/target tables are created first.

Debugging. SHOW TRIGGERS LIKE 'name'; (MySQL) shows the trigger body. When behavior is wrong, the fastest signal is to SELECT * FROM information_schema.triggers WHERE trigger_name = '...' (Postgres) and compare the stored body to what you wrote — whitespace and quoting issues are common.

Extension point. Because the file is a plain migration, you can call back into Laravel freely: DB::table(), Artisan::call(), config lookups, etc. There's no sealed API. Use that to inject environment-specific behavior (e.g., skip creation when app()->environment('testing')).

Version pinning. With 20 stars and a February 2025 release, the package has no meaningful ecosystem around it. Pin the version in composer.json ("ntimyeboah/laravel-database-trigger": "^1.0") and be prepared for the command signature to drift between versions. Review the scaffold output on upgrade.

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.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log