From 79132ba055bf64411d824f79f6029ed0f710a22c Mon Sep 17 00:00:00 2001 From: Arif Hoque Date: Wed, 9 Sep 2026 23:51:57 +0600 Subject: [PATCH] Fix Model::__get() swallowing \Throwable --- src/Phaseolies/Database/Entity/Model.php | 154 ++++++++++---------- tests/API/Presenter/SQLitePresenterTest.php | 1 + tests/Model/CastSystemTest.php | 12 +- 3 files changed, 85 insertions(+), 82 deletions(-) diff --git a/src/Phaseolies/Database/Entity/Model.php b/src/Phaseolies/Database/Entity/Model.php index 375b57cb..93d1b898 100644 --- a/src/Phaseolies/Database/Entity/Model.php +++ b/src/Phaseolies/Database/Entity/Model.php @@ -1008,96 +1008,88 @@ public function reload(string $relation) */ public function __get($name) { - try { - if (array_key_exists($name, $this->attributes)) { - return $this->castForGet($name, $this->attributes[$name]); - } + if (array_key_exists($name, $this->attributes)) { + return $this->castForGet($name, $this->attributes[$name]); + } - if (array_key_exists($name, $this->relations)) { - return $this->relations[$name]; - } + if (array_key_exists($name, $this->relations)) { + return $this->relations[$name]; + } - if ($this->isComputed($name)) { - return $this->resolveComputed($name); - } + if ($this->isComputed($name)) { + return $this->resolveComputed($name); + } - if (method_exists($this, $name)) { - $relation = $this->$name(); - - if ($relation instanceof Builder) { - $relationType = $this->getLastRelationType(); - - switch ($relationType) { - case 'linkOne': - $result = $relation->first(); - $this->setRelation($name, $result); - return $result; - - case 'bindTo': - $result = $relation->first(); - $this->setRelation($name, $result); - return $result; - - case 'linkMany': - $results = $relation->get(); - $this->setRelation($name, $results); - return $results; - - case 'bindToMany': - $relatedModel = app($this->getLastRelatedModel()); - $relatedModelClass = get_class($relatedModel); - $pivotColumns = app('db')->getTableColumns($this->getLastPivotTable()); - $pivotTable = $this->getLastPivotTable(); - $pivotSelects = array_map(function ($column) use ($pivotTable) { - return "{$pivotTable}.{$column} as pivot_{$column}"; - }, $pivotColumns); - - $query = $relatedModel->query() - ->select(array_merge( - ["{$relatedModel->getTable()}.*"], - $pivotSelects - )) - ->join( - $this->getLastPivotTable(), - "{$this->getLastPivotTable()}.{$this->getLastRelatedKey()}", - '=', - "{$relatedModel->getTable()}.{$relatedModel->getKeyName()}" - ) - ->where("{$this->getLastPivotTable()}.{$this->getLastForeignKey()}", '=', $this->getKey()); - - $results = $query->get(); - $grouped = []; - foreach ($results as $result) { - $pivot = []; - foreach ($pivotColumns as $column) { - $pivot[$column] = $result["pivot_{$column}"]; - unset($result["pivot_{$column}"]); - } - $pivotObj = (object) $pivot; - $result->pivot = $pivotObj; - $grouped[$pivot[$this->getLastForeignKey()]][] = $result; + if (method_exists($this, $name)) { + $relation = $this->$name(); + + if ($relation instanceof Builder) { + $relationType = $this->getLastRelationType(); + + switch ($relationType) { + case 'linkOne': + $result = $relation->first(); + $this->setRelation($name, $result); + return $result; + + case 'bindTo': + $result = $relation->first(); + $this->setRelation($name, $result); + return $result; + + case 'linkMany': + $results = $relation->get(); + $this->setRelation($name, $results); + return $results; + + case 'bindToMany': + $relatedModel = app($this->getLastRelatedModel()); + $relatedModelClass = get_class($relatedModel); + $pivotColumns = app('db')->getTableColumns($this->getLastPivotTable()); + $pivotTable = $this->getLastPivotTable(); + $pivotSelects = array_map(function ($column) use ($pivotTable) { + return "{$pivotTable}.{$column} as pivot_{$column}"; + }, $pivotColumns); + + $query = $relatedModel->query() + ->select(array_merge( + ["{$relatedModel->getTable()}.*"], + $pivotSelects + )) + ->join( + $this->getLastPivotTable(), + "{$this->getLastPivotTable()}.{$this->getLastRelatedKey()}", + '=', + "{$relatedModel->getTable()}.{$relatedModel->getKeyName()}" + ) + ->where("{$this->getLastPivotTable()}.{$this->getLastForeignKey()}", '=', $this->getKey()); + + $results = $query->get(); + $grouped = []; + foreach ($results as $result) { + $pivot = []; + foreach ($pivotColumns as $column) { + $pivot[$column] = $result["pivot_{$column}"]; + unset($result["pivot_{$column}"]); } + $pivotObj = (object) $pivot; + $result->pivot = $pivotObj; + $grouped[$pivot[$this->getLastForeignKey()]][] = $result; + } + + $this->setRelation( + $name, + new Collection($relatedModelClass, $grouped[$this->getKey()] ?? []) + ); - $this->setRelation( - $name, - new Collection($relatedModelClass, $grouped[$this->getKey()] ?? []) - ); - - return $results; - } + return $results; } - - return $relation; } - if (!isset($this->attributes[$name])) { - throw new \Exception("Property or relation '$name' does not exist on " . static::class); - } - - return $this->attributes[$name]; - } catch (\Throwable) { - return; + return $relation; } + + return null; } /** diff --git a/tests/API/Presenter/SQLitePresenterTest.php b/tests/API/Presenter/SQLitePresenterTest.php index 8bd2b9a3..635f755b 100644 --- a/tests/API/Presenter/SQLitePresenterTest.php +++ b/tests/API/Presenter/SQLitePresenterTest.php @@ -26,6 +26,7 @@ protected function setUp(): void $container = new Container(); $container->bind('request', fn() => new Request()); $container->bind('url', fn() => UrlGenerator::class); + $container->bind('db', fn() => new Database('default')); $this->pdo = new PDO('sqlite::memory:'); $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); diff --git a/tests/Model/CastSystemTest.php b/tests/Model/CastSystemTest.php index 34b64e36..fe758769 100644 --- a/tests/Model/CastSystemTest.php +++ b/tests/Model/CastSystemTest.php @@ -603,11 +603,21 @@ public function testEnumSetCastSerializesUnitEnum(): void public function testInvalidEnumValueThrows(): void { - // __get swallows all Throwables, so test the handler directly $this->expectException(\ValueError::class); CastManager::resolve(CastTestColor::class)->get('Purple'); } + + public function testInvalidEnumValuePropagatesThroughModelGetter(): void + { + // Model::__get() must let real errors (not just "property missing") + // propagate, instead of silently returning null. + $model = new MockEnumCastModel(['color' => 'Purple']); + + $this->expectException(\ValueError::class); + + $model->color; + } public function testCustomCastGetUppercases(): void { $model = new MockCustomCastModel(['name' => 'doppar']);