# 📃 Laravel Advanced: Little Known Relationships - hasOneThrough() and hasManyThrough()

Có lẽ bạn đã từng sử dụng các mối quan hệ `hasOne`, `hasMany`, `belongsTo` và `belongsToMany` trong các dự án của mình. Nhưng bạn có biết Laravel cũng cung cấp các mối quan hệ `hasOneThrough` và `hasManyThrough`? Chúng rất lý tưởng để truy cập các mối quan hệ lồng nhau mà không cần thêm thao tác phức tạp. Hãy cùng phân tích chi tiết hơn.

Hãy tưởng tượng bạn có một ứng dụng quản lý dự án với các bảng sau:

```
Users: Đại diện cho người dùng trong hệ thống.
Projects: Mỗi người dùng có thể có nhiều dự án.
Tasks: Mỗi dự án có thể có nhiều nhiệm vụ.
Statuses: Mỗi nhiệm vụ có một trạng thái (ví dụ: đang chờ xử lý, đã hoàn thành).
```

Thiết lập Mô hình & Mối quan hệ

Hãy định nghĩa các mô hình và mối quan hệ của chúng trong dự án Laravel của chúng ta.

**🟡 Project Model**

```php
<?php
class Project extends Model
{
  public function user()
  {
    return $this->belongsTo(User::class);
  }
  public function tasks()
  {
    return $this->hasMany(Task::class);
  }
}
```

**🟡 Task Model**

```php
<?php
class Task extends Model
{
  public function project()
  {
    return $this->belongsTo(Project::class);
  }
  public function status()
  {
    return $this->hasOne(Status::class);
  }
}
```

**🟡 Status Model**

```php
<?php
class Status extends Model
{
  public function task()
  {
    return $this->belongsTo(Task::class);
  }
}
```

**🔴 User Model**

```php
class User extends Model
{
  public function projects()
  {
    return $this->hasMany(Project::class);
  }
  public function tasks()
  {
    return $this->hasManyThrough(Task::class, Project::class);
  }
  public function status()
  {
    return $this->hasOneThrough(Status::class, Task::class);
  }
}
```

#### Using hasOneThrough

You can access the status of a task directly from a user:

```php
<?php
$user = User::find(1);
$status = $user->status;
echo $status->name;
```

Nó lấy trạng thái của tác vụ đầu tiên mà nó gặp (dựa trên thứ tự truy vấn cơ sở dữ liệu).

#### Using hasManyThrough

Trong mô hình User, chúng ta đã sử dụng hasManyThrough để lấy tất cả các nhiệm vụ của một người dùng, mặc dù các nhiệm vụ có liên quan trực tiếp đến dự án. Sau đây là cách bạn có thể sử dụng nó:

```php
$user = User::find(1);
$tasks = $user->tasks;
foreach ($tasks as $task) {
  echo $task->name;
}
```

Các mối quan hệ `hasOneThrough` và `hasManyThrough` có thể đơn giản hóa mã của bạn để dễ dàng truy cập các mối quan hệ lồng nhau sâu. Chúng rất phù hợp cho các trường hợp bạn cần duyệt qua nhiều cấp độ quan hệ.

### [Has One Through](https://laravel.com/docs/11.x/eloquent-relationships#has-one-through) <a href="#has-one-through" id="has-one-through"></a>

Ví dụ, trong ứng dụng sửa chữa xe **Vehicle**, mỗi mô hình thợ sửa xe **Mechanic** có thể được liên kết với một mô hình **Car**, và mỗi mô hình **Car** có thể được liên kết với một mô hình người sở hữu **Owner**. Mặc dù cơ sở dữ liệu không có mối quan hệ trực tiếp giữa **mechanic** và **owner**, nhưng **mechanic** có thể truy cập **owner** thông qua mô hình **Car**. Hãy xem xét các bảng cần thiết để định nghĩa mối quan hệ này:

```php
mechanics
  id - integer
  name - string
cars
  id - integer
  model - string
  mechanic_id - integer
owners
  id - integer
  name - string
  car_id - integer
```

Giờ chúng ta đã xem xét cấu trúc bảng cho mối quan hệ này, hãy định nghĩa mối quan hệ đó trên mô hình **Mechanic**:

```php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
class Mechanic extends Model
{
  /**
   * Get the car's owner.
   */
  public function carOwner(): HasOneThrough
  {
    return $this->hasOneThrough(Owner::class, Car::class);
  }
}
```

