Skip to content

Commit 0cc503f

Browse files
authored
Merge pull request #211 from akalongman/feature/support-many-to-many
Add support for many to many relation
2 parents e3759ef + 1eddc64 commit 0cc503f

22 files changed

Lines changed: 1881 additions & 208 deletions

.github/workflows/run-tests.yml

Lines changed: 2 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -8,68 +8,11 @@ jobs:
88
strategy:
99
fail-fast: false
1010
matrix:
11-
laravel: [13.*, 12.*, 11.*, 10.*, 9.*]
12-
php: ['8.5', '8.4', '8.3', '8.2', '8.1', '8.0']
13-
include:
14-
- laravel: 8.*
15-
php: '8.1'
16-
- laravel: 8.*
17-
php: '8.0'
18-
- laravel: 8.*
19-
php: '7.4'
20-
- laravel: 8.*
21-
php: '7.3'
22-
- laravel: 7.*
23-
php: '8.0'
24-
- laravel: 7.*
25-
php: '7.4'
26-
- laravel: 7.*
27-
php: '7.3'
28-
- laravel: 7.*
29-
php: '7.2'
30-
- laravel: 6.*
31-
php: '8.0'
32-
- laravel: 6.*
33-
php: '7.4'
34-
- laravel: 6.*
35-
php: '7.3'
36-
- laravel: 6.*
37-
php: '7.2'
38-
- laravel: 5.8
39-
php: '7.2'
40-
- laravel: 5.8
41-
php: '7.1'
42-
- laravel: 5.7
43-
php: '7.2'
44-
- laravel: 5.7
45-
php: '7.1'
46-
- laravel: 5.6
47-
php: '7.2'
48-
- laravel: 5.6
49-
php: '7.1'
11+
laravel: [13.*, 12.*]
12+
php: ['8.5', '8.4', '8.3', '8.2']
5013
exclude:
5114
- laravel: 13.*
5215
php: '8.2'
53-
- laravel: 13.*
54-
php: '8.1'
55-
- laravel: 13.*
56-
php: '8.0'
57-
- laravel: 12.*
58-
php: '8.1'
59-
- laravel: 12.*
60-
php: '8.0'
61-
- laravel: 11.*
62-
php: '8.1'
63-
- laravel: 11.*
64-
php: '8.0'
65-
- laravel: 10.*
66-
php: '8.0'
67-
- laravel: 10.*
68-
php: '8.5'
69-
- laravel: 9.*
70-
php: '8.4'
71-
- laravel: 9.*
72-
php: '8.5'
7316

7417
name: L${{ matrix.laravel }} - PHP${{ matrix.php }}
7518

@@ -88,14 +31,8 @@ jobs:
8831
extensions: mbstring, pdo, sqlite, pdo_sqlite, iconv
8932
coverage: none
9033

91-
- name: Fix dependencies
92-
if: ${{ startsWith(matrix.laravel, '11') || startsWith(matrix.laravel, '12') || startsWith(matrix.laravel, '13') }}
93-
run: |
94-
composer require "laravel/serializable-closure:>=1.3" --no-interaction --no-update
9534
- name: Install dependencies
9635
run: |
97-
composer config audit.block-insecure false || true
98-
composer config --no-plugins allow-plugins.kylekatarnls/update-helper true
9936
composer require "illuminate/database:${{ matrix.laravel }}" --no-interaction --no-update
10037
composer update --no-interaction
10138
- name: Execute tests

