Skip to content

Commit 89acddd

Browse files
committed
add through relation
1 parent dbb44d5 commit 89acddd

24 files changed

+1134
-366
lines changed

.php-cs-fixer.dist.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
use CodeIgniter\CodingStandard\CodeIgniter4;
4+
use Nexus\CsConfig\Factory;
5+
use PhpCsFixer\Finder;
6+
7+
$finder = Finder::create()
8+
->files()
9+
->in([
10+
__DIR__ . '/src/',
11+
__DIR__ . '/tests/',
12+
])
13+
->exclude([
14+
'build',
15+
'Views',
16+
])
17+
->append([
18+
__FILE__,
19+
__DIR__ . '/rector.php',
20+
]);
21+
22+
$overrides = [
23+
// 'declare_strict_types' => true,
24+
// 'void_return' => true,
25+
];
26+
27+
$options = [
28+
'finder' => $finder,
29+
'cacheFile' => 'build/.php-cs-fixer.cache',
30+
];
31+
32+
return Factory::create(new CodeIgniter4(), $overrides, $options)->forProjects();

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"autoload": {
2525
"psr-4": {
2626
"Michalsn\\CodeIgniterNestedModel\\": "src"
27-
}
27+
},
28+
"files": ["src/Common.php"]
2829
},
2930
"autoload-dev": {
3031
"psr-4": {

docs/installation.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ composer require michalsn/codeigniter-nested-model
1515

1616
In the example below we will assume that files from this project will be located in `app/ThirdParty/nested-model` directory.
1717

18-
Download this project and then enable it by editing the `app/Config/Autoload.php` file and adding the `Michalsn\CodeIgniterNestedModel` namespace to the `$psr4` array, like in the below example:
18+
Download this project and then enable it by editing the `app/Config/Autoload.php` file and adding the `Michalsn\CodeIgniterNestedModel` namespace to the `$psr4` array. You also have to add `Common.php` to the `$files` array, like in the below example:
1919

2020
```php
2121
<?php
@@ -28,4 +28,11 @@ public $psr4 = [
2828
];
2929

3030
// ...
31+
32+
public $files = [
33+
APPPATH . 'ThirdParty/nested-model/src/Common.php',
34+
];
35+
36+
// ...
37+
3138
```

docs/relations.md

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
# Relations
22

3-
- [One-to-one](#one-to-one)
4-
- [One-to-many](#one-to-many)
5-
- [One-to-many inverse](#one-to-many-inverse)
6-
- [One-of-many](#one-of-many)
3+
- [One to one](#one-to-one)
4+
- [One to many](#one-to-many)
5+
- [One to many inverse](#one-to-many-inverse)
6+
- [One of many](#one-of-many)
7+
- [Has one through](#has-one-through)
8+
- [Has many through](#has-many-through)
79

8-
## One-to-one
10+
## One to one
911

1012
A one-to-one relationship where one model is associated with exactly one instance of another model.
1113

@@ -38,7 +40,7 @@ class UserModel extends Model
3840
model(UserModel::class)->with('profile')->find(1);
3941
```
4042

41-
## One-to-many
43+
## One to many
4244

4345
A one-to-many relationship where one model is associated with multiple instances of another model.
4446

@@ -71,7 +73,7 @@ class UserModel extends Model
7173
model(UserModel::class)->with('posts')->find(1);
7274
```
7375

74-
## One-to-many inverse
76+
## One to many inverse
7577

7678
A one-to-many inverse relationship where a model belongs to another model.
7779

@@ -104,7 +106,7 @@ class PostModel extends Model
104106
model(PostModel::class)->with('user')->find(1);
105107
```
106108

107-
## One-of-many
109+
## One of many
108110

109111
A specialized type of one-to-one relationship where a parent model has multiple related records, but only one of them is considered active or relevant at any given time, based on a specific condition (e.g., the most recent, the highest priority, or the one meeting a custom criterion).
110112

@@ -157,3 +159,73 @@ model(UserModel::class)->with('firstPost')->find(1);
157159

158160
model(UserModel::class)->with('bestPost')->find(1);
159161
```
162+
163+
## Has one through
164+
165+
A one-to-one relationship that is linked through an intermediate model. This means the parent model is related to a single record in the final model through another model acting as a bridge.
166+
167+
### Example
168+
169+
Consider an application where:
170+
171+
- A **User** belongs to a **Company**.
172+
- Each **Company** has an **Address**.
173+
174+
You want to retrieve a User's Address without needing to manually query through the Company.
175+
176+
```php
177+
class UserModel extends Model
178+
{
179+
use HasRelations;
180+
181+
// ...
182+
183+
public function initialize()
184+
{
185+
$this->initRelations();
186+
}
187+
188+
public function address(): Relation
189+
{
190+
return $this->hasOneThrough(AddressModel::class, CompanyModel::class);
191+
}
192+
}
193+
```
194+
195+
### Usage
196+
197+
```php
198+
model(UserModel::class)->with('address')->find(1);
199+
```
200+
201+
## Has many through
202+
203+
A one-to-many relationship that is linked through an intermediate model. This means the parent model is related to multiple records in the final model through another model acting as a bridge.
204+
205+
### Example
206+
207+
Consider an application where::
208+
209+
- A *Country* has many *Users*.
210+
- A *User* has many *Posts*.
211+
212+
You want to fetch all Posts for a Country, even though the Posts table does not directly reference the Country.
213+
214+
```php
215+
class CountryModel extends Model
216+
{
217+
use HasRelations;
218+
219+
// ...
220+
221+
public function initialize()
222+
{
223+
$this->initRelations();
224+
}
225+
226+
public function posts(): Relation
227+
{
228+
return $this->hasManyThrough(PostModel::class, UserModel::class);
229+
}
230+
}
231+
```

src/Common.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
use CodeIgniter\Model;
4+
5+
if (! function_exists('get_foreign_key')) {
6+
/**
7+
* Get foreign key based on model info.
8+
*/
9+
function get_foreign_key(Model $model): string
10+
{
11+
$refObj = new ReflectionObject($model);
12+
13+
$refProp = $refObj->getProperty('table');
14+
$table = $refProp->getValue($model);
15+
16+
$refProp = $refObj->getProperty('primaryKey');
17+
$primaryKey = $refProp->getValue($model);
18+
19+
return singular($table) . '_' . $primaryKey;
20+
}
21+
}
22+
23+
if (! function_exists('get_primary_key')) {
24+
/**
25+
* Get primary key based on model info.
26+
*/
27+
function get_primary_key(Model $model): string
28+
{
29+
$refObj = new ReflectionObject($model);
30+
31+
$refProp = $refObj->getProperty('primaryKey');
32+
33+
return $refProp->getValue($model);
34+
}
35+
}

src/Language/en/NestedModel.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<?php
22

33
return [
4-
'methodNotSupported' => 'This method is not supported for the "{0}" relation.',
4+
'methodNotSupported' => 'This method is not supported for the "{0}" relation.',
55
'parentRelationNotDeclared' => 'Parent relation "{0}" has not been declared yet.',
6-
'relationNotDefined' => 'Relation "{0}" is not defined.',
7-
'missingReturnType' => 'Method "{0}()" is missing a required return type declaration.',
8-
'incorrectReturnType' => 'Method "{0}()" returned an incorrect type.'
6+
'relationNotDefined' => 'Relation "{0}" is not defined.',
7+
'missingReturnType' => 'Method "{0}()" is missing a required return type declaration.',
8+
'incorrectReturnType' => 'Method "{0}()" returned an incorrect type.',
99
];

src/Relation.php

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

55
namespace Michalsn\CodeIgniterNestedModel;
66

7-
use BadMethodCallException;
87
use Closure;
98
use CodeIgniter\Entity\Entity;
109
use CodeIgniter\Model;
@@ -17,6 +16,7 @@ class Relation
1716
{
1817
private ?Closure $conditions = null;
1918
private ?OfMany $ofMany = null;
19+
private ?Through $through = null;
2020

2121
/**
2222
* @var list<With>
@@ -98,7 +98,63 @@ public function applyWith(): static
9898
return $this;
9999
}
100100

101-
public function getOfMany(): OfMany
101+
public function setThrough(Model $model, ?string $foreignKey = null, ?string $primaryKey = null): static
102+
{
103+
$this->through = new Through(
104+
$model,
105+
$foreignKey ?? get_foreign_key($model),
106+
$primaryKey ?? get_primary_key($model)
107+
);
108+
109+
return $this;
110+
}
111+
112+
public function hasThrough(): bool
113+
{
114+
return $this->through !== null;
115+
}
116+
117+
public function applyThrough(array $id, string $primaryKey): static
118+
{
119+
if ($this->through === null) {
120+
$this->model->whereIn(
121+
sprintf(
122+
'%s.%s',
123+
$this->model->getTable(),
124+
$this->foreignKey
125+
),
126+
$id
127+
);
128+
129+
return $this;
130+
}
131+
132+
$this->model
133+
->select(sprintf('%s.*', $this->model->getTable()))
134+
->join(
135+
$this->through->model->getTable(),
136+
sprintf(
137+
'%s.%s = %s.%s',
138+
$this->through->model->getTable(),
139+
$this->through->foreignKey,
140+
$this->model->getTable(),
141+
$this->primaryKey
142+
),
143+
'LEFT'
144+
)
145+
->whereIn(
146+
sprintf(
147+
'%s.%s',
148+
$this->through->model->getTable(),
149+
$primaryKey
150+
),
151+
$id
152+
);
153+
154+
return $this;
155+
}
156+
157+
public function getOfMany(): ?OfMany
102158
{
103159
return $this->ofMany;
104160
}

src/Through.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Michalsn\CodeIgniterNestedModel;
4+
5+
use CodeIgniter\Model;
6+
7+
readonly class Through
8+
{
9+
public function __construct(public Model $model, public string $foreignKey, public string $primaryKey)
10+
{
11+
}
12+
}

0 commit comments

Comments
 (0)