Tham số đầu tiên được truyền cho phương thức **hasOneThrough** là tên của mô hình cuối cùng mà chúng ta muốn truy cập, trong khi tham số thứ hai là tên của mô hình trung gian.

Hoặc, nếu các mối quan hệ liên quan đã được định nghĩa trên tất cả các mô hình tham gia vào mối quan hệ, bạn có thể định nghĩa một mối quan hệ **"has-one-through"** một cách linh hoạt bằng cách gọi phương thức through và cung cấp tên của các mối quan hệ đó. Ví dụ, nếu mô hình **Mechanic** có mối quan hệ **cars** và mô hình **Car** có mối quan hệ **owner**, bạn có thể định nghĩa một mối quan hệ **"has-one-through"** kết nối **mechanic** và **owner** như sau:

```php
<?php
// String based syntax...
return $this->through('cars')->has('owner');
// Dynamic syntax...
return $this->throughCars()->hasOwner();
```

### Các quy ước chính

Các quy ước khoá ngoại thông thường của Eloquent sẽ được sử dụng khi thực hiện các truy vấn của mối quan hệ. Nếu bạn muốn tuỳ chỉnh các khoá của mối quan hệ, bạn có thể truyền chúng dưới dạng đối số thứ ba và thứ tư vào phương thức `hasOneThrough`. Đối số thứ ba là tên của khoá ngoại trên mô hình trung gian. Đối số thứ tư là tên của khoá ngoại trên mô hình cuối cùng. Đối số thứ năm là khoá nội bộ, trong khi đối số thứ sáu là khoá nội bộ của mô hình trung gian.

```php
<?php
class Mechanic extends Model
{
  /**
   * Get the car's owner.
   */
  public function carOwner(): HasOneThrough
  {
    return $this->hasOneThrough(
      Owner::class,
      Car::class,
      'mechanic_id', // Foreign key on the cars table...
      'car_id', // Foreign key on the owners table...
      'id', // Local key on the mechanics table...
      'id' // Local key on the cars table...
    );
  }
}
```

Hoặc, như đã thảo luận trước đó, nếu các mối quan hệ liên quan đã được định nghĩa trên tất cả các mô hình tham gia vào mối quan hệ, bạn có thể định nghĩa một mối quan hệ "has-one-through" một cách linh hoạt bằng cách gọi phương thức `through` và cung cấp tên của các mối quan hệ đó. Cách tiếp cận này mang lại lợi thế là tái sử dụng các quy ước khoá đã được định nghĩa trên các mối quan hệ hiện có.

```php
<?php
// String based syntax...
return $this->through('cars')->has('owner');
// Dynamic syntax...
return $this->throughCars()->hasOwner();
```

### [Has Many Through](https://laravel.com/docs/11.x/eloquent-relationships#has-many-through) <a href="#has-many-through" id="has-many-through"></a>

