diff --git a/README.md b/README.md index f00b3a2c7..f53dfa6b1 100644 --- a/README.md +++ b/README.md @@ -856,6 +856,7 @@ 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 @@ -863,12 +864,15 @@ If you want to embed models, rather than referencing them, you can use the `embe **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 Jenssegers\Mongodb\Eloquent\Model; +use Jenssegers\Mongodb\Relations\EmbedsMany; class User extends Model { - public function books() + public function books(): EmbedsMany { return $this->embedsMany(Book::class); } @@ -937,10 +941,11 @@ Like other relations, embedsMany assumes the local key of the relationship based ```php use Jenssegers\Mongodb\Eloquent\Model; +use Jenssegers\Mongodb\Relations\EmbedsMany; class User extends Model { - public function books() + public function books(): EmbedsMany { return $this->embedsMany(Book::class, 'local_key'); } @@ -953,12 +958,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 Jenssegers\Mongodb\Eloquent\Model; +use Jenssegers\Mongodb\Relations\EmbedsOne; class Book extends Model { - public function author() + public function author(): EmbedsOne { return $this->embedsOne(Author::class); } diff --git a/src/Eloquent/Model.php b/src/Eloquent/Model.php index 2d985f627..9fc8f0b2b 100644 --- a/src/Eloquent/Model.php +++ b/src/Eloquent/Model.php @@ -15,15 +15,22 @@ use Illuminate\Support\Str; use function in_array; use Jenssegers\Mongodb\Query\Builder as QueryBuilder; +use Jenssegers\Mongodb\Relations\EmbedsMany; +use Jenssegers\Mongodb\Relations\EmbedsOne; use MongoDB\BSON\Binary; use MongoDB\BSON\ObjectID; use MongoDB\BSON\UTCDateTime; +use ReflectionException; +use ReflectionMethod; +use ReflectionNamedType; use function uniqid; abstract class Model extends BaseModel { use HybridRelations, EmbedsRelations; + public static array $embeddedCache = []; + /** * The collection associated with the model. * @@ -147,6 +154,7 @@ public function getTable() /** * @inheritdoc + * @throws ReflectionException */ public function getAttribute($key) { @@ -161,9 +169,7 @@ public function getAttribute($key) // This checks for embedded relation support. if ( - method_exists($this, $key) - && ! method_exists(self::class, $key) - && ! $this->hasAttributeGetMutator($key) + $this->hasEmbeddedRelation($key) ) { return $this->getRelationValue($key); } @@ -171,6 +177,30 @@ public function getAttribute($key) return parent::getAttribute($key); } + /** + * 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(static::$embeddedCache[get_class($this)][$key])) { + return static::$embeddedCache[get_class($this)][$key]; + } + + $returnType = (new ReflectionMethod($this, $method))->getReturnType(); + + return $returnType && static::$embeddedCache[static::class][$key] = + $returnType instanceof ReflectionNamedType && + $returnType->getName() === EmbedsOne::class || $returnType->getName() === EmbedsMany::class; + } + /** * @inheritdoc */ @@ -401,7 +431,7 @@ public function getForeignKey() /** * Set the parent relation. * - * @param \Illuminate\Database\Eloquent\Relations\Relation $relation + * @param Relation $relation */ public function setParentRelation(Relation $relation) { @@ -411,7 +441,7 @@ public function setParentRelation(Relation $relation) /** * Get the parent relation. * - * @return \Illuminate\Database\Eloquent\Relations\Relation + * @return Relation */ public function getParentRelation() { diff --git a/tests/ModelTest.php b/tests/ModelTest.php index 1042a07bc..6033dfdcd 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -907,4 +907,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); + } } diff --git a/tests/Models/Group.php b/tests/Models/Group.php index 8631dbfc8..71386fca8 100644 --- a/tests/Models/Group.php +++ b/tests/Models/Group.php @@ -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); } } diff --git a/tests/Models/User.php b/tests/Models/User.php index f559af470..36d6d19ff 100644 --- a/tests/Models/User.php +++ b/tests/Models/User.php @@ -4,6 +4,7 @@ namespace Jenssegers\Mongodb\Tests\Models; +use Carbon\Carbon; use DateTimeInterface; use Illuminate\Auth\Authenticatable; use Illuminate\Auth\Passwords\CanResetPassword; @@ -14,6 +15,8 @@ use Illuminate\Support\Str; use Jenssegers\Mongodb\Eloquent\HybridRelations; use Jenssegers\Mongodb\Eloquent\Model as Eloquent; +use Jenssegers\Mongodb\Relations\EmbedsMany; +use Jenssegers\Mongodb\Relations\EmbedsOne; /** * Class User. @@ -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 */ @@ -76,7 +79,7 @@ public function clients() public function groups() { - return $this->belongsToMany(Group::class, 'groups', 'users', 'groups', '_id', '_id', 'groups'); + return $this->belongsToMany(Group::class); } public function photos() @@ -84,12 +87,12 @@ 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); } @@ -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'; + } } diff --git a/tests/RelationsTest.php b/tests/RelationsTest.php index 66c27583f..969fdb0a6 100644 --- a/tests/RelationsTest.php +++ b/tests/RelationsTest.php @@ -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('user_ids', $group->getAttributes()); + $this->assertArrayHasKey('group_ids', $user->getAttributes()); // Assert they are attached $this->assertContains($group->_id, $user->groups->pluck('_id')->toArray());