Skip to content
Open
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
23 changes: 23 additions & 0 deletions src/Eloquent/DocumentModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Illuminate\Contracts\Queue\QueueableCollection;
use Illuminate\Contracts\Queue\QueueableEntity;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Concerns\HasAttributes;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
Expand All @@ -22,7 +23,9 @@
use MongoDB\BSON\ObjectID;
use MongoDB\BSON\Type;
use MongoDB\BSON\UTCDateTime;
use MongoDB\Laravel\Eloquent\Model as MongoDBModel;
use MongoDB\Laravel\Query\Builder as QueryBuilder;
use ReflectionProperty;
use Stringable;

use function array_key_exists;
Expand Down Expand Up @@ -94,6 +97,26 @@ public function getIdAttribute($value = null)
return $value;
}

/** @inheritdoc */
public function initializeModelAttributes()
{
parent::initializeModelAttributes();

$keyType = static::resolveClassAttribute(Table::class)?->keyType;

if ($keyType === null) {
return;
}

$declaringClass = (new ReflectionProperty($this, 'keyType'))->getDeclaringClass()->getName();

if ($declaringClass !== Model::class && $declaringClass !== MongoDBModel::class) {
return;
}

$this->keyType = $keyType;
}

/** @inheritdoc */
public function getQualifiedKeyName()
{
Expand Down
265 changes: 265 additions & 0 deletions tests/Eloquent/ModelAttributesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
<?php

declare(strict_types=1);

namespace MongoDB\Laravel\Tests\Eloquent;

use Illuminate\Database\Eloquent\Model as EloquentModel;
use Illuminate\Support\Facades\DB;
use MongoDB\Laravel\Connection as MongoDBConnection;
use MongoDB\Laravel\Tests\Models\AttributedAuthor;
use MongoDB\Laravel\Tests\Models\AttributedDeclaredKeyTypeDocument;
use MongoDB\Laravel\Tests\Models\AttributedDocument;
use MongoDB\Laravel\Tests\Models\AttributedDocumentFactory;
use MongoDB\Laravel\Tests\Models\AttributedGuardedDocument;
use MongoDB\Laravel\Tests\Models\AttributedIntegerKeyDocument;
use MongoDB\Laravel\Tests\Models\AttributedTraitDocument;
use MongoDB\Laravel\Tests\Models\AttributedUnguardedDocument;
use MongoDB\Laravel\Tests\Models\AttributedVisibleDocument;
use MongoDB\Laravel\Tests\TestCase;

use function get_debug_type;
use function method_exists;
use function sleep;

final class ModelAttributesTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();

if (! self::supportsModelAttributes()) {
$this->markTestSkipped('Configuring models with attributes requires Laravel v13+');
}

EloquentModel::clearBootedModels();
}

protected function tearDown(): void
{
if (! self::supportsModelAttributes()) {
parent::tearDown();

return;
}

AttributedDocument::truncate();
AttributedAuthor::truncate();
AttributedIntegerKeyDocument::truncate();
AttributedGuardedDocument::truncate();
AttributedUnguardedDocument::truncate();

parent::tearDown();
}

private static function supportsModelAttributes(): bool
{
return method_exists(EloquentModel::class, 'initializeModelAttributes');
}

public function testTableAttributeSetsCollectionName(): void
{
self::assertSame('custom_collection', (new AttributedDocument())->getTable());
}

public function testTableAttributeSetsCollectionNameOnDocumentModelTrait(): void
{
self::assertSame('custom_collection', (new AttributedTraitDocument())->getTable());
}

public function testTableAttributeSetsPrimaryKey(): void
{
self::assertSame('custom_id', (new AttributedDocument())->getKeyName());
}

public function testTableAttributeSetsKeyType(): void
{
self::assertSame('int', (new AttributedIntegerKeyDocument())->getKeyType());
}

public function testKeyTypePropertyTakesPrecedenceOverTableAttribute(): void
{
self::assertSame('string', (new AttributedDeclaredKeyTypeDocument())->getKeyType());
}

public function testKeyTypeDefaultsToStringWithoutTableAttribute(): void
{
self::assertSame('string', (new AttributedGuardedDocument())->getKeyType());
}

public function testTableAttributeSetsIncrementing(): void
{
self::assertFalse((new AttributedDocument())->getIncrementing());
}

public function testTableAttributeSetsDateFormat(): void
{
self::assertSame('U', (new AttributedDocument())->getDateFormat());
}

public function testWithoutTimestampsAttributeDisablesTimestamps(): void
{
self::assertFalse((new AttributedDocument())->usesTimestamps());
}

public function testConnectionAttributeSetsConnectionName(): void
{
self::assertSame('mongodb', (new AttributedDocument())->getConnectionName());
}

public function testFillableAttribute(): void
{
self::assertSame(['name', 'title'], (new AttributedDocument())->getFillable());
}

public function testGuardedAttribute(): void
{
self::assertSame(['secret'], (new AttributedGuardedDocument())->getGuarded());
}

public function testUnguardedAttribute(): void
{
self::assertSame([], (new AttributedUnguardedDocument())->getGuarded());
}

public function testHiddenAttribute(): void
{
self::assertSame(['password'], (new AttributedDocument())->getHidden());
}

public function testVisibleAttribute(): void
{
self::assertSame(['name'], (new AttributedVisibleDocument())->getVisible());
}

public function testAppendsAttribute(): void
{
$document = new AttributedDocument();
$document->name = 'Bilbo';

self::assertSame(['upper_name'], $document->getAppends());
self::assertArrayHasKey('upper_name', $document->attributesToArray());
}

public function testTouchesAttribute(): void
{
self::assertSame(['author'], (new AttributedDocument())->getTouchedRelations());
}