Mối quan hệ **"has-many-through"** cung cấp một cách tiện lợi để truy cập các mối quan hệ xa thông qua một mối quan hệ trung gian. Ví dụ, giả sử chúng ta đang xây dựng một nền tảng triển khai như [Laravel Vapor](https://vapor.laravel.com/). Mô hình Project có thể truy cập nhiều mô hình Deployment thông qua một mô hình trung gian là Environment. Sử dụng ví dụ này, bạn có thể dễ dàng thu thập tất cả các lần triển khai cho một dự án cụ thể. Hãy cùng xem các bảng cần thiết để định nghĩa mối quan hệ này:

```php
<?php
projects
  id - integer
  name - string
environments
  id - integer
  project_id - integer
  name - string
deployments
  id - integer
  environment_id - integer
  commit_hash - string
```

Bây giờ chúng ta đã xem xét cấu trúc bảng cho mối quan hệ, hãy cùng định nghĩa mối quan hệ trên mô hình Project:

```php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
class Project extends Model
{
  /**
   * Get all of the deployments for the project.
   */
  public function deployments(): HasManyThrough
  {
    return $this->hasManyThrough(Deployment::class, Environment::class);
  }
}
```

Đối số đầu tiên được truyền vào phương thức `hasManyThrough` là tên của mô hình cuối cùng mà chúng ta muốn truy cập, trong khi đối số thứ hai là tên của mô hình trung gian.

Hoặc, nếu các mối quan hệ liên quan đã được định nghĩa trên tất cả các mô hình tham gia vào mối quan hệ, bạn có thể định nghĩa một mối quan hệ "has-many-through" một cách linh hoạt bằng cách gọi phương thức `through` và cung cấp tên của các mối quan hệ đó. Ví dụ, nếu mô hình Project có mối quan hệ environments và mô hình Environment có mối quan hệ deployments, bạn có thể định nghĩa một mối quan hệ "has-many-through" kết nối project và deployments như sau:

```php
<?php
// String based syntax...
return $this->through('environments')->has('deployments');
// Dynamic syntax...
return $this->throughEnvironments()->hasDeployments();
```

Mặc dù bảng của mô hình Deployment không chứa cột `project_id`, mối quan hệ `hasManyThrough` cung cấp quyền truy cập vào các deployment của một project thông qua `$project->deployments`. Để truy xuất các mô hình này, Eloquent kiểm tra cột `project_id` trên bảng của mô hình trung gian Environment. Sau khi tìm thấy các ID môi trường liên quan, chúng được sử dụng để truy vấn bảng của mô hình Deployment.

#### Các quy ước chính

Các quy ước khoá ngoại thông thường của Eloquent sẽ được sử dụng khi thực hiện các truy vấn của mối quan hệ. Nếu bạn muốn tuỳ chỉnh các khoá của mối quan hệ, bạn có thể truyền chúng dưới dạng đối số thứ ba và thứ tư vào phương thức `hasManyThrough`. Đối số thứ ba là tên của khoá ngoại trên mô hình trung gian. Đối số thứ tư là tên của khoá ngoại trên mô hình cuối cùng. Đối số thứ năm là khoá nội bộ, trong khi đối số thứ sáu là khoá nội bộ của mô hình trung gian.

```php
<?php
class Project extends Model
{
  public function deployments(): HasManyThrough
  {
    return $this->hasManyThrough(
      Deployment::class,
      Environment::class,
      'project_id', // Foreign key on the environments table...
      'environment_id', // Foreign key on the deployments table...
      'id', // Local key on the projects table...
      'id' // Local key on the environments table...
    );
  }
}
```

Hoặc, như đã thảo luận trước đó, nếu các mối quan hệ liên quan đã được định nghĩa trên tất cả các mô hình tham gia vào mối quan hệ, bạn có thể định nghĩa một mối quan hệ "has-many-through" một cách linh hoạt bằng cách gọi phương thức `through` và cung cấp tên của các mối quan hệ đó. Cách tiếp cận này mang lại lợi thế là tái sử dụng các quy ước khoá đã được định nghĩa trên các mối quan hệ hiện có.

```php
<?php
// String based syntax...
return $this->through('environments')->has('deployments');
// Dynamic syntax...
return $this->throughEnvironments()->hasDeployments();
```

Mối quan hệ có phạm vi (Scoped Relationships)

Việc thêm các phương thức bổ sung vào mô hình để ràng buộc các mối quan hệ là rất phổ biến. Ví dụ, bạn có thể thêm một phương thức `featuredPosts` vào mô hình User để ràng buộc mối quan hệ `posts` rộng hơn với một điều kiện `where` bổ sung:

```php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class User extends Model
{
  /**
   * Get the user's posts.
   */
  public function posts(): HasMany
  {
    return $this->hasMany(Post::class)->latest();
  }
  /**
   * Get the user's featured posts.
   */
  public function featuredPosts(): HasMany
  {
    return $this->posts()->where('featured', true);
  }
}
```

Tuy nhiên, nếu bạn cố gắng tạo một mô hình thông qua phương thức `featuredPosts`, thuộc tính `featured` của nó sẽ không được thiết lập thành `true`. Nếu bạn muốn tạo các mô hình thông qua các phương thức mối quan hệ và cũng chỉ định các thuộc tính nên được thêm vào tất cả các mô hình được tạo ra qua mối quan hệ đó, bạn có thể sử dụng phương thức `withAttributes` khi xây dựng truy vấn mối quan hệ.

```php
<?php
/**
 * Get the user's featured posts.
 */
public function featuredPosts(): HasMany
{
    return $this->posts()->withAttributes(['featured' => true]);
}
```

Phương thức `withAttributes` sẽ thêm các điều kiện `where` vào truy vấn sử dụng các thuộc tính đã cho, và nó cũng sẽ thêm các thuộc tính đã cho vào bất kỳ mô hình nào được tạo ra thông qua phương thức mối quan hệ.

```php
<?php
$post = $user->featuredPosts()->create(['title' => 'Featured Post']);
$post->featured; // true
```
