Skip to content

Commit

Permalink
Fix priority of embeds vs attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
mshamaseen authored and GromNaN committed Aug 31, 2023
1 parent 9d36d17 commit 68afe15
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 19 deletions.
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -805,19 +805,23 @@ class User extends Model
}
}
```
**Warning:** naming the foreign key same as the relation name will prevent the relation for being called on dynamic property, i.e. in the example above if you replaced `group_ids` with `groups` calling `$user->groups` will return the column instead of the relation.

### EmbedsMany Relationship

If you want to embed models, rather than referencing them, you can use the `embedsMany` relation. This relation is similar to the `hasMany` relation but embeds the models inside the parent object.

**REMEMBER**: These relations return Eloquent collections, they don't return query builder objects!

**Breaking changes** starting from v4.0 you need to define the return type of EmbedsOne and EmbedsMany relation for it to work

```php
use MongoDB\Laravel\Eloquent\Model;
use MongoDB\Laravel\Relations\EmbedsMany;

class User extends Model
{
public function books()
public function books(): EmbedsMany
{
return $this->embedsMany(Book::class);
}
Expand Down Expand Up @@ -886,10 +890,11 @@ Like other relations, embedsMany assumes the local key of the relationship based

```php
use MongoDB\Laravel\Eloquent\Model;
use MongoDB\Laravel\Relations\EmbedsMany;

class User extends Model
{
public function books()
public function books(): EmbedsMany
{
return $this->embedsMany(Book::class, 'local_key');
}
Expand All @@ -902,12 +907,15 @@ Embedded relations will return a Collection of embedded items instead of a query

The embedsOne relation is similar to the embedsMany relation, but only embeds a single model.

**Breaking changes** starting from v4.0 you need to define the return type of EmbedsOne and EmbedsMany relation for it to work

```php
use MongoDB\Laravel\Eloquent\Model;
use MongoDB\Laravel\Relations\EmbedsOne;

class Book extends Model
{
public function author()
public function author(): EmbedsOne
{
return $this->embedsOne(Author::class);
}
Expand Down
30 changes: 30 additions & 0 deletions src/Eloquent/EmbedsRelations.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@
use Illuminate\Support\Str;
use MongoDB\Laravel\Relations\EmbedsMany;
use MongoDB\Laravel\Relations\EmbedsOne;
use ReflectionMethod;
use ReflectionNamedType;

/**
* Embeds relations for MongoDB models.
*/
trait EmbedsRelations
{
/**
* @var array<class-string, array<string, bool>>
*/
private static array $hasEmbeddedRelation = [];

/**
* Define an embedded one-to-many relationship.
*
Expand Down Expand Up @@ -76,4 +83,27 @@ protected function embedsOne($related, $localKey = null, $foreignKey = null, $re

return new EmbedsOne($query, $this, $instance, $localKey, $foreignKey, $relation);
}

/**
* Determine if an attribute is an embedded relation.
*
* @param string $key
* @return bool
* @throws \ReflectionException
*/
private function hasEmbeddedRelation(string $key): bool
{
if (! method_exists($this, $method = Str::camel($key))) {
return false;
}

if (isset(self::$hasEmbeddedRelation[static::class][$key])) {
return self::$hasEmbeddedRelation[static::class][$key];
}

$returnType = (new ReflectionMethod($this, $method))->getReturnType();

return self::$hasEmbeddedRelation[static::class][$key] = $returnType instanceof ReflectionNamedType
&& in_array($returnType->getName(), [EmbedsOne::class, EmbedsMany::class], true);
}
}
4 changes: 4 additions & 0 deletions src/Eloquent/HybridRelations.php
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,10 @@ public function belongsToMany(

$instance = new $related;

if ($relatedPivotKey === $relation) {
throw new \LogicException(sprintf('In %s::%s(), the key cannot be identical to the relation name "%s". The default key is "%s".', static::class, $relation, $relation, $instance->getForeignKey().'s'));
}

$relatedPivotKey = $relatedPivotKey ?: $instance->getForeignKey().'s';

// If no table name was provided, we can guess it by concatenating the two
Expand Down
12 changes: 5 additions & 7 deletions src/Eloquent/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use MongoDB\BSON\ObjectID;
use MongoDB\BSON\UTCDateTime;
use MongoDB\Laravel\Query\Builder as QueryBuilder;
use ReflectionException;
use function uniqid;

abstract class Model extends BaseModel
Expand Down Expand Up @@ -154,6 +155,7 @@ public function getTable()

/**
* @inheritdoc
* @throws ReflectionException
*/
public function getAttribute($key)
{
Expand All @@ -172,11 +174,7 @@ public function getAttribute($key)
}

// This checks for embedded relation support.
if (
method_exists($this, $key)
&& ! method_exists(self::class, $key)
&& ! $this->hasAttributeGetMutator($key)
) {
if ($this->hasEmbeddedRelation($key)) {
return $this->getRelationValue($key);
}

Expand Down Expand Up @@ -474,7 +472,7 @@ public function getForeignKey()
/**
* Set the parent relation.
*
* @param \Illuminate\Database\Eloquent\Relations\Relation $relation
* @param Relation $relation
*/
public function setParentRelation(Relation $relation)
{
Expand All @@ -484,7 +482,7 @@ public function setParentRelation(Relation $relation)
/**
* Get the parent relation.
*
* @return \Illuminate\Database\Eloquent\Relations\Relation
* @return Relation
*/
public function getParentRelation()
{
Expand Down
7 changes: 7 additions & 0 deletions tests/ModelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1035,4 +1035,11 @@ public function testEnumCast(): void
$this->assertSame(MemberStatus::Member->value, $check->getRawOriginal('member_status'));
$this->assertSame(MemberStatus::Member, $check->member_status);
}

public function testGetFooAttributeAccessor()
{
$user = new User();

$this->assertSame('normal attribute', $user->foo);
}
}
2 changes: 1 addition & 1 deletion tests/Models/Group.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ class Group extends Eloquent

public function users(): BelongsToMany
{
return $this->belongsToMany(User::class, 'users', 'groups', 'users', '_id', '_id', 'users');
return $this->belongsToMany(User::class, 'users', 'groupIds', 'userIds', '_id', '_id', 'users');
}
}
25 changes: 19 additions & 6 deletions tests/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace MongoDB\Laravel\Tests\Models;

use Carbon\Carbon;
use DateTimeInterface;
use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword;
Expand All @@ -14,6 +15,8 @@
use Illuminate\Support\Str;
use MongoDB\Laravel\Eloquent\HybridRelations;
use MongoDB\Laravel\Eloquent\Model as Eloquent;
use MongoDB\Laravel\Relations\EmbedsMany;
use MongoDB\Laravel\Relations\EmbedsOne;

/**
* Class User.
Expand All @@ -23,9 +26,9 @@
* @property string $email
* @property string $title
* @property int $age
* @property \Carbon\Carbon $birthday
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property Carbon $birthday
* @property Carbon $created_at
* @property Carbon $updated_at
* @property string $username
* @property MemberStatus member_status
*/
Expand Down Expand Up @@ -76,20 +79,20 @@ public function clients()

public function groups()
{
return $this->belongsToMany(Group::class, 'groups', 'users', 'groups', '_id', '_id', 'groups');
return $this->belongsToMany(Group::class, 'groups', 'userIds', 'groupIds', '_id', '_id', 'groups');
}

public function photos()
{
return $this->morphMany(Photo::class, 'has_image');
}

public function addresses()
public function addresses(): EmbedsMany
{
return $this->embedsMany(Address::class);
}

public function father()
public function father(): EmbedsOne
{
return $this->embedsOne(self::class);
}
Expand All @@ -106,4 +109,14 @@ protected function username(): Attribute
set: fn ($value) => Str::slug($value)
);
}

public function getFooAttribute()
{
return 'normal attribute';
}

public function foo()
{
return 'normal function';
}
}
4 changes: 2 additions & 2 deletions tests/RelationsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -344,8 +344,8 @@ public function testBelongsToManyCustom(): void
$group = Group::find($group->_id);

// Check for custom relation attributes
$this->assertArrayHasKey('users', $group->getAttributes());
$this->assertArrayHasKey('groups', $user->getAttributes());
$this->assertArrayHasKey('userIds', $group->getAttributes());
$this->assertArrayHasKey('groupIds', $user->getAttributes());

// Assert they are attached
$this->assertContains($group->_id, $user->groups->pluck('_id')->toArray());
Expand Down

0 comments on commit 68afe15

Please sign in to comment.