diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 6136cca0a..000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,2 +0,0 @@ -github: jenssegers -tidelift: "packagist/jenssegers/mongodb" diff --git a/CHANGELOG.md b/CHANGELOG.md index b1018c1cb..d7088af6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,29 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +None yet. + +## [10.0] - 2023-08-22 + +### Added +- MongoDB v6.0 supports and tests +- use Mongosh in tests instead of the old Mongo +- Mysql 8 Hybrid relation tests + +### Removed +- MongoDB v4.* support was dropped + +### Fixed +- Fixing the priority between attributes and relations dynamic property call to be the same as Laravel, that mean attribute first then relation (previous it was relation then attribute) +- using function names that exist as attribute accessor/caster: previously if you use a function name that has an accessor; say u use foo() and you have getFooAttribute, calling $model->foo will always fail as the function is called before the accessor, this has been fixed. + +### Breaking +- EmbedsOne and EmbedsMany now require return type to work (Example in the documentation has been updated) +- You can't have the belongsToMany foreign key to be the same as the relation name (check documentation for more details) ## [3.9.2] - 2022-09-01 -### Addded +### Addded - Add single word name mutators [#2438](https://github.com/jenssegers/laravel-mongodb/pull/2438) by [@RosemaryOrchard](https://github.com/RosemaryOrchard) & [@mrneatly](https://github.com/mrneatly). ### Fixed diff --git a/README.md b/README.md index 6a6752575..0d2926e34 100644 --- a/README.md +++ b/README.md @@ -839,6 +839,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 @@ -846,12 +847,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 v10.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); } @@ -920,10 +924,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'); } @@ -936,12 +941,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 v10.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 2d938b745..08d810df2 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. * @@ -161,9 +168,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 +176,30 @@ public function getAttribute($key) return parent::getAttribute($key); } + /** + * Determine if an attribute is an embedded relation. + * + * @param string $key + * @return bool + * @throws ReflectionException + */ + public 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[get_class($this)][$key] = + $returnType instanceof ReflectionNamedType && + $returnType->getName() === EmbedsOne::class || $returnType->getName() === EmbedsMany::class; + } + /** * @inheritdoc */ @@ -401,7 +430,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 +440,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/HybridRelationsTest.php b/tests/HybridRelationsTest.php index 7b4e7cdad..0c401ae35 100644 --- a/tests/HybridRelationsTest.php +++ b/tests/HybridRelationsTest.php @@ -13,6 +13,7 @@ public function setUp(): void MysqlUser::executeSchema(); MysqlBook::executeSchema(); MysqlRole::executeSchema(); +// MysqlGroup::executeSchema(); } public function tearDown(): void @@ -20,8 +21,19 @@ public function tearDown(): void MysqlUser::truncate(); MysqlBook::truncate(); MysqlRole::truncate(); +// MysqlGroup::truncate(); } +// public function testMysqlGroups() +// { +// $user = new MysqlUser; +// $user->name = 'John Doe'; +// $user->save(); +// $this->assertIsInt($user->id); +// +// $group = $user->groups()->create(['name' => 'test']); +// } + public function testMysqlRelations() { $user = new MysqlUser; diff --git a/tests/ModelTest.php b/tests/ModelTest.php index e4eeefbb4..a24d27ce9 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -801,4 +801,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/RelationsTest.php b/tests/RelationsTest.php index b1b73e6cc..15be46fa9 100644 --- a/tests/RelationsTest.php +++ b/tests/RelationsTest.php @@ -334,8 +334,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()); diff --git a/tests/models/Group.php b/tests/models/Group.php index bf4edd9bc..7b005703a 100644 --- a/tests/models/Group.php +++ b/tests/models/Group.php @@ -13,6 +13,6 @@ class Group extends Eloquent public function users(): BelongsToMany { - return $this->belongsToMany('User', 'users', 'groups', 'users', '_id', '_id', 'users'); + return $this->belongsToMany('User'); } } diff --git a/tests/models/MysqlGroup.php b/tests/models/MysqlGroup.php new file mode 100644 index 000000000..5efd20e47 --- /dev/null +++ b/tests/models/MysqlGroup.php @@ -0,0 +1,48 @@ +belongsToMany(MysqlUser::class); + } + + /** + * Check if we need to run the schema. + */ + public static function executeSchema(): void + { + /** @var MySqlBuilder $schema */ + $schema = Schema::connection('mysql'); + + if (! $schema->hasTable('groups')) { + Schema::connection('mysql')->create('groups', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->timestamps(); + }); + } + + if (! $schema->hasTable('group_user')) { + Schema::connection('mysql')->create('group_user', function (Blueprint $table) { + $table->foreignId('user_id')->constrained('users')->cascadeOnUpdate()->cascadeOnDelete(); + $table->foreignId('group_id')->constrained('groups')->cascadeOnUpdate()->cascadeOnDelete(); + }); + } + } +} diff --git a/tests/models/MysqlUser.php b/tests/models/MysqlUser.php index 8c1393fd5..ddd2142ad 100644 --- a/tests/models/MysqlUser.php +++ b/tests/models/MysqlUser.php @@ -2,9 +2,11 @@ declare(strict_types=1); +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Schema\MySqlBuilder; use Illuminate\Support\Facades\Schema; use Jenssegers\Mongodb\Eloquent\HybridRelations; @@ -31,17 +33,22 @@ public function mysqlBooks(): HasMany return $this->hasMany(MysqlBook::class); } + public function groups(): BelongsToMany + { + return $this->belongsToMany(MysqlGroup::class, 'group_user', 'user_id', 'group_id'); + } + /** * Check if we need to run the schema. */ public static function executeSchema(): void { - /** @var \Illuminate\Database\Schema\MySqlBuilder $schema */ + /** @var MySqlBuilder $schema */ $schema = Schema::connection('mysql'); if (! $schema->hasTable('users')) { Schema::connection('mysql')->create('users', function (Blueprint $table) { - $table->increments('id'); + $table->id(); $table->string('name'); $table->timestamps(); }); diff --git a/tests/models/User.php b/tests/models/User.php index d32d1f8b4..430e5a06a 100644 --- a/tests/models/User.php +++ b/tests/models/User.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Carbon\Carbon; use Illuminate\Auth\Authenticatable; use Illuminate\Auth\Passwords\CanResetPassword; use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; @@ -11,6 +12,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. @@ -20,9 +23,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 */ @@ -73,7 +76,7 @@ public function clients() public function groups() { - return $this->belongsToMany('Group', 'groups', 'users', 'groups', '_id', '_id', 'groups'); + return $this->belongsToMany('Group'); } public function photos() @@ -81,12 +84,12 @@ public function photos() return $this->morphMany('Photo', 'has_image'); } - public function addresses() + public function addresses(): EmbedsMany { return $this->embedsMany('Address'); } - public function father() + public function father(): EmbedsOne { return $this->embedsOne('User'); } @@ -103,4 +106,14 @@ protected function username(): Attribute set: fn ($value) => Str::slug($value) ); } + + public function getFooAttribute() + { + return 'normal attribute'; + } + + public function foo() + { + return 'normal function'; + } }