public function testUseFactoryAndUseModelAttributes(): void
{
$factory = AttributedDocument::factory();

self::assertInstanceOf(AttributedDocumentFactory::class, $factory);
self::assertSame(AttributedDocument::class, $factory->modelName());
}

public function testTableAttributeStoresDocumentsInTheCollection(): void
{
AttributedDocument::create(['name' => 'John Doe']);

$collection = DB::connection('mongodb')->getCollection('custom_collection');

$this->assertEquals(1, $collection->countDocuments());
}

public function testTableAttributeUsesCustomPrimaryKeyWhenSaving(): void
{
$document = new AttributedDocument();
$document->custom_id = 'a-game-of-thrones';
$document->name = 'A Game of Thrones';
$document->save();

$this->assertSame('a-game-of-thrones', $document->getKey());

$check = AttributedDocument::find('a-game-of-thrones');
$this->assertInstanceOf(AttributedDocument::class, $check);
$this->assertSame('custom_id', $check->getKeyName());
$this->assertSame('A Game of Thrones', $check->name);
}

public function testTableAttributeKeyTypeIsAppliedWhenSaving(): void
{
$document = new AttributedIntegerKeyDocument();
$document->id = 12345;
$document->save();

$check = AttributedIntegerKeyDocument::find(12345);
$this->assertInstanceOf(AttributedIntegerKeyDocument::class, $check);
$this->assertSame('int', get_debug_type($check->getKey()));
}

public function testConnectionAttributeResolvesTheConnection(): void
{
$document = AttributedDocument::create(['name' => 'John Doe']);

$this->assertInstanceOf(MongoDBConnection::class, $document->getConnection());
$this->assertSame('mongodb', $document->getConnection()->getName());
}

public function testWithoutTimestampsAttributeDoesNotStoreTimestamps(): void
{
AttributedDocument::create(['name' => 'John Doe']);

$stored = DB::connection('mongodb')->getCollection('custom_collection')->findOne();

$this->assertArrayNotHasKey('created_at', (array) $stored);
$this->assertArrayNotHasKey('updated_at', (array) $stored);
}

public function testFillableAttributeIsAppliedOnCreate(): void
{
$document = AttributedDocument::create(['name' => 'John Doe', 'password' => 'secret']);

$check = AttributedDocument::find($document->getKey());
$this->assertSame('John Doe', $check->name);
$this->assertNull($check->password);
}

public function testUnguardedAttributeAllowsMassAssignment(): void
{
$document = AttributedUnguardedDocument::create(['name' => 'John Doe', 'secret' => 'hidden']);

$check = AttributedUnguardedDocument::find($document->getKey());
$this->assertSame('hidden', $check->secret);
}

public function testHiddenAndAppendsAttributesAreAppliedToRetrievedDocuments(): void
{
$document = new AttributedDocument();
$document->name = 'John Doe';
$document->password = 'secret';
$document->save();

$array = AttributedDocument::find($document->getKey())->toArray();

$this->assertArrayNotHasKey('password', $array);
$this->assertSame('JOHN DOE', $array['upper_name']);
}

public function testTouchesAttributeUpdatesTheRelatedDocument(): void
{
$author = AttributedAuthor::create(['name' => 'George R. R. Martin']);

$document = new AttributedDocument();
$document->name = 'A Game of Thrones';
$document->author_id = $author->getKey();
$document->save();

$old = $author->updated_at;
sleep(1);

$document->name = 'A Clash of Kings';
$document->save();

$this->assertNotEquals($old, AttributedAuthor::find($author->getKey())->updated_at);
}

public function testUseFactoryAttributeCreatesPersistedDocument(): void
{
$document = AttributedDocument::factory()->create();

$this->assertSame('Bilbo', $document->name);
$this->assertEquals(1, AttributedDocument::count());
}
}
15 changes: 15 additions & 0 deletions tests/Models/AttributedAuthor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace MongoDB\Laravel\Tests\Models;

use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Table;
use MongoDB\Laravel\Eloquent\Model;

#[Table('attributed_authors')]
#[Fillable(['name'])]
class AttributedAuthor extends Model
{
}
14 changes: 14 additions & 0 deletions tests/Models/AttributedDeclaredKeyTypeDocument.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace MongoDB\Laravel\Tests\Models;

use Illuminate\Database\Eloquent\Attributes\Table;
use MongoDB\Laravel\Eloquent\Model;

#[Table('declared_key_types', keyType: 'int')]
class AttributedDeclaredKeyTypeDocument extends Model
{
protected $keyType = 'string';
}
48 changes: 48 additions & 0 deletions tests/Models/AttributedDocument.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace MongoDB\Laravel\Tests\Models;

use Illuminate\Database\Eloquent\Attributes\Appends;
use Illuminate\Database\Eloquent\Attributes\Connection;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Attributes\Touches;
use Illuminate\Database\Eloquent\Attributes\UseFactory;
use Illuminate\Database\Eloquent\Attributes\WithoutTimestamps;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use MongoDB\Laravel\Eloquent\Model;

use function strtoupper;

#[Table(
'custom_collection',
key: 'custom_id',
keyType: 'string',
incrementing: false,
dateFormat: 'U',
)]
#[WithoutTimestamps]
#[Connection('mongodb')]
#[Fillable(['name', 'title'])]
#[Hidden(['password'])]
#[Appends(['upper_name'])]
#[Touches(['author'])]
#[UseFactory(AttributedDocumentFactory::class)]
class AttributedDocument extends Model
{
use HasFactory;

public function getUpperNameAttribute(): string
{
return strtoupper((string) $this->name);
}

public function author(): BelongsTo
{
return $this->belongsTo(AttributedAuthor::class);
}
}
Loading
Loading