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); + } }