diff --git a/core/lib/Drupal/Core/Link.php b/core/lib/Drupal/Core/Link.php index 489f54f..d807012 100644 --- a/core/lib/Drupal/Core/Link.php +++ b/core/lib/Drupal/Core/Link.php @@ -7,15 +7,11 @@ namespace Drupal\Core; -use Drupal\Core\Routing\LinkGeneratorTrait; - /** * Defines an object that holds information about a link. */ class Link { - use LinkGeneratorTrait; - /** * The text of the link. * @@ -67,11 +63,12 @@ public function __construct($text, Url $url) { * current language for the language type LanguageInterface::TYPE_URL. * - 'https': Whether this URL should point to a secure location. If not * defined, the current scheme is used, so the user stays on HTTP or HTTPS - * respectively. TRUE enforces HTTPS and FALSE enforces HTTP. + * respectively. if mixed mode sessions are permitted, TRUE enforces HTTPS + * and FALSE enforces HTTP. * * @return static */ - public static function createFromRoute($text, $route_name, $route_parameters = array(), $options = array()) { + public static function createFromRouteArray($text, $route_name, $route_parameters = array(), $options = array()) { return new static($text, new Url($route_name, $route_parameters, $options)); } @@ -92,7 +89,7 @@ public function getText() { * * @return $this */ - public function setText($text) { + public function setText($text) { $this->text = $text; return $this; } @@ -105,25 +102,4 @@ public function setText($text) { public function getUrl() { return $this->url; } - - /** - * Sets the URL of this link. - * - * @param Url $url - * The URL object to set - * - * @return $this - */ - public function setUrl(Url $url) { - $this->url = $url; - return $this; - } - - /** - * Generates the HTML for this Link object. - */ - public function toString() { - return $this->getLinkGenerator()->generateFromLink($this); - } - -} +} diff --git a/core/lib/Drupal/Core/Template/Attribute.php b/core/lib/Drupal/Core/Template/Attribute.php index 5333acb..ffb2018 100644 --- a/core/lib/Drupal/Core/Template/Attribute.php +++ b/core/lib/Drupal/Core/Template/Attribute.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Template; use Drupal\Component\Utility\SafeMarkup; +use Drupal\Component\Utility\String; /** * Collects, sanitizes, and renders HTML attributes. @@ -87,6 +88,9 @@ public function offsetSet($name, $value) { * @param mixed $value * The attribute value. * + * @throws \UnexpectedValueException + * If invalid object was passed as a value. + * * @return \Drupal\Core\Template\AttributeValueBase * An AttributeValueBase representation of the attribute's value. */ @@ -107,12 +111,24 @@ protected function createAttributeValue($name, $value) { elseif (is_bool($value)) { $value = new AttributeBoolean($name, $value); } - elseif (!is_object($value)) { + // If $value is int, string, null or other pure type lets pass it directly. + // If $value is object but implements __toString() lets pass it + // and it will be typecasted to string later. + elseif (!is_object($value) || (is_object($value) && method_exists($value, '__toString'))) { $value = new AttributeString($name, $value); } + // Provide a better exception message when invalid object is passed as value. + elseif (is_object($value)) { + throw new \UnexpectedValueException(String::format('"@name" attribute value of type "@type" can not be converted to string', + array( + '@name' => $name, + '@type' => get_class($value), + ))); + } + return $value; } - + /** * Implements ArrayAccess::offsetUnset(). */ diff --git a/core/lib/Drupal/Core/Template/TwigNodeExpressionFunction.php b/core/lib/Drupal/Core/Template/TwigNodeExpressionFunction.php new file mode 100644 index 0000000..6eda886 --- /dev/null +++ b/core/lib/Drupal/Core/Template/TwigNodeExpressionFunction.php @@ -0,0 +1,56 @@ +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..df756c1 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'))), $line, array($name)), $line ); } diff --git a/core/tests/Drupal/Tests/Core/Template/AttributeTest.php b/core/tests/Drupal/Tests/Core/Template/AttributeTest.php index d587db2..a1a92c8 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; @@ -247,6 +248,7 @@ public function testChainAddRemoveClasses() { /** * Tests the twig calls to the Attribute. + * * @dataProvider providerTestAttributeClassHelpers * * @covers ::removeClass @@ -348,5 +350,100 @@ public function testStorage() { $this->assertEquals(array('class' => new AttributeArray('class', array('example-class'))), $attribute->storage()); } + /** + * Provides test data sets for testCreateAttributeValue(). + * + * @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', array(), array(), '', FALSE); + $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); + + + // Test object with render and __toString methods present + // but it is not instance of AttributeValueBase. + $value = $this->getMock('Drupal\Core\StringTranslation\TranslationWrapper', array(), array(), '', FALSE); + $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))); + } + + /** + * @covers ::createAttributeValue() + * + * @expectedException \Exception + * @expectedExceptionMessage "Random string !@#$" attribute value of type "stdClass" can not be converted to string + */ + public function testCreateAttributeValueException() { + $attribute = new Attribute(); + $name = 'Random string !@#$'; + $value = $this->getRandomGenerator()->object(); + $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 5ef2f10..c5d8b61 100644 --- a/core/themes/engines/twig/twig.engine +++ b/core/themes/engines/twig/twig.engine @@ -117,16 +117,19 @@ function twig_render_template($template_file, $variables) { * If an array is passed it is rendered via render() and scalar values are * returned directly. * + * @param string $arg_name + * Name of object that will be rendered. * @param mixed $arg * String, Object or Render Array * - * @return - * The rendered output or an Twig_Markup object. + * @return string The rendered output or an Twig_Markup object. * - * @see render + * @see render() * @see TwigNodeVisitor + * + * @throws \UnexpectedValueException */ -function twig_render_var($arg) { +function twig_render_var($arg_name, $arg) { // Check for numeric zero. if ($arg === 0) { return 0; @@ -151,7 +154,11 @@ 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 \UnexpectedValueException(String::format('@arg_name of type "@class" doesn\'t implement __toString.', + array( + '@arg_name' => $arg_name, + '@class' => get_class($arg), + ))); } }