Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion lib/Factories/Column.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ final class Column
protected ?array $typeArgs = null;
protected array $attributes = [];

/**
* @var callable|null
*/
protected $phpDefault = null;

/**
* @param string $name
* @param string $type
Expand Down Expand Up @@ -60,4 +65,38 @@ public function getAttributes(): array
{
return $this->attributes;
}
}

/**
* Provides a PHP-side default value for this column.
*
* When set, the datastore layer fills in this value at create time for any
* insert that does not already include the column. The value flows into the
* INSERT and into the in-memory model that `create()` returns, which removes
* the need for a post-insert read-back to capture DB-generated defaults.
*
* Pair this with the matching DB-side DEFAULT (e.g. `DEFAULT CURRENT_TIMESTAMP`)
* for belt-and-suspenders coverage when rows are inserted outside the framework.
*
* The callable receives no arguments and should return a value already in the
* shape the column expects (e.g. a MySQL-format datetime string for a TIMESTAMP).
*
* @param callable():mixed $default
* @return self
*/
public function withPhpDefault(callable $default): self
{
$this->phpDefault = $default;

return $this;
}

/**
* Returns the PHP-side default callable, or null if none is set.
*
* @return callable|null
*/
public function getPhpDefault(): ?callable
{
return $this->phpDefault;
}
}
6 changes: 4 additions & 2 deletions lib/Factories/Columns/DateCreatedFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

namespace PHPNomad\Database\Factories\Columns;

use DateTimeImmutable;
use PHPNomad\Database\Factories\Column;
use PHPNomad\Database\Interfaces\CanConvertToColumn;

class DateCreatedFactory implements CanConvertToColumn
{
public function toColumn(): Column
{
return new Column('dateCreated', 'TIMESTAMP', null, 'NOT NULL DEFAULT CURRENT_TIMESTAMP');
return (new Column('dateCreated', 'TIMESTAMP', null, 'NOT NULL DEFAULT CURRENT_TIMESTAMP'))
->withPhpDefault(static fn (): string => (new DateTimeImmutable())->format('Y-m-d H:i:s'));
}
}
}
6 changes: 4 additions & 2 deletions lib/Factories/Columns/DateModifiedFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

namespace PHPNomad\Database\Factories\Columns;

use DateTimeImmutable;
use PHPNomad\Database\Factories\Column;
use PHPNomad\Database\Interfaces\CanConvertToColumn;

class DateModifiedFactory implements CanConvertToColumn
{
public function toColumn(): Column
{
return new Column('dateModified','TIMESTAMP', null, 'NOT NULL DEFAULT CURRENT_TIMESTAMP');
return (new Column('dateModified', 'TIMESTAMP', null, 'NOT NULL DEFAULT CURRENT_TIMESTAMP'))
->withPhpDefault(static fn (): string => (new DateTimeImmutable())->format('Y-m-d H:i:s'));
}
}
}
39 changes: 35 additions & 4 deletions lib/Traits/WithDatastoreHandlerMethods.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,19 +148,50 @@ public function create(array $attributes): DataModel

$this->maybeThrowForDuplicateUniqueFields($attributes);

// Apply PHP-side defaults so the values that land in the DB also land
// in the in-memory model we hand back. This eliminates the post-insert
// read-back, which is the operation that races read-replicas behind a
// write/read-split router (ProxySQL, MaxScale, Aurora, etc.).
$attributes = $this->applyPhpDefaults($attributes);

$ids = $this->serviceProvider->queryStrategy->insert($this->table, $attributes);

$result = Arr::first($this->getModels([$ids]));
$result = $this->modelAdapter->toModel(Arr::merge($attributes, $ids));

if(!$result){
throw new DatastoreErrorException('Failed to create the record');
}
// Pre-warm the cache so subsequent reads of this record don't have to
// round-trip the DB at all. Same key the framework's getModels() flow
// would have written, so existing read paths transparently pick it up.
$this->cacheItems([$result]);

$this->serviceProvider->eventStrategy->broadcast(new RecordCreated($result));

return $result;
}

