marktopper/doctrine-dbal-timestamp-type
A Doctrine DBAL custom type that maps SQL TIMESTAMP columns to PHP DateTime objects, making timestamp columns work correctly with Laravel's Doctrine integration. Simple to register and use in your entity mappings.
Install via Composer:
composer require marktopper/doctrine-dbal-timestamp-type
The package provides a single Doctrine DBAL type (MarkTopper\DoctrineDBALTimestampType\TimestampType) that teaches DBAL the SQL TIMESTAMP column type — something DBAL does not ship out of the box (it has datetime, datetimetz, date, and time). This matters most in Laravel when Doctrine DBAL is used under the hood for operations like ->change() on a timestamp column, or when introspecting an existing schema that contains TIMESTAMP columns.
Look first at the package's src/ directory — it is a small class that extends DBAL's DateTimeType and overrides getName() / getSQLDeclaration(). Read that before anything else; the entire surface area is one class.
Register the type before any schema work happens — typically in AppServiceProvider::boot():
use Doctrine\DBAL\Types\Type;
use MarkTopper\DoctrineDBALTimestampType\TimestampType;
public function boot(): void
{
if (! Type::hasType('timestamp')) {
Type::addType('timestamp', TimestampType::class);
}
}
First use case: modifying a timestamp column in a Laravel migration that previously failed with Unknown column type "timestamp" requested:
Schema::table('users', function (Blueprint $table) {
$table->timestamp('email_verified_at')->nullable()->change();
});
Guard registration in a service provider. DBAL throws if you call Type::addType() for an already-registered type. Both AppServiceProvider and package auto-discovery can run, so always wrap in Type::hasType().
Mapping platform types to the custom type. For schema introspection to recognize existing MySQL TIMESTAMP columns, register the platform mapping on the connection:
DB::connection()->getDoctrineConnection()
->getDatabasePlatform()
->registerDoctrineTypeMapping('timestamp', 'timestamp');
Call this once per connection after the type is registered.
Laravel ->change() workflows. Any migration that mutates a timestamp column (nullable, default, precision, unsigned) goes through DBAL and therefore benefits from this type being present. Use it together with a modern doctrine/dbal 3.x install, since DBAL is a hard dependency for ->change().
Doctrine entity usage. With doctrine/dbal or barryvdh/laravel-doctrine, you can declare the column explicitly:
#[Column(type: 'timestamp', nullable: true)]
private ?\DateTimeImmutable $verifiedAt = null;
The type extends DateTimeType, so value coercion to DateTime/DateTimeImmutable behaves as expected — you get the same conversion behavior as datetime, only the SQL declaration differs.
Schema tools / SchemaBuilder. When building tables programmatically through the DBAL schema API:
$table->addColumn('created_at', 'timestamp', ['notnull' => false]);
Testing. Register the type in a base TestCase::setUp() or globally in tests/TestCase.php so schema-introspection tests don't silently pass with datetime behavior and fail in production where the actual column type is TIMESTAMP.
Do not confuse layers. Laravel's $table->timestamp() builder method and this package are different things — the former creates a TIMESTAMP column already; the latter teaches Doctrine that the returned type name exists. If you never touch DBAL (no ->change(), no schema introspection, no Doctrine entities), you probably don't need this package.
DBAL re-registration errors. Type::addType() throws DBALException when a type name already exists. Guard it, and be mindful that in Octane or in long-running workers the boot method may execute per request/worker — the guard handles this.
MySQL TIMESTAMP semantics leak through. MySQL stores TIMESTAMP in UTC and converts by session timezone, and implicitly supports ON UPDATE CURRENT_TIMESTAMP. This package only handles the PHP/DBAL type mapping; it does not change MySQL's auto-update or TZ behavior. If you rely on the auto-update, declare it in the migration default string rather than expecting the type to emit it.
Range limits. MySQL TIMESTAMP is limited to roughly 1970–2038 (2038 problem) on older versions. For columns that need to store dates outside that window, datetime is the safer choice. Only reach for timestamp when you specifically need UTC-normalized storage.
Cross-platform SQL. The type's SQL declaration is TIMESTAMP, which is valid on MySQL, MariaDB, and PostgreSQL but not on every platform (SQLite treats it as text-like). If you target SQLite for tests and MySQL for production, test the schema diff, not just the value round-trip.
Version compatibility. Last release is 2023; verify against your doctrine/dbal version (3.x is the safe bet). If you upgrade to DBAL 4, check that the class's overridden methods still match — DBAL 4 dropped several deprecated type APIs and moved to stricter signatures.
Extension point. Because it subclasses DateTimeType, overriding convertToDatabaseValue() is the cleanest way to enforce a specific storage format (e.g. 'Y-m-d H:i:s') or to strip microseconds before persisting, rather than trying to configure this at the connection level.
Debugging. If schema introspection still fails, dump what DBAL sees: DB::connection()->getDoctrineSchemaManager()->listTableColumns('users') and inspect the returned type names. A mismatch (timestamp vs datetime) usually means the platform mapping wasn't registered for that connection.
How can I help you explore Laravel packages today?