diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt index 3d59abe..48c2959 100644 --- a/core/MAINTAINERS.txt +++ b/core/MAINTAINERS.txt @@ -245,7 +245,6 @@ Book module Breakpoint module - Peter Droogmans 'attiks' http://drupal.org/user/105002 -- Marc Drummond 'mdrummond' http://www.drupal.org/user/360968/ CKEditor module - Wim Leers 'Wim Leers' http://drupal.org/user/99777 @@ -373,7 +372,6 @@ REST module Responsive Image module - Peter Droogmans 'attiks' http://drupal.org/user/105002 -- Marc Drummond 'mdrummond' http://www.drupal.org/user/360968/ Search module - Jennifer Hodgdon 'jhodgdon' https://drupal.org/user/155601 diff --git a/core/lib/Drupal/Core/Link.php b/core/lib/Drupal/Core/Link.php new file mode 100644 index 0000000..d08675b --- /dev/null +++ b/core/lib/Drupal/Core/Link.php @@ -0,0 +1,106 @@ +text = $text; + $this->url = $url; + } + + /** + * Creates a link object from a given route name and parameters. + * + * @param string $text + * The text of the link. + * @param string $route_name + * The name of the route + * @param array $route_parameters + * (optional) An associative array of parameter names and values. + * @param array $options + * (optional) An associative array of additional options, with the following + * elements: + * - 'query': An array of query key/value-pairs (without any URL-encoding) + * to append to the URL. Merged with the parameters array. + * - 'fragment': A fragment identifier (named anchor) to append to the URL. + * Do not include the leading '#' character. + * - 'absolute': Defaults to FALSE. Whether to force the output to be an + * absolute link (beginning with http:). Useful for links that will be + * displayed outside the site, such as in an RSS feed. + * - 'language': An optional language object used to look up the alias + * for the URL. If $options['language'] is omitted, it defaults to the + * 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. if mixed mode sessions are permitted, TRUE enforces HTTPS + * and FALSE enforces HTTP. + * + * @return static + */ + public static function createFromRouteArray($text, $route_name, $route_parameters = array(), $options = array()) { + return new static($text, new Url($route_name, $route_parameters, $options)); + } + + /** + * Returns the text of the link. + * + * @return string + */ + public function getText() { + return $this->text; + } + + /** + * Sets the new text of the link. + * + * @param string $text + * The new text. + * + * @return $this + */ + public function setText($text) { + $this->text = $text; + return $this; + } + + /** + * Returns the URL of the link. + * + * @return \Drupal\Core\Url + */ + public function getUrl() { + return $this->url; + } + +} diff --git a/core/lib/Drupal/Core/Template/Attribute.php b/core/lib/Drupal/Core/Template/Attribute.php index 846174d..9911d02 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; /** * A class that can be used for collecting then rendering HTML attributtes. @@ -76,6 +77,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. */ @@ -96,9 +100,21 @@ 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; } diff --git a/core/lib/Drupal/Core/Template/TwigNodeExpressionFunction.php b/core/lib/Drupal/Core/Template/TwigNodeExpressionFunction.php new file mode 100644 index 0000000..0e3f9b4 --- /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..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..55dd4de 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,98 @@ 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 + */ + public function testCreateAttributeValueException() { + $attribute = new Attribute(); + $name = $this->getRandomGenerator()->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 e0cfcc2..bf014bf 100644 --- a/core/themes/engines/twig/twig.engine +++ b/core/themes/engines/twig/twig.engine @@ -110,16 +110,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; @@ -144,7 +147,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), + ))); } }