From 07113a6104cf45590efba64f3c857f5a30eb0812 Mon Sep 17 00:00:00 2001 From: Ingolf Steinhardt Date: Thu, 6 Aug 2026 14:10:53 +0200 Subject: [PATCH] Do not mask the render error with "Parent has not been set" When rendering an attribute with a render setting failed, parseValue() retried with the default render settings and carried the parent over from the failing setting: $objSettingsFallback = $this->getDefaultRenderSettings()->setParent($objSettings->getParent()); Render settings created on the fly have no parent - getDefaultRenderSettings() builds them without one - so getParent() threw "Parent has not been set" and replaced the real error with a misleading one. The FIXME in that line said exactly that. Parsing an item without a render setting collection runs into this: parseValue() falls back to the default render settings, and any attribute failing to render (in the case at hand a text attribute holding an array, which makes the template raise "Array to string conversion") ends in a 500 that points at the parent instead of the actual problem. The parent is now only carried over when the failing setting has one, and a failure of the fallback rendering re-throws the original exception instead of its follow-up error. --- src/Attribute/Base.php | 38 +++++++++++++++++++++---- tests/Attribute/BaseTest.php | 55 ++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 5 deletions(-) diff --git a/src/Attribute/Base.php b/src/Attribute/Base.php index 8a197af4a..7ff2849fe 100644 --- a/src/Attribute/Base.php +++ b/src/Attribute/Base.php @@ -626,6 +626,30 @@ public function getDefaultRenderSettings() return new Simple(['template' => 'mm_attr_' . ($this->get('type') ?? '')]); } + /** + * Build the render setting to retry with when rendering with the passed setting has failed. + * + * The parent is only carried over when the passed setting has one - render settings created on the fly (see + * getDefaultRenderSettings()) do not have a parent and must not let the fallback fail with + * "Parent has not been set", which would mask the problem that made the rendering fail in the first place. + * + * @param ISimpleRenderSetting $objSettings The render setting that failed. + * + * @return ISimpleRenderSetting + */ + protected function getFallbackRenderSetting($objSettings) + { + $objSettingsFallback = $this->getDefaultRenderSettings(); + + try { + $parent = $objSettings->getParent(); + } catch (\RuntimeException) { + return $objSettingsFallback; + } + + return $objSettingsFallback->setParent($parent); + } + /** * {@inheritdoc} */ @@ -646,17 +670,21 @@ public function parseValue($arrRowData, $strOutputFormat = 'text', $objSettings // Text rendering is mandatory, try with the current setting, // upon exception, try again with the default settings, as the template name might have changed. - // if this fails again, we are definitely out of luck and bail the exception. + // if this fails again, we are definitely out of luck and bail the original exception. try { $arrResult['text'] = $objTemplate->parse('text', true); - } catch (\Exception $e) { - // FIXME: this throws when no parent has been set - need to catch! - $objSettingsFallback = $this->getDefaultRenderSettings()->setParent($objSettings->getParent()); + } catch (\Exception $exception) { + $objSettingsFallback = $this->getFallbackRenderSetting($objSettings); $objTemplate = new Template($objSettingsFallback->get('template') ?? ''); $this->prepareTemplate($objTemplate, $arrRowData, $objSettingsFallback); - $arrResult['text'] = $objTemplate->parse('text', true); + try { + $arrResult['text'] = $objTemplate->parse('text', true); + } catch (\Exception $fallbackException) { + // Do not mask the real problem with the follow-up error of the fallback rendering. + throw $exception; + } } } else { // Text rendering is mandatory, therefore render using default render settings. diff --git a/tests/Attribute/BaseTest.php b/tests/Attribute/BaseTest.php index 382036b2b..22d13e3ba 100644 --- a/tests/Attribute/BaseTest.php +++ b/tests/Attribute/BaseTest.php @@ -22,6 +22,9 @@ use MetaModels\Attribute\Base; use MetaModels\IMetaModel; +use MetaModels\Render\Setting\ICollection; +use MetaModels\Render\Setting\ISimple; +use MetaModels\Render\Setting\Simple as RenderSetting; use PHPUnit\Framework\TestCase; /** @@ -191,4 +194,56 @@ public function testGetFieldDefinition() self::assertEquals('some_widget_class', $fieldDefinition['eval']['tl_class']); self::assertEquals(true, $fieldDefinition['eval']['readonly']); } + + /** + * The fallback render setting must not fail for render settings without parent. + * + * Settings created on the fly (getDefaultRenderSettings()) have no parent - obtaining it would throw and thereby + * mask the problem that made the rendering fail in the first place. + * + * @return void + */ + public function testGetFallbackRenderSettingWithoutParent() + { + $attribute = $this->getAttribute(); + + $fallback = $this->getFallbackRenderSetting($attribute, new RenderSetting([])); + + self::assertInstanceOf(RenderSetting::class, $fallback); + self::assertSame('mm_attr_base', $fallback->get('template')); + } + + /** + * The parent of the failing render setting is carried over to the fallback when there is one. + * + * @return void + */ + public function testGetFallbackRenderSettingKeepsParent() + { + $attribute = $this->getAttribute(); + $parent = $this->getMockForAbstractClass(ICollection::class); + + $setting = new RenderSetting([]); + $setting->setParent($parent); + + $fallback = $this->getFallbackRenderSetting($attribute, $setting); + + self::assertSame($parent, $fallback->getParent()); + } + + /** + * Call the protected getFallbackRenderSetting method on the passed attribute. + * + * @param Base $attribute The attribute to call the method on. + * @param ISimple $setting The render setting that failed. + * + * @return ISimple + */ + private function getFallbackRenderSetting($attribute, $setting) + { + $method = new \ReflectionMethod($attribute, 'getFallbackRenderSetting'); + $method->setAccessible(true); + + return $method->invoke($attribute, $setting); + } }