/**
* Fills in PHP-side defaults for any column the table declares with a
* `phpDefault` callable that wasn't already supplied by the caller.
*
* @param array<string, mixed> $attributes
* @return array<string, mixed>
*/
protected function applyPhpDefaults(array $attributes): array
{
foreach ($this->table->getColumns() as $column) {
$name = $column->getName();
if (array_key_exists($name, $attributes)) {
continue;
}
$default = $column->getPhpDefault();
if ($default === null) {
continue;
}
$attributes[$name] = $default();
}

return $attributes;
}

/**
* Delete all items that fit the specified condition.
*
Expand Down
47 changes: 47 additions & 0 deletions tests/Unit/Factories/ColumnTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace PHPNomad\Database\Tests\Unit\Factories;

use PHPNomad\Database\Factories\Column;
use PHPNomad\Database\Tests\TestCase;

class ColumnTest extends TestCase
{
public function testGetPhpDefaultIsNullByDefault(): void
{
$column = new Column('name', 'VARCHAR', [255]);

$this->assertNull($column->getPhpDefault());
}

public function testWithPhpDefaultStoresAndReturnsCallable(): void
{
$column = (new Column('createdAt', 'TIMESTAMP'))
->withPhpDefault(static fn () => 'now');

$default = $column->getPhpDefault();

$this->assertIsCallable($default);
$this->assertSame('now', $default());
}

public function testWithPhpDefaultIsFluent(): void
{
$column = new Column('createdAt', 'TIMESTAMP');

$returned = $column->withPhpDefault(static fn () => 'now');

$this->assertSame($column, $returned);
}

public function testWithPhpDefaultDoesNotAffectOtherFields(): void
{
$column = (new Column('createdAt', 'TIMESTAMP', null, 'NOT NULL DEFAULT CURRENT_TIMESTAMP'))
->withPhpDefault(static fn () => 'now');

$this->assertSame('createdAt', $column->getName());
$this->assertSame('TIMESTAMP', $column->getType());
$this->assertNull($column->getTypeArgs());
$this->assertSame(['NOT NULL DEFAULT CURRENT_TIMESTAMP'], $column->getAttributes());
}
}
31 changes: 31 additions & 0 deletions tests/Unit/Factories/Columns/DateCreatedFactoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace PHPNomad\Database\Tests\Unit\Factories\Columns;

use PHPNomad\Database\Factories\Columns\DateCreatedFactory;
use PHPNomad\Database\Tests\TestCase;

class DateCreatedFactoryTest extends TestCase
{
public function testProducesDateCreatedColumnWithDbDefault(): void
{
$column = (new DateCreatedFactory())->toColumn();

$this->assertSame('dateCreated', $column->getName());
$this->assertSame('TIMESTAMP', $column->getType());
$this->assertSame(['NOT NULL DEFAULT CURRENT_TIMESTAMP'], $column->getAttributes());
}

public function testProvidesPhpDefaultThatReturnsMysqlFormatTimestamp(): void
{
$column = (new DateCreatedFactory())->toColumn();
$default = $column->getPhpDefault();

$this->assertIsCallable($default);

$value = $default();

$this->assertIsString($value);
$this->assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $value);
}
}
31 changes: 31 additions & 0 deletions tests/Unit/Factories/Columns/DateModifiedFactoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace PHPNomad\Database\Tests\Unit\Factories\Columns;

use PHPNomad\Database\Factories\Columns\DateModifiedFactory;
use PHPNomad\Database\Tests\TestCase;

class DateModifiedFactoryTest extends TestCase
{
public function testProducesDateModifiedColumnWithDbDefault(): void
{
$column = (new DateModifiedFactory())->toColumn();

$this->assertSame('dateModified', $column->getName());
$this->assertSame('TIMESTAMP', $column->getType());
$this->assertSame(['NOT NULL DEFAULT CURRENT_TIMESTAMP'], $column->getAttributes());
}

public function testProvidesPhpDefaultThatReturnsMysqlFormatTimestamp(): void
{
$column = (new DateModifiedFactory())->toColumn();
$default = $column->getPhpDefault();

$this->assertIsCallable($default);

$value = $default();

$this->assertIsString($value);
$this->assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $value);
}
}
Loading
Loading