README.md

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,41 @@ use Illuminate\Database\Eloquent\Model;
8080
class B extends Model
8181
{
8282
use \Awobaz\Compoships\Compoships;
83-
83+
8484
public function a()
8585
{
8686
return $this->belongsTo('A', ['foreignKey1', 'foreignKey2'], ['ownerKey1', 'ownerKey2']);
8787
}
8888
}
8989
```
9090

91+
We can also define many-to-many relationships with composite keys through a pivot table:
92+
93+
```php
94+
namespace App;
95+
96+
use Illuminate\Database\Eloquent\Model;
97+
98+
class A extends Model
99+
{
100+
use \Awobaz\Compoships\Compoships;
101+
102+
public function b()
103+
{
104+
return $this->belongsToMany(
105+
B::class,
106+
'a_b', // pivot table
107+
['a_foreignKey1', 'a_foreignKey2'], // foreign pivot keys for A
108+
['b_foreignKey1', 'b_foreignKey2'], // foreign pivot keys for B
109+
['localKey1', 'localKey2'], // local keys on A
110+
['localKey1', 'localKey2'] // local keys on B
111+
);
112+
}
113+
}
114+
```
115+
116+
All standard `belongsToMany` operations work with composite keys: `attach()`, `detach()`, `sync()`, `toggle()`, `withPivot()`, `withTimestamps()`, eager loading, and existence queries (`has()`, `whereHas()`).
117+
91118
### Factories
92119

93120
Chances are that you may need factories for your Compoships models. If so, you will probably need to use
@@ -132,23 +159,51 @@ use Illuminate\Database\Eloquent\Model;
132159
class Task extends Model
133160
{
134161
use \Awobaz\Compoships\Compoships;
135-
162+
136163
public function user()
137164
{
138165
return $this->belongsTo(User::class, ['team_id', 'category_id'], ['team_id', 'category_id']);
139166
}
140167
}
141168
```
169+
170+
For a many-to-many scenario, imagine users can be assigned to projects, where both the user and the project are identified by a composite key (`team_id` and `department_id`):
171+
172+
```php
173+
namespace App;
174+
175+
use Illuminate\Database\Eloquent\Model;
176+
177+
class User extends Model
178+
{
179+
use \Awobaz\Compoships\Compoships;
180+
181+
public function projects()
182+
{
183+
return $this->belongsToMany(
184+
Project::class,
185+
'project_user',
186+
['user_team_id', 'user_department_id'],
187+
['project_team_id', 'project_department_id'],
188+
['team_id', 'department_id'],
189+
['team_id', 'department_id']
190+
);
191+
}
192+
}
193+
```
142194
## Supported relationships
143195

144-
**Compoships** only supports the following Laravel's Eloquent relationships:
196+
**Compoships** supports the following Laravel Eloquent relationships:
145197

146198
* hasOne
147-
* HasMany
199+
* hasMany
148200
* belongsTo
201+
* belongsToMany
149202

150203
Also please note that while **nullable columns are supported by Compoships**, relationships with only null values are not currently possible.
151204

205+
**Note on `belongsToMany`:** Custom pivot models (via `using()`) with composite keys are supported. Your custom pivot class should extend `Awobaz\Compoships\Database\Eloquent\Relations\Pivot` instead of Laravel's base `Pivot` class to ensure correct behavior for save, delete, and queue operations.
206+
152207
## Support for nullable columns in 2.x
153208

154209
Version 2.x brings support for nullable columns. The results may now be different than on version 1.x when a column is null on a relationship, so we bumped the version to 2.x, as this might be a breaking change.

