Repository: barryvdh/laravel-ide-helper (PHP)

Issue:
Incorrect PHPDoc being written to model for BelongsTo relation
### Versions:
- ide-helper Version: 2.13.0
- Laravel Version: 10.x
- PHP Version: 8.2.x

### Description:
Hello! 

I'm currently writing the PHPDocs to a few models and I noticed the wrong doc was being generated for the `BelongsTo` relation (and possibly other relations that return single models):

```php
/** @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $createdByUser */
```

However, a `BelongsTo` relation *never* returns a collection so this instead should be:

```php
/** @property-read null|\App\Models\User $createdByUser */
```

This is causing PHPStan Level 2 to fail with:

```
  27     Access to an undefined property Illuminate\Database\Eloquent\Collection<int, App\Models\User>::$name.                                   
```
when trying to call `$model->createdByUser->name`

### Steps To Reproduce:

- Create model with `BelongsTo` relation defined
- Write PHPDocs to model

Thanks!



Discussion: Turns out this was due to the following config:

```php
    'additional_relation_return_types' => [
        'belongsTo' => 'many',
    ],
```

A developer added this to prevent the model helper file generator from failing when using the [Compoships](https://github.com/topclaudy/compoships) package...however, this broke the types for the `BelongsTo` relation.



Fix (patch):
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d9dfde61e..0a918604f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file.
 ### Added
 - Add support for enum default arguments using enum cases. [#1464 / d8vjork](https://github.com/barryvdh/laravel-ide-helper/pull/1464)
 - Add support for real-time facades in the helper file. [#1455 / filipac](https://github.com/barryvdh/laravel-ide-helper/pull/1455)
+- Add support for relations with composite keys. [#1479 / calebdw](https://github.com/barryvdh/laravel-ide-helper/pull/1479)
 
 2024-02-05, 2.14.0
 ------------------
diff --git a/config/ide-helper.php b/config/ide-helper.php
index 80afdb2d1..cec91c68a 100644
--- a/config/ide-helper.php
+++ b/config/ide-helper.php
@@ -322,7 +322,8 @@
     |
     | When using custom relation types its possible for the class name to not contain
     | the proper return type of the relation. The key of the array is the relationship
-    | method name. The value of the array is the return type of the relation.
+    | method name. The value of the array is the return type of the relation ('many'
+    | or 'morphTo').
     | e.g. `'relationName' => 'many'`.
     |
     */
diff --git a/src/Console/ModelsCommand.php b/src/Console/ModelsCommand.php
index 8759a345d..b5cb107b9 100644
--- a/src/Console/ModelsCommand.php
+++ b/src/Console/ModelsCommand.php
@@ -38,6 +38,7 @@
 use Illuminate\Database\Eloquent\Relations\MorphToMany;
 use Illuminate\Database\Eloquent\Relations\Relation;
 use Illuminate\Filesystem\Filesystem;
+use Illuminate\Support\Arr;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Str;
 use phpDocumentor\Reflection\Types\ContextFactory;
@@ -843,12 +844,17 @@ protected function isRelationNullable(string $relation, Relation $relationObj):
         $fkProp = $reflectionObj->getProperty('foreignKey');
         $fkProp->setAccessible(true);
 
-        if ($relation === 'belongsTo') {
-            return isset($this->nullableColumns[$fkProp->getValue($relationObj)]) ||
-                !in_array($fkProp->getValue($relationObj), $this->foreignKeyConstraintsColumns, true);
+        foreach (Arr::wrap($fkProp->getValue($relationObj)) as $foreignKey) {
+            if (isset($this->nullableColumns[$foreignKey])) {
+                return true;
+            }
+
+            if (!in_array($foreignKey, $this->foreignKeyConstraintsColumns, true)) {
+                return true;
+            }
         }
 
-        return isset($this->nullableColumns[$fkProp->getValue($relationObj)]);
+        return false;
     }
 
     /**