diff --git a/core/lib/Drupal/Core/Template/Attribute.php b/core/lib/Drupal/Core/Template/Attribute.php index 846174d..698ce85 100644 --- a/core/lib/Drupal/Core/Template/Attribute.php +++ b/core/lib/Drupal/Core/Template/Attribute.php @@ -99,6 +99,14 @@ protected function createAttributeValue($name, $value) { elseif (!is_object($value)) { $value = new AttributeString($name, $value); } + // Provide a better exception message when invalid object is passed as value. + elseif (is_object($value)) { + throw new \Exception(t('"@attribute_name" attribute value of type "@class" can not be converted to string', array( + '@attribute_name' => $name, + '@class' => get_class($value) + ))); + } + return $value; } diff --git a/core/lib/Drupal/Core/Template/TwigNodeExpressionFunction.php b/core/lib/Drupal/Core/Template/TwigNodeExpressionFunction.php new file mode 100644 index 0000000..3902abd --- /dev/null +++ b/core/lib/Drupal/Core/Template/TwigNodeExpressionFunction.php @@ -0,0 +1,51 @@ +extra_arguments = $extra_arguments; + } + + /** + * {@inheritdoc} + */ + public function compile(\Twig_Compiler $compiler) { + $name = $this->getAttribute('name'); + $function = $compiler->getEnvironment()->getFunction($name); + + $this->setAttribute('name', $name); + $this->setAttribute('type', 'function'); + $this->setAttribute('thing', $function); + $this->setAttribute('needs_environment', $function->needsEnvironment()); + $this->setAttribute('needs_context', $function->needsContext()); + $this->setAttribute('arguments', array_merge($function->getArguments(), $this->extra_arguments)); + if ($function instanceof \Twig_FunctionCallableInterface || $function instanceof \Twig_SimpleFunction) { + $this->setAttribute('callable', $function->getCallable()); + } + + $this->compileCallable($compiler); + } +} + diff --git a/core/lib/Drupal/Core/Template/TwigNodeVisitor.php b/core/lib/Drupal/Core/Template/TwigNodeVisitor.php index 4915c16..d4095a4 100644 --- a/core/lib/Drupal/Core/Template/TwigNodeVisitor.php +++ b/core/lib/Drupal/Core/Template/TwigNodeVisitor.php @@ -39,8 +39,9 @@ function leaveNode(\Twig_NodeInterface $node, \Twig_Environment $env) { } $class = get_class($node); $line = $node->getLine(); + $name = $node->getNode('expr')->hasAttribute('name') ? $node->getNode('expr')->getAttribute('name') : 'Object'; return new $class( - new \Twig_Node_Expression_Function('render_var', new \Twig_Node(array($node->getNode('expr'))), $line), + new TwigNodeExpressionFunction('render_var', new \Twig_Node(array($node->getNode('expr'))), array($name), $line), $line ); } diff --git a/core/tests/Drupal/Tests/Core/Template/AttributeTest.php b/core/tests/Drupal/Tests/Core/Template/AttributeTest.php index bcd04a2..0a83173 100644 --- a/core/tests/Drupal/Tests/Core/Template/AttributeTest.php +++ b/core/tests/Drupal/Tests/Core/Template/AttributeTest.php @@ -9,6 +9,7 @@ use Drupal\Core\Template\Attribute; use Drupal\Core\Template\AttributeArray; +use Drupal\Core\Template\AttributeBoolean; use Drupal\Core\Template\AttributeString; use Drupal\Tests\UnitTestCase; @@ -227,4 +228,111 @@ public function testStorage() { $this->assertEquals(array('class' => new AttributeArray('class', array('example-class'))), $attribute->storage()); } + /** + * PHPUnit's data provider callback. + * + * @see $this->testCreateAttributeValue() + * @return array + */ + public function createAttributeValuesProvider() { + $testCases = array(); + + // Test pure strings. + $name = $this->randomMachineName(); + $value = $this->randomMachineName(); + $result = new AttributeString($name, $value); + $testCases[] = array($name, $value, $result); + + // Test AttributeValue instance passed as value. + $value = $this->getMock('Drupal\Core\Template\AttributeValueBase'); + $result = $value; + $testCases[] = array($name, $value, $result); + + // Test array passed as value. + $value = array($this->getRandomGenerator()->string(), $this->getRandomGenerator()->string() => $this->getRandomGenerator()->string()); + $result = new AttributeArray($name, $value); + $testCases[] = array($name, $value, $result); + + // Test random value passed with 'class' name. + $value = $this->getRandomGenerator()->object(); + $name = 'class'; + $result = new AttributeArray($name, (array) $value); + $testCases[] = array($name, $value, $result); + + // Test boolean value. + $value = FALSE; + $name = $this->randomMachineName(); + $result = new AttributeBoolean($name, $value); + $testCases[] = array($name, $value, $result); + + // Lets test TRUE as well because php type casting brings surprises sometimes. + $value = TRUE; + $result = new AttributeBoolean($name, $value); + $testCases[] = array($name, $value, $result); + + // Test NULL as value. + $value = NULL; + $result = new AttributeString($name, $value); + $testCases[] = array($name, $value, $result); + + return $testCases; + } + + /** + * @covers ::createAttributeValue() + * @dataProvider createAttributeValuesProvider + */ + public function testCreateAttributeValue($name, $value, $result) { + $attribute = new Attribute(); + $createAttributeValueMethod = $this->getCreateAttributeValueMethod(); + + $this->assertEquals($result, $createAttributeValueMethod->invokeArgs($attribute, array($name, $value))); + } + + /** + * PHPUnit's data provider callback to test exceptions. + * + * @see $this->testCreateAttributeValueException() + * @return array + */ + public function createAttributeValueExceptionProvider() { + $testCases = array(); + + // Test passed object without 'class' as name. + $name = $this->getRandomGenerator()->string(); + $value = $this->getRandomGenerator()->object(); + $testCases[] = array($name, $value); + + // Test object with render and __toString methods present + // but it is not instance of AttributeValueBase. + $value = $this->getMock('Drupal\Core\StringTranslation\TranslationWrapper'); + $testCases[] = array($name, $value); + + return $testCases; + } + + /** + * @covers ::createAttributeValue() + * @dataProvider createAttributeValueExceptionProvider + * @expectedException \Exception + */ + public function testCreateAttributeValueException($name, $value) { + $attribute = new Attribute(); + $createAttributeValueMethod = $this->getCreateAttributeValueMethod(); + $createAttributeValueMethod->invokeArgs($attribute, array($name, $value)); + } + + /** + * Helper method to get protected createAttributeValue method. + * + * @return \ReflectionMethod + */ + protected function getCreateAttributeValueMethod() { + $attributeClass = new \ReflectionClass('Drupal\Core\Template\Attribute'); + $method = $attributeClass->getMethod('createAttributeValue'); + $method->setAccessible(TRUE); + return $method; + } + } + diff --git a/core/themes/engines/twig/twig.engine b/core/themes/engines/twig/twig.engine index e0cfcc2..c20da07 100644 --- a/core/themes/engines/twig/twig.engine +++ b/core/themes/engines/twig/twig.engine @@ -119,7 +119,7 @@ function twig_render_template($template_file, $variables) { * @see render * @see TwigNodeVisitor */ -function twig_render_var($arg) { +function twig_render_var($arg_name, $arg) { // Check for numeric zero. if ($arg === 0) { return 0; @@ -144,7 +144,7 @@ function twig_render_var($arg) { return (string) $arg; } else { - throw new Exception(t('Object of type "@class" cannot be printed.', array('@class' => get_class($arg)))); + throw new Exception(t('@arg_name of type "@class" doesn\'t implement __toString.', array('@arg_name' => $arg_name, '@class' => get_class($arg)))); } }