composer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@
1414
}
1515
],
1616
"require": {
17-
"illuminate/database": ">=5.6 <14.0"
17+
"php": "^8.2",
18+
"illuminate/database": "^12.0|^13.0"
1819
},
1920
"require-dev": {
2021
"ext-sqlite3": "*",
2122
"fakerphp/faker": "^1.18",
22-
"phpunit/phpunit": "^6.0|^8.0|^9.0|^10.0|^11.0|^12.0"
23+
"phpunit/phpunit": "^11.0|^12.0"
2324
},
2425
"autoload": {
2526
"psr-4": {

src/Database/Eloquent/Concerns/HasRelationships.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Awobaz\Compoships\Compoships;
66
use Awobaz\Compoships\Database\Eloquent\Relations\BelongsTo;
7+
use Awobaz\Compoships\Database\Eloquent\Relations\BelongsToMany;
78
use Awobaz\Compoships\Database\Eloquent\Relations\HasMany;
89
use Awobaz\Compoships\Database\Eloquent\Relations\HasOne;
910
use Awobaz\Compoships\Exceptions\InvalidUsageException;
@@ -221,6 +222,89 @@ protected function newBelongsTo(Builder $query, Model $child, $foreignKey, $owne
221222
return new BelongsTo($query, $child, $foreignKey, $ownerKey, $relation);
222223
}
223224

225+
/**
226+
* Define a many-to-many relationship.
227+
*
228+
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
229+
*
230+
* @param class-string<TRelatedModel> $related
231+
* @param string|null $table
232+
* @param string|array|null $foreignPivotKey
233+
* @param string|array|null $relatedPivotKey
234+
* @param string|array|null $parentKey
235+
* @param string|array|null $relatedKey
236+
* @param string|null $relation
237+
*
238+
* @return \Awobaz\Compoships\Database\Eloquent\Relations\BelongsToMany<TRelatedModel, $this>
239+
*/
240+
public function belongsToMany(
241+
$related,
242+
$table = null,
243+
$foreignPivotKey = null,
244+
$relatedPivotKey = null,
245+
$parentKey = null,
246+
$relatedKey = null,
247+
$relation = null
248+
) {
249+
if (is_array($foreignPivotKey)) {
250+
$this->validateRelatedModel($related);
251+
}
252+
253+
if (is_null($relation)) {
254+
$relation = $this->guessBelongsToManyRelation();
255+
}
256+
257+
$instance = $this->newRelatedInstance($related);
258+
259+
$foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey();
260+
$relatedPivotKey = $relatedPivotKey ?: $instance->getForeignKey();
261+
262+
if (is_null($table)) {
263+
$table = $this->joiningTable($related, $instance);
264+
}
265+
266+
return $this->newBelongsToMany(
267+
$instance->newQuery(),
268+
$this,
269+
$table,
270+
$foreignPivotKey,
271+
$relatedPivotKey,
272+
$parentKey ?: $this->getKeyName(),
273+
$relatedKey ?: $instance->getKeyName(),
274+
$relation
275+
);
276+
}
277+
278+
/**
279+
* Instantiate a new BelongsToMany relationship.
280+
*
281+
* @template TRelatedModel of \Illuminate\Database\Eloquent\Model
282+
* @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
283+
*
284+
* @param \Illuminate\Database\Eloquent\Builder<TRelatedModel> $query
285+
* @param TDeclaringModel $parent
286+
* @param string $table
287+
* @param string|array $foreignPivotKey
288+
* @param string|array $relatedPivotKey
289+
* @param string|array $parentKey
290+
* @param string|array $relatedKey
291+
* @param string|null $relationName
292+
*
293+
* @return \Awobaz\Compoships\Database\Eloquent\Relations\BelongsToMany<TRelatedModel, TDeclaringModel>
294+
*/
295+
protected function newBelongsToMany(
296+
Builder $query,
297+
Model $parent,
298+
$table,
299+
$foreignPivotKey,
300+
$relatedPivotKey,
301+
$parentKey,
302+
$relatedKey,
303+
$relationName = null
304+
) {
305+
return new BelongsToMany($query, $parent, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey, $relationName);
306+
}
307+
224308
/**
225309
* Honor DB::raw instances.
226310
*

src/Database/Eloquent/Relations/BelongsTo.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@ public function associate($model)
5050
$value = $ownerKey[$i];
5151
$this->child->setAttribute($foreignKey, $value);
5252
}
53-
// BC break in 5.8 : https://github.com/illuminate/database/commit/87b9833019f48b88d98a6afc46f38ce37f08237d
54-
$relationName = property_exists($this, 'relationName') ? $this->relationName : $this->relation;
53+
$relationName = $this->relationName;
5554
if ($model instanceof Model) {
5655
$this->child->setRelation($relationName, $model);
5756
// proper unset // https://github.com/illuminate/database/commit/44411c7288fc7b7d4e5680cfcdaa46d348b5c981
@@ -115,9 +114,6 @@ public function addEagerConstraints(array $models)
115114
$keys[] = $this->related->getTable().'.'.$key;
116115
}
117116

118-
// method \Awobaz\Compoships\Database\Eloquent\Relations\HasOneOrMany::whereInMethod
119-
// 5.6 - does not exist
120-
// 5.7 - added in 5.7.17 / https://github.com/illuminate/database/commit/9af300d1c50c9ec526823c1e6548daa3949bf9a9
121117
$this->query->whereIn($keys, $this->getEagerModelKeys($models));
122118
} else {
123119
parent::addEagerConstraints($models);

0 commit comments

Comments
 (0)