diff --git a/README.md b/README.md index 55105eb..4ce6d8b 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Use this command if you are in PowerShell on Windows (e.g. in VS Code): - [Many-To-Many Relationships](#many-to-many-relationships) - [Array of IDs](#array-of-ids) - [Array of Objects](#array-of-objects) + - [Composite Keys](#composite-keys) - [Query Performance](#query-performance) - [Has-Many-Through Relationships](#has-many-through-relationships) - [Deep Relationship Concatenation](#deep-relationship-concatenation) @@ -223,6 +224,38 @@ $user->roles()->toggle([2 => ['active' => true], 3])->save(); **Limitations:** On SQLite and SQL Server, these relationships only work partially. +#### Composite Keys + +If multiple columns need to match, you can define a composite key. + +Pass an array of keys that starts with JSON key: + +```php +class Employee extends Model +{ + public function tasks() + { + return $this->belongsToJson( + Task::class, + ['options->work_stream_ids', 'team_id'], + ['work_stream_id', 'team_id'] + ); + } +} + +class Task extends Model +{ + public function employees() + { + return $this->hasManyJson( + Employee::class, + ['options->work_stream_ids', 'team_id'], + ['work_stream_id', 'team_id'] + ); + } +} +``` + #### Query Performance ##### MySQL diff --git a/src/HasJsonRelationships.php b/src/HasJsonRelationships.php index aea32ce..acd2c85 100644 --- a/src/HasJsonRelationships.php +++ b/src/HasJsonRelationships.php @@ -33,7 +33,7 @@ trait HasJsonRelationships */ public function getAttribute($key) { - $attribute = preg_split('/(->|\[\])/', $key)[0]; + $attribute = preg_split('/(->|\[])/', $key)[0]; if (array_key_exists($attribute, $this->attributes)) { return $this->getAttributeValue($key); @@ -221,8 +221,8 @@ protected function newMorphMany(Builder $query, Model $parent, $type, $id, $loca * Define an inverse one-to-one or many JSON relationship. * * @param string $related - * @param string $foreignKey - * @param string $ownerKey + * @param string|array $foreignKey + * @param string|array $ownerKey * @param string $relation * @return \Staudenmeir\EloquentJsonRelations\Relations\BelongsToJson */ @@ -251,8 +251,8 @@ public function belongsToJson($related, $foreignKey, $ownerKey = null, $relation * * @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Model $child - * @param string $foreignKey - * @param string $ownerKey + * @param string|array $foreignKey + * @param string|array $ownerKey * @param string $relation * @return \Staudenmeir\EloquentJsonRelations\Relations\BelongsToJson */ @@ -265,8 +265,8 @@ protected function newBelongsToJson(Builder $query, Model $child, $foreignKey, $ * Define a one-to-many JSON relationship. * * @param string $related - * @param string $foreignKey - * @param string $localKey + * @param string|array $foreignKey + * @param string|array $localKey * @return \Staudenmeir\EloquentJsonRelations\Relations\HasManyJson */ public function hasManyJson($related, $foreignKey, $localKey = null) @@ -274,12 +274,21 @@ public function hasManyJson($related, $foreignKey, $localKey = null) /** @var \Illuminate\Database\Eloquent\Model $instance */ $instance = $this->newRelatedInstance($related); + if (is_array($foreignKey)) { + $foreignKey = array_map( + fn (string $key) => "{$instance->getTable()}.$key", + (array) $foreignKey + ); + } else { + $foreignKey = "{$instance->getTable()}.$foreignKey"; + } + $localKey = $localKey ?: $this->getKeyName(); return $this->newHasManyJson( $instance->newQuery(), $this, - $instance->getTable().'.'.$foreignKey, + $foreignKey, $localKey ); } @@ -289,8 +298,8 @@ public function hasManyJson($related, $foreignKey, $localKey = null) * * @param \Illuminate\Database\Eloquent\Builder $query * @param \Illuminate\Database\Eloquent\Model $parent - * @param string $foreignKey - * @param string $localKey + * @param string|array $foreignKey + * @param string|array $localKey * @return \Staudenmeir\EloquentJsonRelations\Relations\HasManyJson */ protected function newHasManyJson(Builder $query, Model $parent, $foreignKey, $localKey) diff --git a/src/Relations/BelongsToJson.php b/src/Relations/BelongsToJson.php index e2eb154..4c72878 100644 --- a/src/Relations/BelongsToJson.php +++ b/src/Relations/BelongsToJson.php @@ -9,6 +9,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Collection as BaseCollection; use Staudenmeir\EloquentHasManyDeepContracts\Interfaces\ConcatenableRelation; +use Staudenmeir\EloquentJsonRelations\Relations\Traits\CompositeKeys\SupportsBelongsToJsonCompositeKeys; use Staudenmeir\EloquentJsonRelations\Relations\Traits\Concatenation\IsConcatenableBelongsToJsonRelation; use Staudenmeir\EloquentJsonRelations\Relations\Traits\IsJsonRelation; @@ -17,6 +18,43 @@ class BelongsToJson extends BelongsTo implements ConcatenableRelation use InteractsWithPivotRecords; use IsConcatenableBelongsToJsonRelation; use IsJsonRelation; + use SupportsBelongsToJsonCompositeKeys; + + /** + * The foreign key of the parent model. + * + * @var string|array + */ + protected $foreignKey; + + /** + * The associated key on the parent model. + * + * @var string|array + */ + protected $ownerKey; + + /** + * Create a new belongs to JSON relationship instance. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Model $child + * @param string $foreignKey + * @param string $ownerKey + * @param string $relationName + * @return void + */ + public function __construct(Builder $query, Model $child, $foreignKey, $ownerKey, $relationName) + { + $segments = is_array($foreignKey) + ? explode('[]->', $foreignKey[0]) + : explode('[]->', $foreignKey); + + $this->path = $segments[0]; + $this->key = $segments[1] ?? null; + + parent::__construct($query, $child, $foreignKey, $ownerKey, $relationName); + } /** * Get the results of the relationship. @@ -61,10 +99,33 @@ public function addConstraints() if (static::$constraints) { $table = $this->related->getTable(); - $this->query->whereIn($table.'.'.$this->ownerKey, $this->getForeignKeys()); + $ownerKey = $this->hasCompositeKey() ? $this->ownerKey[0] : $this->ownerKey; + + $this->query->whereIn("$table.$ownerKey", $this->getForeignKeys()); + + if ($this->hasCompositeKey()) { + $this->addConstraintsWithCompositeKey(); + } } } + /** + * Set the constraints for an eager load of the relation. + * + * @param array $models + * @return void + */ + public function addEagerConstraints(array $models) + { + if ($this->hasCompositeKey()) { + $this->addEagerConstraintsWithCompositeKey($models); + + return; + } + + parent::addEagerConstraints($models); + } + /** * Gather the keys from an array of related models. * @@ -94,22 +155,30 @@ protected function getEagerModelKeys(array $models) */ public function match(array $models, Collection $results, $relation) { - $dictionary = $this->buildDictionary($results); + if ($this->hasCompositeKey()) { + $this->matchWithCompositeKey($models, $results, $relation); + } else { + $dictionary = $this->buildDictionary($results); - foreach ($models as $model) { - $matches = []; + foreach ($models as $model) { + $matches = []; - foreach ($this->getForeignKeys($model) as $id) { - if (isset($dictionary[$id])) { - $matches[] = $dictionary[$id]; + foreach ($this->getForeignKeys($model) as $id) { + if (isset($dictionary[$id])) { + $matches[] = $dictionary[$id]; + } } - } - $model->setRelation($relation, $collection = $this->related->newCollection($matches)); + $collection = $this->related->newCollection($matches); + + $model->setRelation($relation, $collection); + } + } + foreach ($models as $model) { if ($this->key) { $this->hydratePivotRelation( - $collection, + $model->getRelation($relation), $model, fn (Model $model, Model $parent) => $parent->{$this->path} ); @@ -150,7 +219,9 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, return $this->getRelationExistenceQueryForSelfRelation($query, $parentQuery, $columns); } - [$sql, $bindings] = $this->relationExistenceQueryOwnerKey($query, $this->ownerKey); + $ownerKey = $this->hasCompositeKey() ? $this->ownerKey[0] : $this->ownerKey; + + [$sql, $bindings] = $this->relationExistenceQueryOwnerKey($query, $ownerKey); $query->addBinding($bindings); @@ -160,6 +231,10 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $query->getQuery()->connection->raw($sql) ); + if ($this->hasCompositeKey()) { + $this->getRelationExistenceQueryWithCompositeKey($query); + } + return $query->select($columns); } @@ -237,9 +312,11 @@ public function pivotAttributes(Model $model, Model $parent, array $records) { $key = str_replace('->', '.', $this->key); + $ownerKey = $this->hasCompositeKey() ? $this->ownerKey[0] : $this->ownerKey; + $record = (new BaseCollection($records)) - ->filter(function ($value) use ($key, $model) { - return Arr::get($value, $key) == $model->{$this->ownerKey}; + ->filter(function ($value) use ($key, $model, $ownerKey) { + return Arr::get($value, $key) == $model->$ownerKey; })->first(); return Arr::except($record, $key); @@ -255,7 +332,9 @@ public function getForeignKeys(Model $model = null) { $model = $model ?: $this->child; - return (new BaseCollection($model->{$this->foreignKey}))->filter(fn ($key) => $key !== null)->all(); + $foreignKey = $this->hasCompositeKey() ? $this->foreignKey[0] : $this->foreignKey; + + return (new BaseCollection($model->$foreignKey))->filter(fn ($key) => $key !== null)->all(); } /** diff --git a/src/Relations/HasManyJson.php b/src/Relations/HasManyJson.php index 4d1c964..1768b7c 100644 --- a/src/Relations/HasManyJson.php +++ b/src/Relations/HasManyJson.php @@ -10,12 +10,49 @@ use Illuminate\Support\Collection as BaseCollection; use Staudenmeir\EloquentHasManyDeepContracts\Interfaces\ConcatenableRelation; use Staudenmeir\EloquentJsonRelations\Relations\Traits\Concatenation\IsConcatenableHasManyJsonRelation; +use Staudenmeir\EloquentJsonRelations\Relations\Traits\CompositeKeys\SupportsHasManyJsonCompositeKeys; use Staudenmeir\EloquentJsonRelations\Relations\Traits\IsJsonRelation; class HasManyJson extends HasMany implements ConcatenableRelation { use IsConcatenableHasManyJsonRelation; use IsJsonRelation; + use SupportsHasManyJsonCompositeKeys; + + /** + * The foreign key of the parent model. + * + * @var string|array + */ + protected $foreignKey; + + /** + * The local key of the parent model. + * + * @var string|array + */ + protected $localKey; + + /** + * Create a new has many JSON relationship instance. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Model $parent + * @param string $foreignKey + * @param string $localKey + * @return void + */ + public function __construct(Builder $query, Model $parent, $foreignKey, $localKey) + { + $segments = is_array($foreignKey) + ? explode('[]->', $foreignKey[0]) + : explode('[]->', $foreignKey); + + $this->path = $segments[0]; + $this->key = $segments[1] ?? null; + + parent::__construct($query, $parent, $foreignKey, $localKey); + } /** * Get the results of the relationship. @@ -39,7 +76,9 @@ public function get($columns = ['*']) { $models = parent::get($columns); - if ($this->key && !is_null($this->parent->{$this->localKey})) { + $localKey = $this->hasCompositeKey() ? $this->localKey[0] : $this->localKey; + + if ($this->key && !is_null($this->parent->$localKey)) { $this->hydratePivotRelation( $models, $this->parent, @@ -66,6 +105,10 @@ public function addConstraints() $parentKey, fn ($parentKey) => $this->parentKeyToArray($parentKey) ); + + if ($this->hasCompositeKey()) { + $this->addConstraintsWithCompositeKey(); + } } } @@ -77,6 +120,12 @@ public function addConstraints() */ public function addEagerConstraints(array $models) { + if ($this->hasCompositeKey()) { + $this->addEagerConstraintsWithCompositeKey($models); + + return; + } + $parentKeys = $this->getKeys($models, $this->localKey); $this->query->where(function (Builder $query) use ($parentKeys) { @@ -120,7 +169,11 @@ protected function parentKeyToArray($parentKey) */ protected function matchOneOrMany(array $models, Collection $results, $relation, $type) { - $models = parent::matchOneOrMany(...func_get_args()); + if ($this->hasCompositeKey()) { + $this->matchWithCompositeKey($models, $results, $relation); + } else { + parent::matchOneOrMany($models, $results, $relation, $type); + } if ($this->key) { foreach ($models as $model) { @@ -196,6 +249,10 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $query->getQuery()->connection->raw($sql) ); + if ($this->hasCompositeKey()) { + $this->getRelationExistenceQueryWithCompositeKey($query); + } + return $query->select($columns); } @@ -272,9 +329,11 @@ public function pivotAttributes(Model $model, Model $parent, array $records) { $key = str_replace('->', '.', $this->key); + $localKey = $this->hasCompositeKey() ? $this->localKey[0] : $this->localKey; + $record = (new BaseCollection($records)) - ->filter(function ($value) use ($key, $parent) { - return Arr::get($value, $key) == $parent->{$this->localKey}; + ->filter(function ($value) use ($key, $localKey, $parent) { + return Arr::get($value, $key) == $parent->$localKey; })->first(); return Arr::except($record, $key); @@ -289,4 +348,38 @@ public function getPathName() { return last(explode('.', $this->path)); } + + /** + * Get the key value of the parent's local key. + * + * @return mixed + */ + public function getParentKey() + { + $localKey = $this->hasCompositeKey() ? $this->localKey[0] : $this->localKey; + + return $this->parent->getAttribute($localKey); + } + + /** + * Get the fully qualified parent key name. + * + * @return string + */ + public function getQualifiedParentKeyName() + { + $localKey = $this->hasCompositeKey() ? $this->localKey[0] : $this->localKey; + + return $this->parent->qualifyColumn($localKey); + } + + /** + * Get the foreign key for the relationship. + * + * @return string + */ + public function getQualifiedForeignKeyName() + { + return $this->hasCompositeKey() ? $this->foreignKey[0] : $this->foreignKey; + } } diff --git a/src/Relations/Traits/CompositeKeys/SupportsBelongsToJsonCompositeKeys.php b/src/Relations/Traits/CompositeKeys/SupportsBelongsToJsonCompositeKeys.php new file mode 100644 index 0000000..0da579f --- /dev/null +++ b/src/Relations/Traits/CompositeKeys/SupportsBelongsToJsonCompositeKeys.php @@ -0,0 +1,163 @@ +foreignKey); + } + + /** + * Set the base constraints on the relation query for a composite key. + * + * @return void + */ + protected function addConstraintsWithCompositeKey(): void + { + $columns = array_slice($this->ownerKey, 1); + + foreach ($columns as $column) { + $this->query->where( + $this->related->qualifyColumn($column), + '=', + $this->child->$column + ); + } + } + + /** + * Set the constraints for an eager load of the relation for a composite key. + * + * @param array $models + * @return void + */ + protected function addEagerConstraintsWithCompositeKey(array $models): void + { + $keys = (new BaseCollection($models))->map( + function (Model $model) { + return array_map( + fn (string $column) => $model[$column], + $this->foreignKey + ); + } + )->values()->unique(null, true)->all(); + + $this->query->where( + function (Builder $query) use ($keys) { + foreach ($keys as $key) { + $query->orWhere( + function (Builder $query) use ($key) { + foreach ($this->ownerKey as $i => $column) { + if ($i === 0) { + $query->whereIn( + $this->related->qualifyColumn($column), + $key[$i] + ); + } else { + $query->where( + $this->related->qualifyColumn($column), + '=', + $key[$i] + ); + } + } + } + ); + } + } + ); + } + + /** + * Match the eagerly loaded results to their parents for a composite key. + * + * @param array $models + * @param \Illuminate\Database\Eloquent\Collection $results + * @param string $relation + * @return array + */ + protected function matchWithCompositeKey(array $models, Collection $results, string $relation): array + { + $dictionary = $this->buildDictionaryWithCompositeKey($results); + + foreach ($models as $model) { + $matches = []; + + $additionalValues = array_map( + fn (string $key) => $model->$key, + array_slice($this->ownerKey, 1) + ); + + foreach ($this->getForeignKeys($model) as $id) { + $values = $additionalValues; + + array_unshift($values, $id); + + $key = implode("\0", $values); + + $matches = array_merge($matches, $dictionary[$key] ?? []); + } + + $collection = $this->related->newCollection($matches); + + $model->setRelation($relation, $collection); + } + + return $models; + } + + /** + * Build model dictionary keyed by the relation's composite foreign key. + * + * @param \Illuminate\Database\Eloquent\Collection $results + * @return array + */ + protected function buildDictionaryWithCompositeKey(Collection $results): array + { + $dictionary = []; + + foreach ($results as $result) { + $values = array_map( + fn (string $key) => $result->$key, + $this->ownerKey + ); + + $values = implode("\0", $values); + + $dictionary[$values][] = $result; + } + + return $dictionary; + } + + /** + * Add the constraints for a relationship query for a composite key. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return void + */ + public function getRelationExistenceQueryWithCompositeKey(Builder $query): void + { + $columns = array_slice($this->foreignKey, 1, preserve_keys: true); + + foreach ($columns as $i => $column) { + $query->whereColumn( + $this->child->qualifyColumn($column), + '=', + $query->qualifyColumn($this->ownerKey[$i]) + ); + } + } +} diff --git a/src/Relations/Traits/CompositeKeys/SupportsHasManyJsonCompositeKeys.php b/src/Relations/Traits/CompositeKeys/SupportsHasManyJsonCompositeKeys.php new file mode 100644 index 0000000..9788ec3 --- /dev/null +++ b/src/Relations/Traits/CompositeKeys/SupportsHasManyJsonCompositeKeys.php @@ -0,0 +1,186 @@ +foreignKey); + } + + /** + * Set the base constraints on the relation query for a composite key. + * + * @return void + */ + protected function addConstraintsWithCompositeKey(): void + { + $columns = array_slice($this->localKey, 1); + + foreach ($columns as $column) { + $this->query->where( + $this->related->qualifyColumn($column), + '=', + $this->parent->$column + ); + } + } + + /** + * Set the constraints for an eager load of the relation for a composite key. + * + * @param array $models + * @return void + */ + protected function addEagerConstraintsWithCompositeKey(array $models): void + { + $keys = (new BaseCollection($models))->map( + function (Model $model) { + return array_map( + fn (string $column) => $model[$column], + $this->localKey + ); + } + )->values()->unique(null, true)->all(); + + $this->query->where( + function (Builder $query) use ($keys) { + foreach ($keys as $key) { + $query->orWhere( + function (Builder $query) use ($key) { + foreach ($this->foreignKey as $i => $column) { + if ($i === 0) { + $this->whereJsonContainsOrMemberOf( + $query, + $this->path, + $key[$i], + fn ($parentKey) => $this->parentKeyToArray($parentKey) + ); + } else { + $query->where( + $this->related->qualifyColumn($column), + '=', + $key[$i] + ); + } + } + } + ); + } + } + ); + } + + /** + * Match the eagerly loaded results to their parents for a composite key. + * + * @param array $models + * @param \Illuminate\Database\Eloquent\Collection $results + * @param string $relation + * @return array + */ + protected function matchWithCompositeKey(array $models, Collection $results, string $relation): array + { + $dictionary = $this->buildDictionaryWithCompositeKey($results); + + foreach ($models as $model) { + $values = array_map( + fn ($key) => $model->$key, + $this->localKey + ); + + $key = implode("\0", $values); + + if (isset($dictionary[$key])) { + $model->setRelation( + $relation, + $this->getRelationValue($dictionary, $key, 'many') + ); + } + } + + return $models; + } + + /** + * Build model dictionary keyed by the relation's composite foreign key. + * + * @param \Illuminate\Database\Eloquent\Collection $results + * @return array + */ + protected function buildDictionaryWithCompositeKey(Collection $results): array + { + $dictionary = []; + + $foreignKey = $this->getForeignKeyName(); + + $additionalColumns = $this->getAdditionalForeignKeyNames(); + + foreach ($results as $result) { + $additionalValues = array_map( + fn (string $column) => $result->getAttribute($column), + $additionalColumns + ); + + foreach($result->$foreignKey as $value) { + $values = [$value, ...$additionalValues]; + + $key = implode("\0", $values); + + $dictionary[$key][] = $result; + } + } + + return $dictionary; + } + + /** + * Add the constraints for a relationship query for a composite key. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return void + */ + public function getRelationExistenceQueryWithCompositeKey(Builder $query): void + { + $columns = $this->getAdditionalForeignKeyNames(); + + foreach ($columns as $i => $column) { + $query->whereColumn( + $this->parent->qualifyColumn($column), + '=', + $query->qualifyColumn($this->localKey[$i]) + ); + } + } + + /** + * Get the plain additional foreign keys. + * + * @return array + */ + protected function getAdditionalForeignKeyNames(): array + { + $names = []; + + $columns = array_slice($this->foreignKey, 1, preserve_keys: true); + + foreach ($columns as $i => $column) { + $segments = explode('.', $column); + + $names[$i] = end($segments); + } + + return $names; + } +} diff --git a/src/Relations/Traits/IsJsonRelation.php b/src/Relations/Traits/IsJsonRelation.php index e23fde1..ec6a817 100644 --- a/src/Relations/Traits/IsJsonRelation.php +++ b/src/Relations/Traits/IsJsonRelation.php @@ -31,23 +31,6 @@ trait IsJsonRelation */ protected $key; - /** - * Create a new JSON relationship instance. - * - * @return void - */ - public function __construct() - { - $args = func_get_args(); - - $foreignKey = explode('[]->', $args[2]); - - $this->path = $foreignKey[0]; - $this->key = $foreignKey[1] ?? null; - - parent::__construct(...$args); - } - /** * Hydrate the pivot relationship on the models. * diff --git a/tests/CompositeKeys/BelongsToJsonTest.php b/tests/CompositeKeys/BelongsToJsonTest.php new file mode 100644 index 0000000..73a2203 --- /dev/null +++ b/tests/CompositeKeys/BelongsToJsonTest.php @@ -0,0 +1,92 @@ +tasks; + + $this->assertEquals([101, 103, 105], $tasks->pluck('id')->all()); + } + + public function testLazyLoadingWithObjects() + { + $tasks = Employee::find(121)->tasksWithObjects; + + $this->assertEquals([101, 103, 105], $tasks->pluck('id')->all()); + $this->assertEquals(['work_stream' => ['active' => true]], $tasks[0]->pivot->getAttributes()); + } + + public function testEmptyLazyLoading() + { + DB::enableQueryLog(); + + $tasks = (new Employee())->tasks; + + $this->assertInstanceOf(Collection::class, $tasks); + $this->assertEmpty(DB::getQueryLog()); + } + + public function testEagerLoading() + { + $employees = Employee::with('tasks')->get(); + + $this->assertEquals([101, 103, 105], $employees[0]->tasks->pluck('id')->all()); + $this->assertEquals([102, 104], $employees[1]->tasks->pluck('id')->all()); + $this->assertEquals([], $employees[3]->tasks->pluck('id')->all()); + } + + public function testEagerLoadingWithObjects() + { + $employees = Employee::with('tasksWithObjects')->get(); + + $this->assertEquals([101, 103, 105], $employees[0]->tasksWithObjects->pluck('id')->all()); + $this->assertEquals([102, 104], $employees[1]->tasksWithObjects->pluck('id')->all()); + $this->assertEquals([], $employees[3]->tasksWithObjects->pluck('id')->all()); + $this->assertEquals(['work_stream' => ['active' => true]], $employees[0]->tasksWithObjects[0]->pivot->getAttributes()); + } + + public function testLazyEagerLoading() + { + $employees = Employee::all()->load('tasks'); + + $this->assertEquals([101, 103, 105], $employees[0]->tasks->pluck('id')->all()); + $this->assertEquals([102, 104], $employees[1]->tasks->pluck('id')->all()); + $this->assertEquals([], $employees[3]->tasks->pluck('id')->all()); + } + + public function testLazyEagerLoadingWithObjects() + { + $employees = Employee::all()->load('tasksWithObjects'); + + $this->assertEquals([101, 103, 105], $employees[0]->tasksWithObjects->pluck('id')->all()); + $this->assertEquals([102, 104], $employees[1]->tasksWithObjects->pluck('id')->all()); + $this->assertEquals([], $employees[3]->tasksWithObjects->pluck('id')->all()); + $this->assertEquals(['work_stream' => ['active' => true]], $employees[0]->tasksWithObjects[0]->pivot->getAttributes()); + } + + public function testExistenceQuery() + { + $employees = Employee::has('tasks')->orderBy('id')->get(); + + $this->assertEquals([121, 122, 123], $employees->pluck('id')->all()); + } + + public function testExistenceQueryWithObjects() + { + if (in_array($this->connection, ['sqlite', 'sqlsrv'])) { + $this->markTestSkipped(); + } + + $employees = Employee::has('tasksWithObjects')->orderBy('id')->get(); + + $this->assertEquals([121, 122, 123], $employees->pluck('id')->all()); + } +} diff --git a/tests/CompositeKeys/HasManyJsonTest.php b/tests/CompositeKeys/HasManyJsonTest.php new file mode 100644 index 0000000..f22f1a9 --- /dev/null +++ b/tests/CompositeKeys/HasManyJsonTest.php @@ -0,0 +1,104 @@ +employees; + + $this->assertEquals([121, 123], $employees->pluck('id')->all()); + } + + public function testLazyLoadingWithObjects() + { + if (in_array($this->connection, ['sqlite', 'sqlsrv'])) { + $this->markTestSkipped(); + } + + $employees = Task::find(101)->employeesWithObjects; + + $this->assertEquals([121, 123], $employees->pluck('id')->all()); + $this->assertEquals(['work_stream' => ['active' => true]], $employees[0]->pivot->getAttributes()); + } + + public function testEmptyLazyLoading() + { + DB::enableQueryLog(); + + $employees = (new Task())->employees; + + $this->assertInstanceOf(Collection::class, $employees); + $this->assertEmpty(DB::getQueryLog()); + } + + public function testEagerLoading() + { + $tasks = Task::with('employees')->get(); + + $this->assertEquals([121, 123], $tasks[0]->employees->pluck('id')->all()); + $this->assertEquals([122], $tasks[1]->employees->pluck('id')->all()); + $this->assertEquals([], $tasks[5]->employees->pluck('id')->all()); + } + + public function testEagerLoadingWithObjects() + { + if (in_array($this->connection, ['sqlite', 'sqlsrv'])) { + $this->markTestSkipped(); + } + + $tasks = Task::with('employeesWithObjects')->get(); + + $this->assertEquals([121, 123], $tasks[0]->employeesWithObjects->pluck('id')->all()); + $this->assertEquals([122], $tasks[1]->employeesWithObjects->pluck('id')->all()); + $this->assertEquals([], $tasks[5]->employeesWithObjects->pluck('id')->all()); + $this->assertEquals(['work_stream' => ['active' => true]], $tasks[0]->employeesWithObjects[0]->pivot->getAttributes()); + } + + public function testLazyEagerLoading() + { + $tasks = Task::all()->load('employees'); + + $this->assertEquals([121, 123], $tasks[0]->employees->pluck('id')->all()); + $this->assertEquals([122], $tasks[1]->employees->pluck('id')->all()); + $this->assertEquals([], $tasks[5]->employees->pluck('id')->all()); + } + + public function testLazyEagerLoadingWithObjects() + { + if (in_array($this->connection, ['sqlite', 'sqlsrv'])) { + $this->markTestSkipped(); + } + + $tasks = Task::all()->load('employeesWithObjects'); + + $this->assertEquals([121, 123], $tasks[0]->employeesWithObjects->pluck('id')->all()); + $this->assertEquals([122], $tasks[1]->employeesWithObjects->pluck('id')->all()); + $this->assertEquals([], $tasks[5]->employeesWithObjects->pluck('id')->all()); + $this->assertEquals(['work_stream' => ['active' => true]], $tasks[0]->employeesWithObjects[0]->pivot->getAttributes()); + } + + public function testExistenceQuery() + { + $tasks = Task::has('employees')->orderBy('id')->get(); + + $this->assertEquals([101, 102, 103, 104, 105], $tasks->pluck('id')->all()); + } + + public function testExistenceQueryWithObjects() + { + if (in_array($this->connection, ['sqlite', 'sqlsrv'])) { + $this->markTestSkipped(); + } + + $tasks = Task::has('employeesWithObjects')->orderBy('id')->get(); + + $this->assertEquals([101, 102, 103, 104, 105], $tasks->pluck('id')->all()); + } +} diff --git a/tests/Concatenation/HasManyThroughJsonTest.php b/tests/Concatenation/HasManyThroughJsonTest.php index 61eb7d9..208a710 100644 --- a/tests/Concatenation/HasManyThroughJsonTest.php +++ b/tests/Concatenation/HasManyThroughJsonTest.php @@ -15,7 +15,7 @@ public function testLazyLoading() { $projects = Role::find(2)->projects; - $this->assertEquals([71, 73], $projects->pluck('id')->all()); + $this->assertEquals([91, 93], $projects->pluck('id')->all()); } public function testLazyLoadingWithObjects() @@ -26,7 +26,7 @@ public function testLazyLoadingWithObjects() $projects = Role::find(2)->projects2; - $this->assertEquals([71, 73], $projects->pluck('id')->all()); + $this->assertEquals([91, 93], $projects->pluck('id')->all()); $pivot = $projects[0]->pivot; $this->assertInstanceOf(Pivot::class, $pivot); $this->assertTrue($pivot->exists); @@ -35,7 +35,7 @@ public function testLazyLoadingWithObjects() public function testLazyLoadingWithReverseRelationship() { - $roles = Project::find(71)->roles; + $roles = Project::find(91)->roles; $this->assertEquals([1, 2], $roles->pluck('id')->all()); } @@ -46,7 +46,7 @@ public function testLazyLoadingWithReverseRelationshipAndObjects() $this->markTestSkipped(); } - $roles = Project::find(71)->roles2; + $roles = Project::find(91)->roles2; $this->assertEquals([1, 2], $roles->pluck('id')->all()); $pivot = $roles[0]->pivot; @@ -76,9 +76,9 @@ public function testEagerLoading() { $roles = Role::with('projects')->get(); - $this->assertEquals([71], $roles[0]->projects->pluck('id')->all()); - $this->assertEquals([71, 73], $roles[1]->projects->pluck('id')->all()); - $this->assertEquals([73], $roles[2]->projects->pluck('id')->all()); + $this->assertEquals([91], $roles[0]->projects->pluck('id')->all()); + $this->assertEquals([91, 93], $roles[1]->projects->pluck('id')->all()); + $this->assertEquals([93], $roles[2]->projects->pluck('id')->all()); $this->assertEquals([], $roles[3]->projects->pluck('id')->all()); } @@ -90,9 +90,9 @@ public function testEagerLoadingWithObjects() $roles = Role::with('projects2')->get(); - $this->assertEquals([71], $roles[0]->projects2->pluck('id')->all()); - $this->assertEquals([71, 73], $roles[1]->projects2->pluck('id')->all()); - $this->assertEquals([73], $roles[2]->projects2->pluck('id')->all()); + $this->assertEquals([91], $roles[0]->projects2->pluck('id')->all()); + $this->assertEquals([91, 93], $roles[1]->projects2->pluck('id')->all()); + $this->assertEquals([93], $roles[2]->projects2->pluck('id')->all()); $this->assertEquals([], $roles[3]->projects2->pluck('id')->all()); $pivot = $roles[1]->projects2[0]->pivot; $this->assertInstanceOf(Pivot::class, $pivot); @@ -148,7 +148,7 @@ public function testExistenceQueryWithReverseRelationship() { $projects = Project::has('roles')->get(); - $this->assertEquals([71, 73], $projects->pluck('id')->all()); + $this->assertEquals([91, 93], $projects->pluck('id')->all()); } public function testExistenceQueryWithReverseRelationshipAndObjects() @@ -159,6 +159,6 @@ public function testExistenceQueryWithReverseRelationshipAndObjects() $projects = Project::has('roles2')->get(); - $this->assertEquals([71, 73], $projects->pluck('id')->all()); + $this->assertEquals([91, 93], $projects->pluck('id')->all()); } } diff --git a/tests/Models/Employee.php b/tests/Models/Employee.php new file mode 100644 index 0000000..f004622 --- /dev/null +++ b/tests/Models/Employee.php @@ -0,0 +1,31 @@ + 'json', + 'options' => 'json', + ]; + + public function tasks(): BelongsToJson + { + return $this->belongsToJson( + Task::class, + ['work_stream_ids', 'team_id'], + ['work_stream_id', 'team_id'] + ); + } + + public function tasksWithObjects(): BelongsToJson + { + return $this->belongsToJson( + Task::class, + ['options->work_streams[]->work_stream->id', 'team_id'], + ['work_stream_id', 'team_id'] + ); + } +} diff --git a/tests/Models/Permission.php b/tests/Models/Permission.php index 1396b2a..76bbff3 100644 --- a/tests/Models/Permission.php +++ b/tests/Models/Permission.php @@ -6,11 +6,9 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Staudenmeir\EloquentHasManyDeep\HasManyDeep; use Staudenmeir\EloquentHasManyDeep\HasRelationships; -use Staudenmeir\EloquentJsonRelations\HasJsonRelationships; class Permission extends Model { - use HasJsonRelationships; use HasRelationships; public $timestamps = false; diff --git a/tests/Models/Task.php b/tests/Models/Task.php new file mode 100644 index 0000000..1f9156c --- /dev/null +++ b/tests/Models/Task.php @@ -0,0 +1,26 @@ +hasManyJson( + Employee::class, + ['work_stream_ids', 'team_id'], + ['work_stream_id', 'team_id'] + ); + } + + public function employeesWithObjects(): HasManyJson + { + return $this->hasManyJson( + Employee::class, + ['options->work_streams[]->work_stream->id', 'team_id'], + ['work_stream_id', 'team_id'] + ); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 23d078e..d838b85 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -9,11 +9,13 @@ use Tests\Models\Category; use Tests\Models\Comment; use Tests\Models\Country; +use Tests\Models\Employee; use Tests\Models\Locale; use Tests\Models\Permission; use Tests\Models\Post; use Tests\Models\Product; use Tests\Models\Role; +use Tests\Models\Task; use Tests\Models\Team; use Tests\Models\Project; use Tests\Models\User; @@ -107,6 +109,19 @@ protected function migrate(): void $table->unsignedInteger('id'); $table->unsignedInteger('user_id'); }); + + DB::schema()->create('tasks', function (Blueprint $table) { + $table->unsignedInteger('id'); + $table->unsignedInteger('team_id'); + $table->unsignedInteger('work_stream_id'); + }); + + DB::schema()->create('employees', function (Blueprint $table) { + $table->unsignedInteger('id'); + $table->unsignedInteger('team_id'); + $table->json('work_stream_ids'); + $table->json('options'); + }); } protected function seed(): void @@ -206,18 +221,58 @@ protected function seed(): void Permission::create(['id' => 85, 'role_id' => 4]); Project::create([ - 'id' => 71, + 'id' => 91, 'user_id' => 21, ]); Project::create([ - 'id' => 72, + 'id' => 92, 'user_id' => 22, ]); Project::create([ - 'id' => 73, + 'id' => 93, 'user_id' => 23, ]); + Task::create(['id' => 101, 'team_id' => 1, 'work_stream_id' => 111]); + Task::create(['id' => 102, 'team_id' => 2, 'work_stream_id' => 111]); + Task::create(['id' => 103, 'team_id' => 1, 'work_stream_id' => 112]); + Task::create(['id' => 104, 'team_id' => 2, 'work_stream_id' => 112]); + Task::create(['id' => 105, 'team_id' => 1, 'work_stream_id' => 113]); + Task::create(['id' => 106, 'team_id' => 2, 'work_stream_id' => 113]); + + Employee::create(['id' => 121, 'team_id' => 1, 'work_stream_ids' => [111, 112, 113], + 'options' => [ + 'work_streams' => [ + ['work_stream' => ['id' => 111, 'active' => true]], + ['work_stream' => ['id' => 112, 'active' => false]], + ['work_stream' => ['id' => 113, 'active' => true]], + ], + ], + ]); + Employee::create(['id' => 122, 'team_id' => 2, 'work_stream_ids' => [111, 112], + 'options' => [ + 'work_streams' => [ + ['work_stream' => ['id' => 111, 'active' => false]], + ['work_stream' => ['id' => 112, 'active' => true]], + ], + ], + ]); + Employee::create(['id' => 123, 'team_id' => 1, 'work_stream_ids' => [111], + 'options' => [ + 'work_streams' => [ + ['work_stream' => ['id' => 111, 'active' => true]], + ], + ], + ]); + Employee::create(['id' => 124, 'team_id' => 3, 'work_stream_ids' => [111, 112], + 'options' => [ + 'work_streams' => [ + ['work_stream' => ['id' => 111, 'active' => true]], + ['work_stream' => ['id' => 112, 'active' => false]], + ], + ], + ]); + Model::reguard(); } }