Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/Component.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ public function getDefinitionFilePath($pathname): ?string {
else {
$filepath = NULL;
}

return $filepath;
}

Expand Down Expand Up @@ -232,6 +233,12 @@ protected function getVariantDefaultVariables(string $modifier, array $defined_v
*/
protected function extractParts(string $compound_name): array {
$loader = $this->env->getLoader();
// Automatically expand a component name to its template filename; e.g.:
// "@card" => "@card/card.twig"
if (strpos($compound_name, '@') !== FALSE && stripos($compound_name, '.twig') === FALSE) {
$parts = array_filter(explode('/', $compound_name));
$compound_name .= '/' . ltrim(end($parts), '@') . '.twig';
}
$pathname = preg_replace('@--[^.]+@', '', $compound_name);
$template_pathname = $loader->exists($compound_name) ? $compound_name : $pathname;

Expand Down
2 changes: 1 addition & 1 deletion src/NodeVisitors/Attributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public function doLeaveNode(Node $node, Environment $env): Node {
/**
* Checks if the passed node is an attributes node.
*
* @return \Node|null
* @return \Twig\Node\Node|null
*/
protected function isAttributes($node): ?Node {
if ($node->hasAttribute('name') && strpos((string) $node->getAttribute('name'), 'attributes') !== FALSE) {
Expand Down
8 changes: 4 additions & 4 deletions src/TokenParser/Render.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
* {% render '@foo/baz--bar.twig' with { qux: 'foobar' } %}
* @endcode
*
* @see Twig\TokenParser\Include
* @see Twig_TokenParser_Include
* @see http://fractal.build/guide/components/variants
*/
class Render extends AbstractTokenParser {
Expand All @@ -39,12 +39,12 @@ class Render extends AbstractTokenParser {
* Parses a Twig token and returns a new Render node.
*
* @param \Twig\Token $token
* The Twig\Token to parse.
* The Twig_Token to parse.
*
* @return \Drupal\twig_fractal\Node\Render
* The Render node.
*
* @see TokenParser_Include::parse()
* @see Twig_TokenParser_Include::parse()
*/
public function parse(Token $token): Node\Render {
$expr = $this->parser->getExpressionParser()->parseExpression();
Expand All @@ -58,7 +58,7 @@ public function parse(Token $token): Node\Render {
* @return array
* The extracted arguments.
*
* @see TokenParser_Include::parseArguments()
* @see Twig_TokenParser_Include::parseArguments()
*/
protected function parseArguments(): array {
$stream = $this->parser->getStream();
Expand Down
218 changes: 181 additions & 37 deletions src/TwigFractal.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,194 @@

/**
* @file
* Contains \Drupal\twig_fractal\TwigFractal.
* Contains \Drupal\twig_fractal\Node\Render.
*/

namespace Drupal\twig_fractal;
namespace Drupal\twig_fractal\Node;

use Drupal\twig_fractal\TokenParser\Render;
use Drupal\twig_fractal\NodeVisitors\Attributes;
use Twig\Extension\AbstractExtension;
use Drupal\Core\Template\Attribute;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\IncludeNode;
use Twig\Compiler;
use Twig_Node_Expression;
use Twig_Node_Include;

/**
* Registers a new `render` token parser to make Fractal components reusable in Drupal.
* Compiles `render` nodes.
*
* @see \Drupal\twig_fractal\TokenParser\Render
* Unlike `include` nodes, the default values for template variables are
* automatically retrieved from the corresponding component definition
* file in the Fractal component library and added to the compiled Twig PHP
* template string.
*
* @implements Twig\Extension\ExtensionInterface
* The resulting template variables consist of:
*
* 1. the default `context` properties in the component's definition file,
* which may be overridden by
* 2. the `context` properties of the requested variants (if any) in the
* component's definition file, which may be overridden by
* 3. the variables passed to the `render` tag itself.
*/
class TwigFractal extends AbstractExtension {

/**
* Adds a new Render token parser instance to the list of parsers.
*
* @return array
* The token parsers.
*/
public function getTokenParsers(): array {
return [
new Render(),
];
}

public function getNodeVisitors() {
return [
new Attributes(),
];
}

/**
* Returns the extension name.
*
* @return string
* The extension name.
*/
public function getName(): string {
return 'twig_fractal';
}
class Render extends IncludeNode {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please note you duplicated the methods (not sure if you also made adjustments) from the actual render class https://github.com/ipwa/twig_fractal/blob/master/src/Node/Render.php#L60

could you please double check, whether your adjustments in this file are correct? Also the class and filename do not align anymore.


/**
* Pre-render callback.
*
* @var Callable
*/
private static $preRenderCallback;

/**
* Constructs a Twig template to render.
*/
public function __construct(AbstractExpression $expr, ?AbstractExpression $variables, $only, $ignoreMissing, $lineno = NULL, $tag = null) {
parent::__construct($expr, $variables, $only, $ignoreMissing, $lineno);
}

/**
* Adds the template variables to the compiled Twig PHP template string.
*
* Variables consist of
*
* 1. the default `context` properties in the component's definition file,
* which may be overridden by
* 2. the `context` properties of the requested variants (if any) in the
* component's definition file, which may be overridden by
* 3. the variables passed to the `render` tag itself.
*
* @param \Twig\Compiler $compiler
*/
protected function addGetTemplate(Compiler $compiler) {
$compiler->write('$handles = (array) ')->subcompile($this->getNode('expr'))->raw(";")->raw("\n");
$compiler->write('$templates = [];')->raw("\n");
$compiler->write('foreach ($handles as $handle):')->raw("\n")
->indent()
->write('$passed_variables = $defaults = [];')->raw("\n")
->write('$component = new \Drupal\twig_fractal\Component(')
->raw('$this->env, ')
->raw('$handle')
->raw(');')->raw("\n")
->write('$templates[] = $component->getTemplatePathname();')->raw("\n")
// Exit loop when component is found to not look further.
->write('if ($component->getDefinitionFilePath($component->getPathname())):')->raw("\n")
->indent()
->write('$defaults = $component->getDefaultVariables();')->raw("\n")
->write('break;')->raw("\n")
->outdent()
->write('endif;')->raw("\n")
->outdent()
->write('endforeach;')->raw("\n");

if (!$this->hasNode('variables')) {
$compiler->write('$variables = $defaults;')->raw("\n");
}
else {
$compiler->write('$passed_variables = ')->subcompile($this->getNode('variables'))->raw(";\n");
$compiler->write('$variables = array_merge($defaults, $passed_variables)')->raw(";\n");
}
$compiler->write('$variables = \Drupal\twig_fractal\Node\Render::convertAttributes($variables, $defaults, $passed_variables);')->raw("\n");

$compiler->write('\Drupal\twig_fractal\Node\Render::doPreRenderCallback($component->getPathname(), $component->getName());')->raw("\n");
$compiler
->write('$this->loadTemplate(')
->raw('$templates')
->raw(', ')
->repr($this->getTemplateName())
->raw(', ')
->repr($this->getTemplateLine())
->raw(')')
;
}

/**
* Recursively converts attributes variables into Attribute objects in context variables.
*
* @param array $variables
* The pre-merged component variables.
* @param array $defaults
* The default variables defined by the component.
* @param array $passed_variables
* The custom variables passed to the component as render tag arguments.
*
* @return array
* The $variables with recursively converted attributes variables.
*/
public static function convertAttributes(array $variables, array $defaults, array $passed_variables) {
foreach ($variables as $name => $value) {
if (FALSE === strpos($name, 'attributes')) {
// Non-attributes variables do not need further processing as they have
// been merged recursively already, but they can contain attributes
// variables in nested keys.
if (is_array($value)) {
// The component may have defined an empty string as a placeholder for
// more complex dynamic content.
if (!isset($defaults[$name]) || !is_array($defaults[$name])) {
$defaults[$name] = [];
}
$variables[$name] = static::convertAttributes($value, $defaults[$name], $passed_variables[$name] ?? []);
}
continue;
}
// Remove variables that are not defined by the component.
// @todo This accidentally removes nested keys (including attributes).
//unset($variables[$name]);
if (!isset($defaults[$name])) {
continue;
}
if (!isset($passed_variables[$name])) {
$variables[$name] = new Attribute($defaults[$name]);
}
else {
$variables[$name] = $passed_variables[$name];
if (!$variables[$name] instanceof Attribute) {
$variables[$name] = new Attribute($variables[$name]);
}
foreach ($defaults[$name] as $default_key => $default_value) {
if (!isset($variables[$name][$default_key])) {
$variables[$name]->setAttribute($default_key, $default_value);
}
}
}
if (FALSE !== strpos($name, 'attributes') && isset($defaults[$name]['class'])) {
$variables[$name]->addClass($defaults[$name]['class']);
}
}
return $variables;
}

/**
* Passes the precompiled template variables to the Twig PHP template display method.
*
* @param \Twig\Compiler $compiler
*/
protected function addTemplateArguments(Compiler $compiler) {
$compiler->raw('$variables');
}

/**
* Sets the pre-render callback function.
*
* @param callable $callback
* The callable must accept 2 arguments.
*/
public static function setPreRenderCallback(callable $callback): void {
static::$preRenderCallback = $callback;
}

/**
* Execute the pre-render callback function if present.
*
* @param string $path
* Component path name, including alias.
* @param string $name
* Component name.
*/
public static function doPreRenderCallback(string $path, string $name): void {
$callback = static::$preRenderCallback ?? NULL;
if (!$callback) {
return;
}
$callback($path, $name);
}

}
12 changes: 7 additions & 5 deletions src/TwigLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
namespace Drupal\twig_fractal;

use Drupal;
use Drupal\components\Template\Loader\ComponentLibraryLoader;
use Drupal\components\Template\ComponentsRegistry;
use Drupal\components\Template\Loader\ComponentsLoader;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Theme\ThemeManagerInterface;

/**
* Allows a subtheme to override Fractal components of its base theme.
Expand All @@ -21,13 +23,13 @@
*
* @see http://symfony.com/doc/current/templating/namespaced_paths.html#multiple-paths-per-namespace
*/
class TwigLoader extends ComponentLibraryLoader {
class TwigLoader extends ComponentsLoader {

/**
* {@inheritdoc}
*/
public function __construct($paths = [], ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) {
parent::__construct($paths, $module_handler, $theme_handler);
public function __construct(ThemeHandlerInterface $theme_handler,ComponentsRegistry $components_registry,ThemeManagerInterface $theme_manager) {
parent::__construct($components_registry);

$default_theme = $theme_handler->getTheme($theme_handler->getDefault());
if ($base_theme = $default_theme->base_theme) {
Expand All @@ -49,7 +51,7 @@ public function __construct($paths = [], ModuleHandlerInterface $module_handler,
/**
* {@inheritdoc}
*/
public function prependPath($path, $namespace = self::MAIN_NAMESPACE) {
public function prependPath($path, $namespace = self::MAIN_NAMESPACE): void {
$this->cache = $this->errorCache = [];

$path = rtrim($path, '/\\');
Expand Down
3 changes: 2 additions & 1 deletion twig_fractal.info.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ name: Twig Fractal Integration
description: Reuse Fractal components and variants in Drupal without duplicating their parameters and variables.
type: module
core: 8.x
php: 7.x
core_version_requirement: ^8 || ^9 || ^10
php: 8.x
dependencies:
- components
12 changes: 6 additions & 6 deletions twig_fractal.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ services:
twig.extension.twig_fractal:
class: Drupal\twig_fractal\TwigFractal
tags:
- { name: twig.extension }
- { name: twig.extension , priority: 220 }
twig.loader.twig_fractal:
class: Drupal\twig_fractal\TwigLoader
arguments: ['@app.root', '@module_handler', '@theme_handler']
tags:
# twig.loader.componentlibrary is registered with 200
- { name: twig.loader, priority: 210 }
class: Drupal\twig_fractal\TwigLoader
arguments: [ '@theme_handler','@components.registry', '@theme.manager' ]
tags:
# twig.loader.componentlibrary is registered with 200
- { name: twig.loader, priority: 210 }