Browse Source

Add view builders and clean-up

merge-requests/2/head 3.0.0-beta1
Chi 5 years ago
parent
commit
3f80912345
  1. 294
      README.md
  2. 9
      README.txt
  3. 3
      composer.json
  4. 16
      phpcs.xml
  5. 1201
      src/TwigExtension.php
  6. 591
      src/TwigTweakExtension.php
  7. 155
      src/View/BlockViewBuilder.php
  8. 60
      src/View/EntityFormViewBuilder.php
  9. 47
      src/View/EntityViewBuilder.php
  10. 74
      src/View/FieldViewBuilder.php
  11. 61
      src/View/ImageViewBuilder.php
  12. 70
      src/View/MenuViewBuilder.php
  13. 111
      src/View/RegionViewBuilder.php
  14. 273
      tests/src/Functional/TwigTweakTest.php
  15. 598
      tests/src/Kernel/AccessTest.php
  16. 119
      tests/src/Kernel/BlockViewBuilderTest.php
  17. 128
      tests/src/Kernel/EntityFormViewBuilderTest.php
  18. 203
      tests/src/Kernel/EntityViewBuilderTest.php
  19. 141
      tests/src/Kernel/FieldViewBuilderTest.php
  20. 163
      tests/src/Kernel/ImageViewBuilderTest.php
  21. 124
      tests/src/Kernel/MenuViewBuilderTest.php
  22. 145
      tests/src/Kernel/RegionViewBuilderTest.php
  23. 19
      tests/twig_tweak_test/config/install/block.block.classy_content.yml
  24. 80
      tests/twig_tweak_test/config/install/core.entity_form_display.node.page.default.yml
  25. 8
      tests/twig_tweak_test/config/install/core.entity_view_display.media.image.default.yml
  26. 3
      tests/twig_tweak_test/config/install/core.entity_view_display.media.remote_video.default.yml
  27. 25
      tests/twig_tweak_test/config/install/core.entity_view_display.node.page.default.yml
  28. 31
      tests/twig_tweak_test/config/install/core.entity_view_display.node.page.teaser.yml
  29. 3
      tests/twig_tweak_test/config/install/field.field.media.image.field_media_image.yml
  30. 3
      tests/twig_tweak_test/config/install/field.field.media.remote_video.field_media_oembed_video.yml
  31. 4
      tests/twig_tweak_test/config/install/field.field.node.page.body.yml
  32. 1
      tests/twig_tweak_test/config/install/field.field.node.page.field_image.yml
  33. 1
      tests/twig_tweak_test/config/install/field.field.node.page.field_media.yml
  34. 3
      tests/twig_tweak_test/config/install/field.storage.media.field_media_image.yml
  35. 3
      tests/twig_tweak_test/config/install/field.storage.media.field_media_oembed_video.yml
  36. 3
      tests/twig_tweak_test/config/install/field.storage.node.field_image.yml
  37. 1
      tests/twig_tweak_test/config/install/field.storage.node.field_media.yml
  38. 2
      tests/twig_tweak_test/config/install/filter.format.twig_tweak_test.yml
  39. 3
      tests/twig_tweak_test/config/install/media.type.image.yml
  40. 5
      tests/twig_tweak_test/config/install/media.type.remote_video.yml
  41. 1
      tests/twig_tweak_test/config/install/node.type.page.yml
  42. 2
      tests/twig_tweak_test/config/install/system.menu.twig-tweak-test.yml
  43. 3
      tests/twig_tweak_test/config/install/views.view.twig_tweak_test.yml
  44. 17
      tests/twig_tweak_test/src/Controller/TwigTweakTestController.php
  45. 15
      tests/twig_tweak_test/src/Plugin/Block/FooBlock.php
  46. 12
      tests/twig_tweak_test/templates/twig-tweak-test.html.twig
  47. 9
      tests/twig_tweak_test/twig_tweak_test.info.yml
  48. 48
      tests/twig_tweak_test/twig_tweak_test.module
  49. 7
      tests/twig_tweak_test/twig_tweak_test.routing.yml
  50. 5
      twig_tweak.info.yml
  51. 29
      twig_tweak.services.yml

294
README.md

@ -0,0 +1,294 @@
## SUMMARY
Twig Tweak module provides a Twig extension with some useful functions and
filters.
## Usage
### Drupal View
```twig
{{ drupal_view('who_s_new', 'block_1') }}
```
### Drupal View Result
```twig
{{ drupal_view('who_s_new', 'block_1') }}
```
### Drupal Block
In order to list all registered plugin IDs fetch them with block plugin manager.
With Drush it can be done like follows:
```
drush ev "print_r(array_keys(\Drupal::service('plugin.manager.block')->getDefinitions()));"
```
```twig
{# Print block using default configuration. #}
{{ drupal_block('system_branding_block') }}
{# Print block using custom configuration. #}
{{ drupal_block('system_branding_block', {label: 'Branding', use_site_name: false})
{# Bypass block.html.twig theming. #}
{{ drupal_block('system_branding_block', wrapper=false) }}
```
@see https://www.drupal.org/node/2964457#block-plugin
### Drupal Region
```twig
{# Print 'Sidebar First' region of the default site theme. #}
{{ drupal_region('sidebar_first') }}
{# Print 'Sidebar First' region of Bartik theme. #}
{{ drupal_region('sidebar_first', 'bartik') }}
```
### Drupal Entity
```twig
{# Print a content block which ID is 1. #}
{{ drupal_entity('block_content', 1) }}
{# Print a node's teaser. #}
{{ drupal_entity('node', 123, 'teaser') }}
{# Print Branding block which was previously disabled on #}
{# admin/structure/block page. #}
{{ drupal_entity('block', 'bartik_branding', check_access=false) }}
```
### Drupal Entity Form
```twig
{# Print edit form for node 1. #}
{{ drupal_entity_form('node', 1) }}
{# Print add form for Article content type. #}
{{ drupal_entity_form('node', values={type: 'article'}) }}
{# Print user register form. #}
{{ drupal_entity_form('user', NULL, 'register', check_access=false) }}
```
### Drupal Field
```twig
{{ drupal_field('field_image', 'node', 1) }}
{{ drupal_field('field_image', 'node', 1, 'teaser') }}
{{ drupal_field('field_image', 'node', 1, {type: 'image_url', settings: {image_style: 'large'}}) }}
```
### Drupal Menu
```twig
{{ drupal_menu('main') }}
```
### Drupal Form
```twig
{{ drupal_form('Drupal\\search\\Form\\SearchBlockForm') }}
```
### Drupal Image
```twig
{# Render image specified by file ID. #}
{{ drupal_image(123) }}
{# Render image specified by file UUID. #}
{{ drupal_image('9bb27144-e6b2-4847-bd24-adcc59613ec0') }}
{# Render image specified by file URI. #}
{{ drupal_image('public://ocean.jpg') }}
{# Render image using 'thumbnail' image style and custom attributes. #}
{{ drupal_image('public://ocean.jpg', 'thumbnail', {alt: 'The alternative text'|t, title: 'The title text'|t}) }}
{# Render responsive image. #}
{{ drupal_image('public://ocean.jpg', 'wide', responsive=true) }}
```
### Drupal Token
```twig
{{ drupal_token('site:name') }}
```
### Drupal Config
```twig
{{ drupal_config('system.site', 'name') }}
```
### Drupal Dump
```twig
{# Basic usage. #}
{{ drupal_dump(var) }}
{# Same as above but shorter. #}
{{ dd(var) }}
{# Dump all available variables for the current template. #}
{{ dd() }}
```
### Drupal Title
```twig
{{ drupal_title() }}
```
### Drupal URL
```twig
{# Basic usage. #}
{{ drupal_url('node/1') }}
{# Complex URL. #}
{{ drupal_url('node/1', {query: {foo: 'bar'}, fragment: 'example', absolute: true}) }}
```
### Drupal Link
```twig
{# It supports the same options as drupal_url(), plus attributes. #}
{{ drupal_link('View'|t, 'node/1', {attributes: {target: '_blank'}}) }}
{# This link will only be shown for privileged users. #}
{{ drupal_link('Example'|t, '/admin', check_access=true) }}
```
### Drupal Messages
```twig
{{ drupal_messages() }}
```
### Drupal Breadcrumb
```twig
{{ drupal_breadcrumb() }}
```
### Drupal Breakpoint
```twig
{{ drupal_breakpoint() }}
```
### Contextual Links
```twig
{# Basic usage. #}
<div class="contextual-region">
{{ contextual_links('entity.view.edit_form:view=frontpage&display_id=feed_1') }}
{{ drupal_view('frontpage') }}
</div>
{# Multiple links. #}
<div class="contextual-region">
{{ contextual_links('node:node=123|block_content:block_content=123') }}
{{ content }}
</div>
```
### Token Replace
```twig
{# Basic usage. #}
{{ '<h1>[site:name]</h1><div>[site:slogan]</div>'|token_replace }}
{# This is more suited to large markup. #}
{% apply token_replace %}
<h1>[site:name]</h1>
<div>[site:slogan]</div>
{% endapply %}
```
### Preg Replace
```twig
{{ 'Drupal - community plumbing!'|preg_replace('/(Drupal)/', '<b>$1</b>') }}
```
For simple string interpolation consider using built-in `replace` or `format`
Twig filters.
### Image Style
```twig
{{ 'public://images/ocean.jpg'|image_style('thumbnail') }}
```
### Transliterate
```twig
{{ 'Привет!'|transliterate }}
```
new TwigFilter('transliterate', [self::class, 'transliterateFilter']),
### Check Markup
```twig
{{ '<b>bold</b> <strong>strong</strong>'|check_markup('restricted_html') }}
```
### Truncate
```twig
{{ 'Some long text'|truncate(10, true) }}
```
### View
```twig
{# Do not put this into node.html.twig template to avoid recursion. #}
{{ node|view }}
{{ node|view('teaser') }}
{{ node.field_image|view }}
{{ node.field_image[0]|view }}
{{ node.field_image|view('teaser') }}
{{ node.field_image|view({settings: {image_style: 'thumbnail'}}) }}
```
### With
```twig
{# Set top level value. #}
{{ content.field_image|with('#title', 'Photo'|t) }}
{# Set nested value. #}
{{ content|with(['field_image', '#title'], 'Photo'|t) }}
```
### Children
```twig
<ul>
{% for tag in content.field_tags|children %}
<li>{{ tag }}</li>
{% endfor %}
</ul>
```
### File URL
For string arguments it works similar to core `file_url()` Twig function.
```twig
{{ 'public://sea.jpg'|file_url }}
```
When field item list passed the URL will be extracted from the first item. In
order to get URL of specific item specify its delta explicitly using array
notation.
```twig
{{ node.field_image|file_url }}
{{ node.field_image[0]|file_url }}
```
Media fields are fully supported including OEmbed resources.
```twig
{{ node.field_media|file_url }}
```
### PHP
PHP filter is disabled by default. You can enable it in settings.php file as
follows:
```twug
$settings['twig_tweak_enable_php_filter'] = TRUE;
```
```twig
{{ 'return date('Y');'|php }}
```
Using PHP filter is discouraged as it may cause security implications. In fact
it is very rarely needed. The above code can be replaced with following.
```twig
{{ 'now'|date('Y') }}
```
## LINKS
Project page: https://www.drupal.org/project/twig_tweak
Twig home page: http://twig.sensiolabs.org
Drupal 8 Twig documentation: https://www.drupal.org/docs/8/theming/twig

9
README.txt

@ -1,9 +0,0 @@
-- SUMMARY --
Twig Tweak module provides a Twig extension with some useful functions and
filters. See src/TwigExtension.php for details.
-- LINKS --
Project page: https://www.drupal.org/project/twig_tweak
Twig home page: http://twig.sensiolabs.org
Drupal 8 Twig documentation: https://www.drupal.org/docs/8/theming/twig

3
composer.json

@ -10,7 +10,8 @@
"source": "https://git.drupalcode.org/project/twig_tweak" "source": "https://git.drupalcode.org/project/twig_tweak"
}, },
"require": { "require": {
"drupal/core": "^8.7 || ^9.0" "php": ">=7.3",
"drupal/core": "^9.0"
}, },
"suggest": { "suggest": {
"symfony/var-dumper": "Better dump() function for debugging Twig variables" "symfony/var-dumper": "Better dump() function for debugging Twig variables"

16
phpcs.xml

@ -0,0 +1,16 @@
<?xml version="1.0"?>
<ruleset name="Twig Tweak">
<description>PHP CodeSniffer configuration for Twig Tweak module.</description>
<arg name="extensions" value="php, module, yml"/>
<rule ref="Drupal"/>
<rule ref="DrupalPractice">
<!-- Dependencies are not injected for performance reason. -->
<exclude name="DrupalPractice.Objects.GlobalDrupal.GlobalDrupal"/>
<!-- False positives. -->
<exclude name="Drupal.Commenting.InlineComment.Empty"/>
<!-- The module does not provide change records. -->
<exclude name="Drupal.Semantics.FunctionTriggerError.TriggerErrorTextLayoutRelaxed"/>
<!-- Code examples have rather long lines. -->
<exclude name="Drupal.Files.LineLength.TooLong"/>
</rule>
</ruleset>

1201
src/TwigExtension.php

File diff suppressed because it is too large Load Diff

591
src/TwigTweakExtension.php

@ -0,0 +1,591 @@
<?php
namespace Drupal\twig_tweak;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Uuid\Uuid;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Link;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Markup;
use Drupal\Core\Site\Settings;
use Drupal\Core\Url;
use Drupal\file\Entity\File;
use Drupal\file\FileInterface;
use Drupal\image\Entity\ImageStyle;
use Drupal\media\MediaInterface;
use Drupal\media\Plugin\media\Source\OEmbedInterface;
use Twig\Environment;
use Twig\Extension\AbstractExtension;
use Twig\Markup as TwigMarkup;
use Twig\TwigFilter;
use Twig\TwigFunction;
/**
* Twig extension with some useful functions and filters.
*
* The extension consumes quite a lot of dependencies. Most of them are not used
* on each page request. For performance reasons services are wrapped in static
* callbacks.
*/
class TwigTweakExtension extends AbstractExtension {
/**
* {@inheritdoc}
*/
public function getFunctions(): array {
$context_options = ['needs_context' => TRUE];
$all_options = ['needs_environment' => TRUE, 'needs_context' => TRUE];
return [
new TwigFunction('drupal_view', 'views_embed_view'),
new TwigFunction('drupal_view_result', 'views_get_view_result'),
new TwigFunction('drupal_block', [self::class, 'drupalBlock']),
new TwigFunction('drupal_region', [self::class, 'drupalRegion']),
new TwigFunction('drupal_entity', [self::class, 'drupalEntity']),
new TwigFunction('drupal_entity_form', [self::class, 'drupalEntityForm']),
new TwigFunction('drupal_field', [self::class, 'drupalField']),
new TwigFunction('drupal_menu', [self::class, 'drupalMenu']),
new TwigFunction('drupal_form', [self::class, 'drupalForm']),
new TwigFunction('drupal_image', [self::class, 'drupalImage']),
new TwigFunction('drupal_token', [self::class, 'drupalToken']),
new TwigFunction('drupal_config', [self::class, 'drupalConfig']),
new TwigFunction('drupal_dump', [self::class, 'drupalDump'], $context_options),
new TwigFunction('dd', [self::class, 'drupalDump'], $context_options),
new TwigFunction('drupal_title', [self::class, 'drupalTitle']),
new TwigFunction('drupal_url', [self::class, 'drupalUrl']),
new TwigFunction('drupal_link', [self::class, 'drupalLink']),
new TwigFunction('drupal_messages', function (): array {
return ['#type' => 'status_messages'];
}),
new TwigFunction('drupal_breadcrumb', [self::class, 'drupalBreadcrumb']),
new TwigFunction('drupal_breakpoint', [self::class, 'drupalBreakpoint'], $all_options),
new TwigFunction('drupal_contextual_links', [self::class, 'drupalContextualLinks']),
];
}
/**
* {@inheritdoc}
*/
public function getFilters(): array {
$filters = [
new TwigFilter('token_replace', [self::class, 'tokenReplaceFilter']),
new TwigFilter('preg_replace', [self::class, 'pregReplaceFilter']),
new TwigFilter('image_style', [self::class, 'imageStyleFilter']),
new TwigFilter('transliterate', [self::class, 'transliterateFilter']),
new TwigFilter('check_markup', 'check_markup'),
new TwigFilter('truncate', [Unicode::class, 'truncate']),
new TwigFilter('view', [self::class, 'viewFilter']),
new TwigFilter('with', [self::class, 'withFilter']),
new TwigFilter('children', [self::class, 'childrenFilter']),
new TwigFilter('file_url', [self::class, 'fileUrlFilter']),
];
if (Settings::get('twig_tweak_enable_php_filter')) {
$filters[] = new TwigFilter('php', [self::class, 'phpFilter']);
}
return $filters;
}
/**
* Builds the render array for a block.
*/
public static function drupalBlock(string $id, array $configuration = [], bool $wrapper = TRUE): array {
return \Drupal::service('twig_tweak.block_view_builder')->build($id, $configuration, $wrapper);
}
/**
* Builds the render array of a given region.
*/
public static function drupalRegion(string $region, string $theme = NULL): array {
return \Drupal::service('twig_tweak.region_view_builder')->build($region, $theme);
}
/**
* Returns the render array to represent an entity.
*/
public static function drupalEntity(string $entity_type, ?string $id, string $view_mode = 'full', string $langcode = NULL, bool $check_access = TRUE): array {
$entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load((string) $id);
if ($entity) {
return \Drupal::service('twig_tweak.entity_view_builder')
->build($entity, $view_mode, $langcode, $check_access);
}
return [];
}
/**
* Gets the built and processed entity form for the given entity type.
*/
public static function drupalEntityForm(string $entity_type, string $id = NULL, string $form_mode = 'default', array $values = [], bool $check_access = TRUE): array {
$entity_storage = \Drupal::entityTypeManager()->getStorage($entity_type);
$entity = $id ? $entity_storage->load($id) : $entity_storage->create($values);
if ($entity) {
return \Drupal::service('twig_tweak.entity_form_view_builder')
->build($entity, $form_mode, $check_access);
}
return [];
}
/**
* Returns the render array for a single entity field.
*/
public static function drupalField(string $field_name, ?string $entity_type, ?string $id, string $view_mode = 'full', string $langcode = NULL, bool $check_access = TRUE): array {
$entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load((string) $id);
if ($entity) {
return \Drupal::service('twig_tweak.field_view_builder')
->build($entity, $field_name, $view_mode, $langcode, $check_access);
}
return [];
}
/**
* Returns the render array for Drupal menu.
*/
public static function drupalMenu(string $menu_name, int $level = 1, int $depth = 0, bool $expand = FALSE): array {
return \Drupal::service('twig_tweak.menu_view_builder')->build($menu_name, $level, $depth, $expand);
}
/**
* Builds and processes a form for a given form ID.
*
* @param string $form_id
* The form ID.
* @param mixed $args
* Additional arguments are passed to form constructor.
*
* @return array
* A render array to represent the form.
*/
public static function drupalForm(string $form_id, ...$args): array {
$callback = [\Drupal::formBuilder(), 'getForm'];
return call_user_func_array($callback, func_get_args());
}
/**
* Builds an image.
*/
public static function drupalImage(string $property, string $style = NULL, array $attributes = [], bool $responsive = FALSE, bool $check_access = TRUE): array {
// Determine property type by its value.
if (preg_match('/^\d+$/', $property)) {
$property_type = 'fid';
}
elseif (Uuid::isValid($property)) {
$property_type = 'uuid';
}
else {
$property_type = 'uri';
}
$files = \Drupal::entityTypeManager()
->getStorage('file')
->loadByProperties([$property_type => $property]);
// To avoid ambiguity render nothing unless exact one image has been found.
if (count($files) != 1) {
return [];
}
$file = reset($files);
return \Drupal::service('twig_tweak.image_view_builder')->build($file, $style, $attributes, $responsive, $check_access);
}
/**
* Replaces a given token with appropriate value.
*
* @param string $token
* A replaceable token.
* @param array $data
* (optional) An array of keyed objects. For simple replacement scenarios
* 'node', 'user', and others are common keys, with an accompanying node or
* user object being the value. Some token types, like 'site', do not
* require any explicit information from $data and can be replaced even if
* it is empty.
* @param array $options
* (optional) A keyed array of settings and flags to control the token
* replacement process.
*
* @return string
* The token value.
*
* @see \Drupal\Core\Utility\Token::replace()
*/
public static function drupalToken(string $token, array $data = [], array $options = []): string {
return \Drupal::token()->replace("[$token]", $data, $options);
}
/**
* Retrieves data from a given configuration object.
*
* @param string $name
* The name of the configuration object to construct.
* @param string $key
* A string that maps to a key within the configuration data.
*
* @return mixed
* The data that was requested.
*/
public static function drupalConfig(string $name, string $key) {
return \Drupal::config($name)->get($key);
}
/**
* Dumps information about variables.
*
* @param array $context
* Variables from the Twig template.
* @param mixed $variable
* (optional) The variable to dump.
*/
public static function drupalDump(array $context, $variable = NULL): void {
$var_dumper = '\Symfony\Component\VarDumper\VarDumper';
if (class_exists($var_dumper)) {
call_user_func($var_dumper . '::dump', func_num_args() == 1 ? $context : $variable);
}
else {
trigger_error('Could not dump the variable because symfony/var-dumper component is not installed.', E_USER_WARNING);
}
}
/**
* Returns a title for the current route.
*/
public static function drupalTitle(): array {
$title = \Drupal::service('title_resolver')->getTitle(
\Drupal::request(),
\Drupal::routeMatch()->getRouteObject()
);
$build['#markup'] = render($title);
$build['#cache']['contexts'] = ['url'];
return $build;
}
/**
* Generates a URL from an internal path.
*
* @param string $user_input
* User input for a link or path.
* @param array $options
* (optional) An array of options.
* @param bool $check_access
* (optional) Indicates that access check is required.
*
* @return \Drupal\Core\Url|null
* A new Url object or null if the URL is not accessible.
*
* @see \Drupal\Core\Url::fromUserInput()
*/
public static function drupalUrl(string $user_input, array $options = [], bool $check_access = FALSE): ?Url {
if (isset($options['langcode'])) {
$language_manager = \Drupal::languageManager();
if ($language = $language_manager->getLanguage($options['langcode'])) {
$options['language'] = $language;
}
}
if (!in_array($user_input[0], ['/', '#', '?'])) {
$user_input = '/' . $user_input;
}
$url = Url::fromUserInput($user_input, $options);
return (!$check_access || $url->access()) ? $url : NULL;
}
/**
* Generates a link from an internal path.
*
* @param string|\Twig\Markup $text
* The text to be used for the link.
* @param string $user_input
* User input for a link or path.
* @param array $options
* (optional) An array of options.
* @param bool $check_access
* (optional) Indicates that access check is required.
*
* @return \Drupal\Core\Link|null
* A new Link object or null of the URL is not accessible.
*
* @see \Drupal\Core\Link::fromTextAndUrl()
*/
public static function drupalLink($text, string $user_input, array $options = [], bool $check_access = FALSE): ?Link {
$url = self::drupalUrl($user_input, $options, $check_access);
if ($url) {
// The text has been processed by twig already, convert it to a safe
// object for the render system.
// @see \Drupal\Core\Template\TwigExtension::getLink()
if ($text instanceof TwigMarkup) {
$text = Markup::create($text);
}
return Link::fromTextAndUrl($text, $url);
}
return NULL;
}
/**
* Builds the breadcrumb.
*/
public static function drupalBreadcrumb(): array {
return \Drupal::service('breadcrumb')
->build(\Drupal::routeMatch())
->toRenderable();
}
/**
* Builds contextual links.
*
* @param string $id
* A serialized representation of a #contextual_links property value array.
*
* @return array
* A renderable array representing contextual links.
*
* @see https://www.drupal.org/node/2133283
*/
public static function drupalContextualLinks(string $id): array {
$build['#cache']['contexts'] = ['user.permissions'];
if (\Drupal::currentUser()->hasPermission('access contextual links')) {
$build['#type'] = 'contextual_links_placeholder';
$build['#id'] = $id;
}
return $build;
}
/**
* Emits a breakpoint to the debug client.
*
* @param \Twig\Environment $environment
* The Twig environment instance.
* @param array $context
* Variables from the current Twig template.
*/
public static function drupalBreakpoint(Environment $environment, array $context): void {
if (function_exists('xdebug_break')) {
xdebug_break();
}
else {
trigger_error('Could not make a break because xdebug is not available.', E_USER_WARNING);
}
}
/**
* Replaces all tokens in a given string with appropriate values.
*
* @param string $text
* An HTML string containing replaceable tokens.
*
* @return string
* The entered HTML text with tokens replaced.
*/
public static function tokenReplaceFilter(string $text): string {
return \Drupal::token()->replace($text);
}
/**
* Performs a regular expression search and replace.
*
* @param string $text
* The text to search and replace.
* @param string $pattern
* The pattern to search for.
* @param string $replacement
* The string to replace.
*
* @return string
* The new text if matches are found, otherwise unchanged text.
*/
public static function pregReplaceFilter(string $text, string $pattern, string $replacement): string {
return preg_replace($pattern, $replacement, $text);
}
/**
* Returns the URL of this image derivative for an original image path or URI.
*
* @param string $path
* The path or URI to the original image.
* @param string $style
* The image style.
*
* @return string|null
* The absolute URL where a style image can be downloaded, suitable for use
* in an <img> tag. Requesting the URL will cause the image to be created.
*/
public static function imageStyleFilter(string $path, string $style): ?string {
if (!$image_style = ImageStyle::load($style)) {
trigger_error(sprintf('Could not load image style %s.', $style));
return NULL;
}
if (!$image_style->supportsUri($path)) {
trigger_error(sprintf('Could not apply image style %s.', $style));
return NULL;
}
return file_url_transform_relative($image_style->buildUrl($path));
}
/**
* Transliterates text from Unicode to US-ASCII.
*
* @param string $text
* The $text to transliterate.
* @param string $langcode
* (optional) The language code of the language the string is in. Defaults
* to 'en' if not provided. Warning: this can be unfiltered user input.
* @param string $unknown_character
* (optional) The character to substitute for characters in $string without
* transliterated equivalents. Defaults to '?'.
* @param int $max_length
* (optional) If provided, return at most this many characters, ensuring
* that the transliteration does not split in the middle of an input
* character's transliteration.
*
* @return string
* $string with non-US-ASCII characters transliterated to US-ASCII
* characters, and unknown characters replaced with $unknown_character.
*/
public static function transliterateFilter(string $text, string $langcode = 'en', string $unknown_character = '?', int $max_length = NULL) {
return \Drupal::transliteration()->transliterate($text, $langcode, $unknown_character, $max_length);
}
/**
* Returns a render array for entity, field list or field item.
*
* @param mixed $object
* The object to build a render array from.
* @param string|array $view_mode
* Can be either the name of a view mode, or an array of display settings.
* @param string $langcode
* (optional) For which language the entity should be rendered, defaults to
* the current content language.
* @param bool $check_access
* (optional) Indicates that access check for an entity is required.
*
* @return array
* A render array to represent the object.
*/
public static function viewFilter(object $object, ?string $view_mode = 'default', string $langcode = NULL, bool $check_access = TRUE): array {
$build = [];
if ($object instanceof FieldItemListInterface || $object instanceof FieldItemInterface) {
$build = $object->view($view_mode);
}
elseif ($object instanceof EntityInterface) {
$build = \Drupal::service('twig_tweak.entity_view_builder')->build($object, $view_mode, $langcode, $check_access);
}
return $build;
}
/**
* Adds new element to the array.
*
* @param array $build
* The renderable array to add the child item.
* @param mixed $key
* The key of the new element.
* @param mixed $element
* The element to add.
*
* @return array
* The modified array.
*/
public static function withFilter(array $build, $key, $element): array {
if (is_array($key)) {
NestedArray::setValue($build, $key, $element);
}
else {
$build[$key] = $element;
}
return $build;
}
/**
* Filters out the children of a render array, optionally sorted by weight.
*
* @param array $build
* The render array whose children are to be filtered.
* @param bool $sort
* Boolean to indicate whether the children should be sorted by weight.
*
* @return array
* The element's children.
*/
public static function childrenFilter(array $build, bool $sort = FALSE): array {
$keys = Element::children($build, $sort);
return array_intersect_key($build, array_flip($keys));
}
/**
* Returns a URL path to the file.
*
* @param string|object $input
* Can be either file URI or an object that contains the URI.
*
* @return string|null
* A URL that may be used to access the file.
*/
public static function fileUrlFilter($input): ?string {
if (is_string($input)) {
return file_url_transform_relative(file_create_url($input));
}
if ($input instanceof EntityReferenceFieldItemListInterface) {
$referenced_entities = $input->referencedEntities();
if (isset($referenced_entities[0])) {
return self::getUrlFromEntity($referenced_entities[0]);
}
}
elseif ($input instanceof EntityReferenceItem) {
return self::getUrlFromEntity($input->entity);
}
}
/**
* Evaluates a string of PHP code.
*
* @param string $code
* Valid PHP code to be evaluated.
*
* @return mixed
* The eval() result.
*/
public static function phpFilter(string $code) {
ob_start();
// @codingStandardsIgnoreStart
print eval($code);
// @codingStandardsIgnoreEnd
$output = ob_get_contents();
ob_end_clean();
return $output;
}
/**
* Extracts file URL form content entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* Entity object that contains information about the file.
*
* @return string|null
* A URL that may be used to access the file.
*/
private static function getUrlFromEntity(EntityInterface $entity): ?string {
if ($entity instanceof MediaInterface) {
$source = $entity->getSource();
$value = $source->getSourceFieldValue($entity);
if ($source instanceof OEmbedInterface) {
return $value;
}
elseif ($file = File::load($value)) {
return $file->createFileUrl();
}
}
elseif ($entity instanceof FileInterface) {
return $entity->createFileUrl();
}
}
}

155
src/View/BlockViewBuilder.php

@ -0,0 +1,155 @@
<?php
namespace Drupal\twig_tweak\View;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Block\TitleBlockPluginInterface;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Controller\TitleResolverInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Plugin\Context\ContextHandlerInterface;
use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* BlockViewBuilder service.
*/
class BlockViewBuilder {
/**
* The plugin.manager.block service.
*
* @var \Drupal\Core\Cache\CacheableDependencyInterface
*/
protected $pluginManagerBlock;
/**
* The context repository service.
*
* @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface
*/
protected $contextRepository;
/**
* The plugin context handler.
*
* @var \Drupal\Core\Plugin\Context\ContextHandlerInterface
*/
protected $contextHandler;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* The title resolver.
*
* @var \Drupal\Core\Controller\TitleResolverInterface
*/
protected $titleResolver;
/**
* Constructs a BlockViewBuilder object.
*/
public function __construct(
CacheableDependencyInterface $plugin_manager_block,
ContextRepositoryInterface $context_repository,
ContextHandlerInterface $context_handler,
AccountInterface $account,
RequestStack $request_stack,
RouteMatchInterface $route_match,
TitleResolverInterface $title_resolver
) {
$this->pluginManagerBlock = $plugin_manager_block;
$this->contextRepository = $context_repository;
$this->contextHandler = $context_handler;
$this->account = $account;
$this->requestStack = $request_stack;
$this->routeMatch = $route_match;
$this->titleResolver = $title_resolver;
}
/**
* Builds the render array for a block.
*
* @param string $id
* The string of block plugin to render.
* @param array $configuration
* (optional) Pass on any configuration to the plugin block.
* @param bool $wrapper
* (optional) Whether or not use block template for rendering.
*
* @return array
* A renderable array representing the content of the block.
*/
public function build(string $id, array $configuration = [], bool $wrapper = TRUE): array {
$configuration += ['label_display' => BlockPluginInterface::BLOCK_LABEL_VISIBLE];
/** @var \Drupal\Core\Block\BlockPluginInterface $block_plugin */
$block_plugin = $this->pluginManagerBlock->createInstance($id, $configuration);
// Inject runtime contexts.
if ($block_plugin instanceof ContextAwarePluginInterface) {
$contexts = $this->contextRepository->getRuntimeContexts($block_plugin->getContextMapping());
$this->contextHandler->applyContextMapping($block_plugin, $contexts);
}
$build = [];
$access = $block_plugin->access($this->account, TRUE);
if ($access->isAllowed()) {
// Title block needs a special treatment.
if ($block_plugin instanceof TitleBlockPluginInterface) {
$request = $this->requestStack->getCurrentRequest();
$title = $this->titleResolver->getTitle($request, $this->routeMatch->getRouteObject());
$block_plugin->setTitle($title);
}
$build['content'] = $block_plugin->build();
if ($block_plugin instanceof TitleBlockPluginInterface) {
$build['content']['#cache']['contexts'][] = 'url';
}
if ($wrapper && !Element::isEmpty($build['content'])) {
$build += [
'#theme' => 'block',
'#attributes' => [],
'#contextual_links' => [],
'#configuration' => $block_plugin->getConfiguration(),
'#plugin_id' => $block_plugin->getPluginId(),
'#base_plugin_id' => $block_plugin->getBaseId(),
'#derivative_plugin_id' => $block_plugin->getDerivativeId(),
];
}
}
CacheableMetadata::createFromRenderArray($build)
->merge(CacheableMetadata::createFromObject($access))
->applyTo($build);
return $build;
}
}

60
src/View/EntityFormViewBuilder.php

@ -0,0 +1,60 @@
<?php
namespace Drupal\twig_tweak\View;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityFormBuilderInterface;
use Drupal\Core\Entity\EntityInterface;
/**
* EntityFormViewBuilder service.
*/
class EntityFormViewBuilder {
/**
* The entity form builder service.
*
* @var \Drupal\Core\Entity\EntityFormBuilderInterface
*/
protected $entityFormBuilder;
/**
* Constructs an EntityFormViewBuilder object.
*/
public function __construct(EntityFormBuilderInterface $entity_form_builder) {
$this->entityFormBuilder = $entity_form_builder;
}
/**
* Gets the built and processed entity form for the given entity type.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
* @param string $form_mode
* (optional) The mode identifying the form variation to be returned.
* @param bool $check_access
* (optional) Indicates that access check is required.
*
* @return array
* The processed form for the given entity type and form mode.
*/
public function build(EntityInterface $entity, string $form_mode = 'default', bool $check_access = TRUE): array {
$build = [];
$operation = $entity->isNew() ? 'create' : 'update';
$access = $check_access ? $entity->access($operation, NULL, TRUE) : AccessResult::allowed();
if ($access->isAllowed()) {
$build = $this->entityFormBuilder->getForm($entity, $form_mode);
}
CacheableMetadata::createFromRenderArray($build)
->merge(CacheableMetadata::createFromObject($entity))
->merge(CacheableMetadata::createFromObject($access))
->applyTo($build);
return $build;
}
}

47
src/View/EntityViewBuilder.php

@ -0,0 +1,47 @@
<?php
namespace Drupal\twig_tweak\View;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
/**
* EntityViewBuilder service.
*/
class EntityViewBuilder {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs an EntityViewBuilder object.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* Builds a render array for a given entity.
*/
public function build(EntityInterface $entity, string $view_mode = 'full', string $langcode = NULL, bool $check_access = TRUE): array {
$build = [];
$access = $check_access ? $entity->access('view', NULL, TRUE) : AccessResult::allowed();
if ($access->isAllowed()) {
$build = $this->entityTypeManager
->getViewBuilder($entity->getEntityTypeId())
->view($entity, $view_mode, $langcode);
}
CacheableMetadata::createFromRenderArray($build)
->merge(CacheableMetadata::createFromObject($entity))
->merge(CacheableMetadata::createFromObject($access))
->applyTo($build);
return $build;
}
}

74
src/View/FieldViewBuilder.php

@ -0,0 +1,74 @@
<?php
namespace Drupal\twig_tweak\View;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
/**
* FieldViewBuilder service.
*/
class FieldViewBuilder {
/**
* The entity repository.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $entityRepository;
/**
* Constructs a FieldViewBuilder object.
*/
public function __construct(EntityRepositoryInterface $entity_repository) {
$this->entityRepository = $entity_repository;
}
/**
* Returns the render array for a single entity field.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
* @param string $field_name
* The field name.
* @param string|array $view_mode
* (optional) The view mode that should be used to render the field.
* @param string $langcode
* (optional) Language code to load translation.
* @param bool $check_access
* (optional) Indicates that access check is required.
*
* @return array
* A render array for the field.
*/
public function build(
EntityInterface $entity,
string $field_name,
$view_mode = 'full',
string $langcode = NULL,
bool $check_access = TRUE
): array {
$build = [];
$access = $check_access ? $entity->access('view', NULL, TRUE) : AccessResult::allowed();
if ($access->isAllowed()) {
$entity = $this->entityRepository->getTranslationFromContext($entity, $langcode);
if (!isset($entity->{$field_name})) {
// @todo Trigger error here.
return [];
}
$build = $entity->{$field_name}->view($view_mode);
}
CacheableMetadata::createFromRenderArray($build)
->merge(CacheableMetadata::createFromObject($access))
->merge(CacheableMetadata::createFromObject($entity))
->applyTo($build);
return $build;
}
}

61
src/View/ImageViewBuilder.php

@ -0,0 +1,61 @@
<?php
namespace Drupal\twig_tweak\View;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\file\FileInterface;
/**
* ImageViewBuilder service.
*/
class ImageViewBuilder {
/**
* Builds an image.
*
* @param \Drupal\file\FileInterface $file
* The file object.
* @param string $style
* (optional) Image style.
* @param array $attributes
* (optional) Image attributes.
* @param bool $responsive
* (optional) Indicates that the provided image style is responsive.
* @param bool $check_access
* (optional) Indicates that access check is required.
*
* @return array
* A renderable array to represent the image.
*/
public function build(FileInterface $file, string $style = NULL, array $attributes = [], bool $responsive = FALSE, bool $check_access = TRUE): array {
$access = $check_access ? $file->access('view', NULL, TRUE) : AccessResult::allowed();
$build = [];
if ($access->isAllowed()) {
$build['#uri'] = $file->getFileUri();
$build['#attributes'] = $attributes;
if ($style) {
if ($responsive) {
$build['#type'] = 'responsive_image';
$build['#responsive_image_style_id'] = $style;
}
else {
$build['#theme'] = 'image_style';
$build['#style_name'] = $style;
}
}
else {
$build['#theme'] = 'image';
}
}
CacheableMetadata::createFromRenderArray($build)
->merge(CacheableMetadata::createFromObject($access))
->applyTo($build);
return $build;
}
}

70
src/View/MenuViewBuilder.php

@ -0,0 +1,70 @@
<?php
namespace Drupal\twig_tweak\View;
use Drupal\Core\Menu\MenuLinkTreeInterface;
/**
* MenuViewBuilder service.
*/
class MenuViewBuilder {
/**
* The menu link tree service.
*
* @var \Drupal\Core\Menu\MenuLinkTreeInterface
*/
protected $menuLinkTree;
/**
* Constructs a MenuViewBuilder object.
*/
public function __construct(MenuLinkTreeInterface $menu_link_tree) {
$this->menuLinkTree = $menu_link_tree;
}
/**
* Returns the render array for a menu.
*
* @param string $menu_name
* The name of the menu.
* @param int $level
* (optional) Initial menu level.
* @param int $depth
* (optional) Maximum number of menu levels to display.
* @param bool $expand
* (optional) Expand all menu links.
*
* @return array
* A render array for the menu.
*
* @see \Drupal\system\Plugin\Block\SystemMenuBlock::build()
*/
public function build(string $menu_name, int $level = 1, int $depth = 0, bool $expand = FALSE): array {
$parameters = $this->menuLinkTree->getCurrentRouteMenuTreeParameters($menu_name);
// Adjust the menu tree parameters based on the block's configuration.
$parameters->setMinDepth($level);
// When the depth is configured to zero, there is no depth limit. When depth
// is non-zero, it indicates the number of levels that must be displayed.
// Hence this is a relative depth that we must convert to an actual
// (absolute) depth, that may never exceed the maximum depth.
if ($depth > 0) {
$parameters->setMaxDepth(min($level + $depth - 1, $this->menuLinkTree->maxDepth()));
}
// If expandedParents is empty, the whole menu tree is built.
if ($expand) {
$parameters->expandedParents = [];
}
$tree = $this->menuLinkTree->load($menu_name, $parameters);
$manipulators = [
['callable' => 'menu.default_tree_manipulators:checkAccess'],
['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
];
$tree = $this->menuLinkTree->transform($tree, $manipulators);
return $this->menuLinkTree->build($tree);
}
}

111
src/View/RegionViewBuilder.php

@ -0,0 +1,111 @@
<?php
namespace Drupal\twig_tweak\View;
use Drupal\Core\Block\TitleBlockPluginInterface;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Controller\TitleResolverInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* RegionViewBuilder service.
*/
class RegionViewBuilder {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* The title resolver.
*
* @var \Drupal\Core\Controller\TitleResolverInterface
*/
protected $titleResolver;
/**
* Constructs a RegionViewBuilder object.
*/
public function __construct(
EntityTypeManagerInterface $entity_type_manager,
ConfigFactoryInterface $config_factory,
RequestStack $request_stack,
TitleResolverInterface $title_resolver
) {
$this->entityTypeManager = $entity_type_manager;
$this->configFactory = $config_factory;
$this->requestStack = $request_stack;
$this->titleResolver = $title_resolver;
}
/**
* Builds the render array of a given region.
*
* @param string $region
* The region to build.
* @param string $theme
* (optional) The name of the theme to load the region. If it is not
* provided then default theme will be used.
*
* @return array
* A render array to display the region content.
*/
public function build(string $region, string $theme = NULL): array {
$blocks = $this->entityTypeManager->getStorage('block')->loadByProperties([
'region' => $region,
'theme' => $theme ?: $this->configFactory->get('system.theme')->get('default'),
]);
$view_builder = $this->entityTypeManager->getViewBuilder('block');
$build = [];
$cache_metadata = new CacheableMetadata();
/* @var $blocks \Drupal\block\BlockInterface[] */
foreach ($blocks as $id => $block) {
$access = $block->access('view', NULL, TRUE);
$cache_metadata = $cache_metadata->merge(CacheableMetadata::createFromObject($access));
if ($access->isAllowed()) {
$block_plugin = $block->getPlugin();
if ($block_plugin instanceof TitleBlockPluginInterface) {
$request = $this->requestStack->getCurrentRequest();
if ($route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)) {
$block_plugin->setTitle($this->titleResolver->getTitle($request, $route));
}
}
$build[$id] = $view_builder->view($block);
}
}
if ($build) {
$build['#region'] = $region;
$build['#theme_wrappers'] = ['region'];
$cache_metadata->applyTo($build);
}
return $build;
}
}

273
tests/src/Functional/TwigTweakTest.php

@ -3,14 +3,14 @@
namespace Drupal\Tests\twig_tweak\Functional; namespace Drupal\Tests\twig_tweak\Functional;
use Drupal\Core\Link; use Drupal\Core\Link;
use Drupal\Core\Render\Markup;
use Drupal\Core\Url; use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\TestFileCreationTrait;
use Drupal\file\Entity\File; use Drupal\file\Entity\File;
use Drupal\language\Entity\ConfigurableLanguage; use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\media\Entity\Media; use Drupal\media\Entity\Media;
use Drupal\responsive_image\Entity\ResponsiveImageStyle; use Drupal\responsive_image\Entity\ResponsiveImageStyle;
use Drupal\Core\Render\Markup;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\TestFileCreationTrait;
use Drupal\user\Entity\Role; use Drupal\user\Entity\Role;
/** /**
@ -18,10 +18,15 @@ use Drupal\user\Entity\Role;
* *
* @group twig_tweak * @group twig_tweak
*/ */
class TwigTweakTest extends BrowserTestBase { final class TwigTweakTest extends BrowserTestBase {
use TestFileCreationTrait; use TestFileCreationTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'classy';
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@ -40,7 +45,7 @@ class TwigTweakTest extends BrowserTestBase {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function setUp() { public function setUp(): void {
parent::setUp(); parent::setUp();
$test_files = $this->getTestFiles('image'); $test_files = $this->getTestFiles('image');
@ -88,292 +93,274 @@ class TwigTweakTest extends BrowserTestBase {
'breakpoint_group' => 'responsive_image', 'breakpoint_group' => 'responsive_image',
])->save(); ])->save();
// Setup Russian. // Setup Russian language.
ConfigurableLanguage::createFromLangcode('ru')->save(); ConfigurableLanguage::createFromLangcode('ru')->save();
} }
/** /**
* Tests output produced by the Twig extension. * Tests output produced by the Twig extension.
*/ */
public function testOutput() { public function testOutput(): void {
// Title block rendered through drupal_region() is cached by some reason.
\Drupal::service('cache_tags.invalidator')->invalidateTags(['block_view']); $this->drupalGet('twig-tweak-test');
$this->drupalGet('<front>');
// -- Test default views display. // -- View (default display).
$xpath = '//div[@class = "tt-view-default"]'; $xpath = '//div[@class = "tt-view-default"]';
$xpath .= '//div[contains(@class, "view-twig-tweak-test") and contains(@class, "view-display-id-default")]'; $xpath .= '//div[contains(@class, "view-twig-tweak-test") and contains(@class, "view-display-id-default")]';
$xpath .= '/div[@class = "view-content"]//ul[count(./li) = 3]/li'; $xpath .= '/div[@class = "view-content"]//ul[count(./li) = 3]/li';
$this->assertByXpath($xpath . '//a[contains(@href, "/node/1") and text() = "Alpha"]'); $this->assertXpath($xpath . '//a[contains(@href, "/node/1") and text() = "Alpha"]');
$this->assertByXpath($xpath . '//a[contains(@href, "/node/2") and text() = "Beta"]'); $this->assertXpath($xpath . '//a[contains(@href, "/node/2") and text() = "Beta"]');
$this->assertByXpath($xpath . '//a[contains(@href, "/node/3") and text() = "Gamma"]'); $this->assertXpath($xpath . '//a[contains(@href, "/node/3") and text() = "Gamma"]');
// -- Test page_1 view display. // -- View (page_1 display).
$xpath = '//div[@class = "tt-view-page_1"]'; $xpath = '//div[@class = "tt-view-page_1"]';
$xpath .= '//div[contains(@class, "view-twig-tweak-test") and contains(@class, "view-display-id-page_1")]'; $xpath .= '//div[contains(@class, "view-twig-tweak-test") and contains(@class, "view-display-id-page_1")]';
$xpath .= '/div[@class = "view-content"]//ul[count(./li) = 3]/li'; $xpath .= '/div[@class = "view-content"]//ul[count(./li) = 3]/li';
$this->assertByXpath($xpath . '//a[contains(@href, "/node/1") and text() = "Alpha"]'); $this->assertXpath($xpath . '//a[contains(@href, "/node/1") and text() = "Alpha"]');
$this->assertByXpath($xpath . '//a[contains(@href, "/node/2") and text() = "Beta"]'); $this->assertXpath($xpath . '//a[contains(@href, "/node/2") and text() = "Beta"]');
$this->assertByXpath($xpath . '//a[contains(@href, "/node/3") and text() = "Gamma"]'); $this->assertXpath($xpath . '//a[contains(@href, "/node/3") and text() = "Gamma"]');
// -- Test view argument. // -- View with arguments.
$xpath = '//div[@class = "tt-view-page_1-with-argument"]'; $xpath = '//div[@class = "tt-view-page_1-with-argument"]';
$xpath .= '//div[contains(@class, "view-twig-tweak-test")]'; $xpath .= '//div[contains(@class, "view-twig-tweak-test")]';
$xpath .= '/div[@class = "view-content"]//ul[count(./li) = 1]/li'; $xpath .= '/div[@class = "view-content"]//ul[count(./li) = 1]/li';
$this->assertByXpath($xpath . '//a[contains(@href, "/node/1") and text() = "Alpha"]'); $this->assertXpath($xpath . '//a[contains(@href, "/node/1") and text() = "Alpha"]');
// -- Test view result. // -- View result.
$xpath = '//div[@class = "tt-view-result" and text() = 3]'; $xpath = '//div[@class = "tt-view-result" and text() = 3]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test block. // -- Block.
$xpath = '//div[@class = "tt-block"]'; $xpath = '//div[@class = "tt-block"]';
$xpath .= '/img[contains(@src, "/core/themes/classy/logo.svg") and @alt="Home"]'; $xpath .= '/img[contains(@src, "/core/themes/classy/logo.svg") and @alt="Home"]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test block with wrapper. // -- Block with wrapper.
$xpath = '//div[@class = "tt-block-with-wrapper"]'; $xpath = '//div[@class = "tt-block-with-wrapper"]';
$xpath .= '/div[@class = "block block-system block-system-branding-block"]'; $xpath .= '/div[@class = "block block-system block-system-branding-block"]';
$xpath .= '/h2[text() = "Branding"]'; $xpath .= '/h2[text() = "Branding"]';
$xpath .= '/following-sibling::a[img[contains(@src, "/core/themes/classy/logo.svg") and @alt="Home"]]'; $xpath .= '/following-sibling::a[img[contains(@src, "/core/themes/classy/logo.svg") and @alt="Home"]]';
$xpath .= '/following-sibling::div[@class = "site-name"]/a'; $xpath .= '/following-sibling::div[@class = "site-name"]/a';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test region. // -- Region.
$xpath = '//div[@class = "tt-region"]/div[@class = "region region-sidebar-first"]'; $xpath = '//div[@class = "tt-region"]/div[@class = "region region-sidebar-first"]';
$xpath .= '/div[contains(@class, "block-page-title-block") and h1[@class="page-title" and text() = "Log in"]]'; $xpath .= '/div[contains(@class, "block-page-title-block") and h1[@class="page-title" and text() = "Twig Tweak Test"]]';
$xpath .= '/following-sibling::div[contains(@class, "block-system-powered-by-block")]/span[. = "Powered by Drupal"]'; $xpath .= '/following-sibling::div[contains(@class, "block-system-powered-by-block")]/span[. = "Powered by Drupal"]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test entity default view mode. // -- Entity (default view mode).
$xpath = '//div[@class = "tt-entity-default"]'; $xpath = '//div[@class = "tt-entity-default"]';
$xpath .= '/article[contains(@class, "node") and not(contains(@class, "node--view-mode-teaser"))]'; $xpath .= '/article[contains(@class, "node") and not(contains(@class, "node--view-mode-teaser"))]';
$xpath .= '/h2/a/span[text() = "Alpha"]'; $xpath .= '/h2/a/span[text() = "Alpha"]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test entity teaser view mode. // -- Entity (teaser view mode).
$xpath = '//div[@class = "tt-entity-teaser"]'; $xpath = '//div[@class = "tt-entity-teaser"]';
$xpath .= '/article[contains(@class, "node") and contains(@class, "node--view-mode-teaser")]'; $xpath .= '/article[contains(@class, "node") and contains(@class, "node--view-mode-teaser")]';
$xpath .= '/h2/a/span[text() = "Alpha"]'; $xpath .= '/h2/a/span[text() = "Alpha"]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test loading entity from URL. // -- Entity add form (unprivileged user).
$xpath = '//div[@class = "tt-entity-from-url" and not(text())]';
$this->assertByXpath($xpath);
$this->drupalGet('/node/2');
$xpath = '//div[@class = "tt-entity-from-url"]';
$xpath .= '/article[contains(@class, "node")]';
$xpath .= '/h2/a/span[text() = "Beta"]';
$this->assertByXpath($xpath);
// -- Test access to entity add form.
$xpath = '//div[@class = "tt-entity-add-form"]/form'; $xpath = '//div[@class = "tt-entity-add-form"]/form';
$this->assertSession()->elementNotExists('xpath', $xpath); $this->assertSession()->elementNotExists('xpath', $xpath);
// -- Test access to entity edit form. // -- Entity edit form (unprivileged user).
$xpath = '//div[@class = "tt-entity-edit-form"]/form'; $xpath = '//div[@class = "tt-entity-edit-form"]/form';
$this->assertSession()->elementNotExists('xpath', $xpath); $this->assertSession()->elementNotExists('xpath', $xpath);
// Grant require permissions and test the forms again. // Grant require permissions and test the forms again.
$permissions = ['create page content', 'edit any page content']; $permissions = ['create page content', 'edit any page content'];
$this->grantPermissions(Role::load(Role::ANONYMOUS_ID), $permissions); /** @var \Drupal\user\RoleInterface $role */
$this->drupalGet('/node/2'); $role = Role::load(Role::ANONYMOUS_ID);
$this->grantPermissions($role, $permissions);
$this->drupalGet($this->getUrl());
// -- Test entity add form. // -- Entity add form.
$xpath = '//div[@class = "tt-entity-add-form"]/form'; $xpath = '//div[@class = "tt-entity-add-form"]/form';
$xpath .= '//input[@name = "title[0][value]" and @value = ""]'; $xpath .= '//input[@name = "title[0][value]" and @value = ""]';
$xpath .= '/../../../div/input[@type = "submit" and @value = "Save"]'; $xpath .= '/../../../div/input[@type = "submit" and @value = "Save"]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test entity edit form. // -- Entity edit form.
$xpath = '//div[@class = "tt-entity-edit-form"]/form'; $xpath = '//div[@class = "tt-entity-edit-form"]/form';
$xpath .= '//input[@name = "title[0][value]" and @value = "Alpha"]'; $xpath .= '//input[@name = "title[0][value]" and @value = "Alpha"]';
$xpath .= '/../../../div/input[@type = "submit" and @value = "Save"]'; $xpath .= '/../../../div/input[@type = "submit" and @value = "Save"]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test field. // -- Field.
$xpath = '//div[@class = "tt-field"]/div[contains(@class, "field--name-body")]/p[text() != ""]'; $xpath = '//div[@class = "tt-field"]/div[contains(@class, "field--name-body")]/p[text() != ""]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test menu (default). // -- Menu.
$xpath = '//div[@class = "tt-menu-default"]/ul[@class = "menu"]/li/a[text() = "Link 1"]/../ul[@class = "menu"]/li/ul[@class = "menu"]/li/a[text() = "Link 3"]'; $xpath = '//div[@class = "tt-menu-default"]/ul[@class = "menu"]/li/a[text() = "Link 1"]/../ul[@class = "menu"]/li/ul[@class = "menu"]/li/a[text() = "Link 3"]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test menu (level). // -- Menu with level option.
$xpath = '//div[@class = "tt-menu-level"]/ul[@class = "menu"]/li/a[text() = "Link 2"]/../ul[@class = "menu"]/li/a[text() = "Link 3"]'; $xpath = '//div[@class = "tt-menu-level"]/ul[@class = "menu"]/li/a[text() = "Link 2"]/../ul[@class = "menu"]/li/a[text() = "Link 3"]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test menu (depth). // -- Menu with depth option.
$xpath = '//div[@class = "tt-menu-depth"]/ul[@class = "menu"]/li[not(ul)]/a[text() = "Link 1"]'; $xpath = '//div[@class = "tt-menu-depth"]/ul[@class = "menu"]/li[not(ul)]/a[text() = "Link 1"]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test form. // -- Form.
$xpath = '//div[@class = "tt-form"]/form[@class="system-cron-settings"]/input[@type = "submit" and @value = "Run cron"]'; $xpath = '//div[@class = "tt-form"]/form[@class="system-cron-settings"]/input[@type = "submit" and @value = "Run cron"]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test image by FID. // -- Image by FID.
$xpath = '//div[@class = "tt-image-by-fid"]/img[contains(@src, "/files/image-test.png")]'; $xpath = '//div[@class = "tt-image-by-fid"]/img[contains(@src, "/files/image-test.png")]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test image by URI. // -- Image by URI.
$xpath = '//div[@class = "tt-image-by-uri"]/img[contains(@src, "/files/image-test.png")]'; $xpath = '//div[@class = "tt-image-by-uri"]/img[contains(@src, "/files/image-test.png")]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test image by UUID. // -- Image by UUID.
$xpath = '//div[@class = "tt-image-by-uuid"]/img[contains(@src, "/files/image-test.png")]'; $xpath = '//div[@class = "tt-image-by-uuid"]/img[contains(@src, "/files/image-test.png")]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test image with style. // -- Image with style.
$xpath = '//div[@class = "tt-image-with-style"]/img[contains(@src, "/files/styles/thumbnail/public/image-test.png")]'; $xpath = '//div[@class = "tt-image-with-style"]/img[contains(@src, "/files/styles/thumbnail/public/image-test.png")]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test image with responsive style. // -- Image with responsive style.
$xpath = '//div[@class = "tt-image-with-responsive-style"]/picture/img[contains(@src, "/files/image-test.png")]'; $xpath = '//div[@class = "tt-image-with-responsive-style"]/picture/img[contains(@src, "/files/image-test.png")]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test token. // -- Token.
$xpath = '//div[@class = "tt-token" and text() = "Drupal"]'; $xpath = '//div[@class = "tt-token" and text() = "Drupal"]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test token with context. // -- Token with context.
$xpath = '//div[@class = "tt-token-data" and text() = "Beta"]'; $xpath = '//div[@class = "tt-token-data" and text() = "Alpha"]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test config. // -- Config.
$xpath = '//div[@class = "tt-config" and text() = "Anonymous"]'; $xpath = '//div[@class = "tt-config" and text() = "Anonymous"]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test page title. // -- Page title.
$xpath = '//div[@class = "tt-title" and text() = "Beta"]'; $xpath = '//div[@class = "tt-title" and text() = "Twig Tweak Test"]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test URL. // -- URL.
$url = Url::fromUserInput('/node/1', ['absolute' => TRUE])->toString(); $url = Url::fromUserInput('/node/1', ['absolute' => TRUE])->toString();
$xpath = sprintf('//div[@class = "tt-url"]/div[@data-case="default" and text() = "%s"]', $url); $xpath = sprintf('//div[@class = "tt-url"]/div[@data-case="default" and text() = "%s"]', $url);
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test URL (with langcode). // -- URL with langcode.
$url = str_replace('node/1', 'ru/node/1', $url); $url = str_replace('node/1', 'ru/node/1', $url);
$xpath = sprintf('//div[@class = "tt-url"]/div[@data-case="with-langcode" and text() = "%s"]', $url); $xpath = sprintf('//div[@class = "tt-url"]/div[@data-case="with-langcode" and text() = "%s"]', $url);
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test link. // -- Link.
$url = Url::fromUserInput('/node/1/edit', ['absolute' => TRUE]); $url = Url::fromUserInput('/node/1/edit', ['absolute' => TRUE]);
$link = Link::fromTextAndUrl('Edit', $url)->toString(); $link = Link::fromTextAndUrl('Edit', $url)->toString();
$xpath = '//div[@class = "tt-link"]'; $xpath = '//div[@class = "tt-link"]';
self::assertEquals($link, trim($this->xpath($xpath)[0]->getHtml())); self::assertEquals($link, trim($this->xpath($xpath)[0]->getHtml()));
// -- Test link with HTML. // -- Link with HTML.
$text = Markup::create('<b>Edit</b>'); $text = Markup::create('<b>Edit</b>');
$url = Url::fromUserInput('/node/1/edit', ['absolute' => TRUE]); $url = Url::fromUserInput('/node/1/edit', ['absolute' => TRUE]);
$link = Link::fromTextAndUrl($text, $url)->toString(); $link = Link::fromTextAndUrl($text, $url)->toString();
$xpath = '//div[@class = "tt-link-html"]'; $xpath = '//div[@class = "tt-link-html"]';
self::assertEquals($link, trim($this->xpath($xpath)[0]->getHtml())); self::assertEquals($link, trim($this->xpath($xpath)[0]->getHtml()));
// -- Test status messages. // -- Status messages.
$xpath = '//div[@class = "tt-messages"]//div[contains(@class, "messages--status") and contains(., "Hello world!")]'; $xpath = '//div[@class = "tt-messages"]//div[contains(@class, "messages--status") and contains(., "Hello world!")]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test breadcrumb. // -- Breadcrumb.
$xpath = '//div[@class = "tt-breadcrumb"]/nav[@class = "breadcrumb"]/ol/li/a[text() = "Home"]'; $xpath = '//div[@class = "tt-breadcrumb"]/nav[@class = "breadcrumb"]/ol/li/a[text() = "Home"]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test protected link. // -- Protected link.
$xpath = '//div[@class = "tt-link-access"]'; $xpath = '//div[@class = "tt-link-access"]';
self::assertEquals('', trim($this->xpath($xpath)[0]->getHtml())); self::assertEquals('', trim($this->xpath($xpath)[0]->getHtml()));
// -- Test token replacement. // -- Token replacement.
$xpath = '//div[@class = "tt-token-replace" and text() = "Site name: Drupal"]'; $xpath = '//div[@class = "tt-token-replace" and text() = "Site name: Drupal"]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test contextual links. // -- Contextual links.
$xpath = '//div[@class="tt-contextual-links" and not(div[@data-contextual-id])]'; $xpath = '//div[@class="tt-contextual-links" and not(div[@data-contextual-id])]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
$this->grantPermissions(Role::load(Role::ANONYMOUS_ID), ['access contextual links']); /** @var \Drupal\user\RoleInterface $role */
$role = Role::load(Role::ANONYMOUS_ID);
$this->grantPermissions($role, ['access contextual links']);
$this->drupalGet($this->getUrl()); $this->drupalGet($this->getUrl());
$xpath = '//div[@class="tt-contextual-links" and div[@data-contextual-id]]'; $xpath = '//div[@class="tt-contextual-links" and div[@data-contextual-id]]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test preg replacement. // -- Replace (preg).
$xpath = '//div[@class = "tt-preg-replace" and text() = "FOO-bar"]'; $xpath = '//div[@class = "tt-preg-replace" and text() = "FOO-bar"]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test image style. // -- Image style.
$xpath = '//div[@class = "tt-image-style" and contains(text(), "styles/thumbnail/public/images/ocean.jpg")]'; $xpath = '//div[@class = "tt-image-style" and contains(text(), "styles/thumbnail/public/images/ocean.jpg")]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test transliteration. // -- Transliterate.
$xpath = '//div[@class = "tt-transliterate" and contains(text(), "Privet!")]'; $xpath = '//div[@class = "tt-transliterate" and contains(text(), "Privet!")]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test text format. // -- Text format.
$xpath = '//div[@class = "tt-check-markup"]'; $xpath = '//div[@class = "tt-check-markup"]';
self::assertEquals('<b>bold</b> strong', trim($this->xpath($xpath)[0]->getHtml())); self::assertEquals('<b>bold</b> strong', trim($this->xpath($xpath)[0]->getHtml()));
// -- Test truncation. // -- Truncate.
$xpath = '//div[@class = "tt-truncate" and text() = "Hello…"]'; $xpath = '//div[@class = "tt-truncate" and text() = "Hello…"]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test 'with'. // -- 'with'.
$xpath = '//div[@class = "tt-with"]/b[text() = "Example"]'; $xpath = '//div[@class = "tt-with"]/b[text() = "Example"]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test nested 'with'. // -- Nested 'with'.
$xpath = '//div[@class = "tt-with-nested" and text() = "{alpha:{beta:{gamma:456}}}"]'; $xpath = '//div[@class = "tt-with-nested" and text() = "{alpha:{beta:{gamma:456}}}"]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test 'children'. // -- 'children'.
$xpath = '//div[@class = "tt-children" and text() = "doremi"]'; $xpath = '//div[@class = "tt-children" and text() = "doremi"]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test entity view. // -- Entity view.
$xpath = '//div[@class = "tt-node-view"]/article[contains(@class, "node--view-mode-default")]/h2[a/span[text() = "Beta"]]'; $xpath = '//div[@class = "tt-node-view"]/article[contains(@class, "node--view-mode-default")]/h2[a/span[text() = "Alpha"]]';
$xpath .= '/following-sibling::div[@class = "node__content"]/div/p'; $xpath .= '/following-sibling::div[@class = "node__content"]/div/p';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test Field list view. // -- Field list view.
$xpath = '//div[@class = "tt-field-list-view"]/span[contains(@class, "field--name-title") and text() = "Beta"]'; $xpath = '//div[@class = "tt-field-list-view"]/span[contains(@class, "field--name-title") and text() = "Alpha"]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test field item view. // -- Field item view.
$xpath = '//div[@class = "tt-field-item-view" and text() = "Beta"]'; $xpath = '//div[@class = "tt-field-item-view" and text() = "Alpha"]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test file URL from URI. // -- File URL from URI.
$xpath = '//div[@class = "tt-file-url-from-uri" and contains(text(), "/files/image-test.png")]'; $xpath = '//div[@class = "tt-file-url-from-uri" and contains(text(), "/files/image-test.png")]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test file URL from image field. // -- File URL from image field.
$this->drupalGet('/node/1');
$xpath = '//div[@class = "tt-file-url-from-image-field" and contains(text(), "/files/image-test.png")]'; $xpath = '//div[@class = "tt-file-url-from-image-field" and contains(text(), "/files/image-test.png")]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test file URL from a specific image field item. // -- File URL from a specific image field item.
$xpath = '//div[@class = "tt-file-url-from-image-field-delta" and contains(text(), "/files/image-test.png")]'; $xpath = '//div[@class = "tt-file-url-from-image-field-delta" and contains(text(), "/files/image-test.png")]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
// -- Test file URL from media field. // -- File URL from media field.
$xpath = '//div[@class = "tt-file-url-from-media-field" and contains(text(), "/files/image-1.png")]'; $xpath = '//div[@class = "tt-file-url-from-media-field" and contains(text(), "/files/image-1.png")]';
$this->assertByXpath($xpath); $this->assertXpath($xpath);
} }
/** /**
* Checks that an element specified by a the xpath exists on the current page. * Checks that an element specified by a the xpath exists on the current page.
*/ */
public function assertByXpath($xpath) { private function assertXpath(string $xpath): void {
$this->assertSession()->elementExists('xpath', $xpath); $this->assertSession()->elementExists('xpath', $xpath);
} }
/**
* {@inheritdoc}
*/
protected function initFrontPage() {
// Intentionally empty. The parent implementation does a request to the
// front page to init cookie. This causes some troubles in rendering
// attached Twig template because page content type is not created at that
// moment. We can skip this step since this test does not rely on any
// session data.
}
} }

598
tests/src/Kernel/AccessTest.php

@ -1,598 +0,0 @@
<?php
namespace Drupal\Tests\twig_tweak\Kernel;
use Drupal\block\BlockViewBuilder;
use Drupal\block\Entity\Block;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\file\Entity\File;
use Drupal\KernelTests\KernelTestBase;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\node\NodeInterface;
use Drupal\Tests\user\Traits\UserCreationTrait;
/**
* Tests for the Twig Tweak access control.
*
* @group twig_tweak
*/
class AccessTest extends KernelTestBase {
use UserCreationTrait;
/**
* A node for testing.
*
* @var \Drupal\node\NodeInterface
*/
private $node;
/**
* The Twig extension.
*
* @var \Drupal\twig_tweak\TwigExtension
*/
private $twigExtension;
/**
* {@inheritdoc}
*/
public static $modules = [
'twig_tweak',
'twig_tweak_test',
'node',
'file',
'user',
'system',
'block',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installConfig(['system']);
$node_type = NodeType::create([
'type' => 'article',
'name' => 'Article',
]);
$node_type->save();
$values = [
'type' => 'article',
'status' => NodeInterface::PUBLISHED,
// @see twig_tweak_test_node_access()
'title' => 'Entity access test',
];
$this->node = Node::create($values);
$this->node->save();
$this->twigExtension = $this->container->get('twig_tweak.twig_extension');
}
/**
* Test callback.
*/
public function testDrupalEntity() {
// -- Unprivileged user with access check.
$this->setUpCurrentUser(['name' => 'User 1']);
$build = $this->twigExtension->drupalEntity('node', $this->node->id());
self::assertArrayNotHasKey('#node', $build);
$expected_cache = [
'contexts' => ['user.permissions'],
'tags' => ['node:1'],
'max-age' => Cache::PERMANENT,
];
self::assertSame($expected_cache, $build['#cache']);
// -- Unprivileged user without access check.
$build = $this->twigExtension->drupalEntity('node', $this->node->id(), NULL, NULL, FALSE);
self::assertArrayHasKey('#node', $build);
$expected_cache = [
'tags' => [
'node:1',
'node_view',
],
'contexts' => [],
'max-age' => Cache::PERMANENT,
];
self::assertSame($expected_cache, $build['#cache']);
// -- Privileged user with access check.
$this->setUpCurrentUser(['name' => 'User 2'], ['access content']);
$build = $this->twigExtension->drupalEntity('node', $this->node->id());
self::assertArrayHasKey('#node', $build);
$expected_cache = [
'tags' => [
'node:1',
'node_view',
'tag_from_twig_tweak_test_node_access',
],
'contexts' => [
'user',
'user.permissions',
],
'max-age' => 50,
];
self::assertSame($expected_cache, $build['#cache']);
// -- Privileged user without access check.
$build = $this->twigExtension->drupalEntity('node', $this->node->id(), NULL, NULL, FALSE);
self::assertArrayHasKey('#node', $build);
$expected_cache = [
'tags' => [
'node:1',
'node_view',
],
'contexts' => [],
'max-age' => Cache::PERMANENT,
];
self::assertSame($expected_cache, $build['#cache']);
}
/**
* Test callback.
*/
public function testDrupalField() {
// -- Unprivileged user with access check.
$this->setUpCurrentUser(['name' => 'User 1']);
$build = $this->twigExtension->drupalField('title', 'node', $this->node->id());
self::assertArrayNotHasKey('#items', $build);
$expected_cache = [
'contexts' => ['user.permissions'],
'tags' => ['node:1'],
'max-age' => Cache::PERMANENT,
];
self::assertSame($expected_cache, $build['#cache']);
// -- Unprivileged user without access check.
$build = $this->twigExtension->drupalField('title', 'node', $this->node->id(), 'default', NULL, FALSE);
self::assertArrayHasKey('#items', $build);
$expected_cache = [
'contexts' => [],
'tags' => ['node:1'],
'max-age' => Cache::PERMANENT,
];
self::assertSame($expected_cache, $build['#cache']);
// -- Privileged user with access check.
$this->setUpCurrentUser(['name' => 'User 2'], ['access content']);
$build = $this->twigExtension->drupalField('title', 'node', $this->node->id());
self::assertArrayHasKey('#items', $build);
$expected_cache = [
'contexts' => [
'user',
'user.permissions',
],
'tags' => [
'node:1',
'tag_from_twig_tweak_test_node_access',
],
'max-age' => 50,
];
self::assertSame($expected_cache, $build['#cache']);
// -- Privileged user without access check.
$build = $this->twigExtension->drupalField('title', 'node', $this->node->id(), 'default', NULL, FALSE);
self::assertArrayHasKey('#items', $build);
$expected_cache = [
'contexts' => [],
'tags' => ['node:1'],
'max-age' => Cache::PERMANENT,
];
self::assertSame($expected_cache, $build['#cache']);
}
/**
* Test callback.
*/
public function testDrupalEntityEditForm() {
// -- Unprivileged user with access check.
$this->setUpCurrentUser(['name' => 'User 1']);
$build = $this->twigExtension->drupalEntityForm('node', $this->node->id());
self::assertArrayNotHasKey('form_id', $build);
$expected_cache = [
'contexts' => ['user.permissions'],
'tags' => ['node:1'],
'max-age' => Cache::PERMANENT,
];
self::assertSame($expected_cache, $build['#cache']);
// -- Unprivileged user without access check.
$build = $this->twigExtension->drupalEntityForm('node', $this->node->id(), 'default', [], FALSE);
self::assertArrayHasKey('form_id', $build);
$expected_cache = [
'contexts' => ['user.roles:authenticated'],
'tags' => [
'config:core.entity_form_display.node.article.default',
'node:1',
],
'max-age' => Cache::PERMANENT,
];
self::assertSame($expected_cache, $build['#cache']);
// -- Privileged user with access check.
$this->setUpCurrentUser(['name' => 'User 2'], ['access content']);
$build = $this->twigExtension->drupalEntityForm('node', $this->node->id());
self::assertArrayHasKey('#form_id', $build);
$expected_cache = [
'contexts' => [
'user',
'user.permissions',
'user.roles:authenticated',
],
'tags' => [
'config:core.entity_form_display.node.article.default',
'node:1',
'tag_from_twig_tweak_test_node_access',
],
'max-age' => 50,
];
self::assertSame($expected_cache, $build['#cache']);
// -- Privileged user without access check.
$build = $this->twigExtension->drupalEntityForm('node', $this->node->id(), 'default', [], FALSE);
self::assertArrayHasKey('#form_id', $build);
$expected_cache = [
'contexts' => ['user.roles:authenticated'],
'tags' => [
'config:core.entity_form_display.node.article.default',
'node:1',
],
'max-age' => Cache::PERMANENT,
];
self::assertSame($expected_cache, $build['#cache']);
}
/**
* Test callback.
*/
public function testDrupalEntityAddForm() {
$node_values = ['type' => 'article'];
// -- Unprivileged user with access check.
$this->setUpCurrentUser(['name' => 'User 1']);
$build = $this->twigExtension->drupalEntityForm('node', NULL, 'default', $node_values);
self::assertArrayNotHasKey('form_id', $build);
$expected_cache = [
'contexts' => ['user.permissions'],
'tags' => [],
'max-age' => Cache::PERMANENT,
];
self::assertSame($expected_cache, $build['#cache']);
// -- Unprivileged user without access check.
$build = $this->twigExtension->drupalEntityForm('node', NULL, 'default', $node_values, FALSE);
self::assertArrayHasKey('form_id', $build);
$expected_cache = [
'contexts' => ['user.roles:authenticated'],
'tags' => ['config:core.entity_form_display.node.article.default'],
'max-age' => Cache::PERMANENT,
];
self::assertSame($expected_cache, $build['#cache']);
// -- Privileged user with access check.
$this->setUpCurrentUser(['name' => 'User 2'], ['access content', 'create article content']);
$build = $this->twigExtension->drupalEntityForm('node', NULL, 'default', $node_values);
self::assertArrayHasKey('form_id', $build);
$expected_cache = [
'contexts' => [
'user.permissions',
'user.roles:authenticated',
],
'tags' => ['config:core.entity_form_display.node.article.default'],
'max-age' => Cache::PERMANENT,
];
self::assertSame($expected_cache, $build['#cache']);
// -- Privileged user without access check.
$build = $this->twigExtension->drupalEntityForm('node', NULL, 'default', $node_values);
self::assertArrayHasKey('form_id', $build);
$expected_cache = [
'contexts' => [
'user.permissions',
'user.roles:authenticated',
],
'tags' => ['config:core.entity_form_display.node.article.default'],
'max-age' => Cache::PERMANENT,
];
self::assertSame($expected_cache, $build['#cache']);
}
/**
* Test callback.
*
* @see \Drupal\twig_tweak_test\Plugin\Block\FooBlock
*/
public function testDrupalBlock() {
// -- Privileged user.
$this->setUpCurrentUser(['name' => 'User 1']);
$build = $this->twigExtension->drupalBlock('twig_tweak_test_foo');
$expected_content = [
'#markup' => 'Foo',
'#cache' => [
'contexts' => ['url'],
'tags' => ['tag_from_build'],
],
];
self::assertSame($expected_content, $build['content']);
$expected_cache = [
'contexts' => ['user'],
'tags' => ['tag_from_blockAccess'],
'max-age' => 35,
];
self::assertSame($expected_cache, $build['#cache']);
// -- Unprivileged user.
$this->setUpCurrentUser(['name' => 'User 2']);
$build = $this->twigExtension->drupalBlock('twig_tweak_test_foo');
self::assertArrayNotHasKey('content', $build);
$expected_cache = [
'contexts' => ['user'],
'tags' => ['tag_from_blockAccess'],
'max-age' => 35,
];
self::assertSame($expected_cache, $build['#cache']);
}
/**
* Test callback.
*/
public function testDrupalRegion() {
// @codingStandardsIgnoreStart
$create_block = function ($id) {
return new class(['id' => $id], 'block') extends Block {
public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
$result = AccessResult::allowedIf($this->id == 'block_1');
$result->cachePerUser();
$result->addCacheTags(['tag_for_' . $this->id]);
$result->setCacheMaxAge(123);
return $return_as_object ? $result : $result->isAllowed();
}
public function getPlugin() {
return NULL;
}
};
};
// @codingStandardsIgnoreEnd
$storage = $this->createMock(EntityStorageInterface::class);
$blocks = [
'block_1' => $create_block('block_1'),
'block_2' => $create_block('block_2'),
];
$storage->expects($this->any())
->method('loadByProperties')
->willReturn($blocks);
$view_builder = $this->createMock(BlockViewBuilder::class);
$content = [
'#markup' => 'foo',
'#cache' => [
'tags' => ['tag_from_view'],
],
];
$view_builder->expects($this->any())
->method('view')
->willReturn($content);
$entity_type_manager = $this->createMock(EntityTypeManagerInterface::class);
$entity_type_manager->expects($this->any())
->method('getStorage')
->willReturn($storage);
$entity_type_manager->expects($this->any())
->method('getViewBuilder')
->willReturn($view_builder);
$this->container->set('entity_type.manager', $entity_type_manager);
$build = $this->twigExtension->drupalRegion('bar');
$expected_build = [
'block_1' => [
'#markup' => 'foo',
'#cache' => [
'tags' => ['tag_from_view'],
],
],
'#region' => 'bar',
'#theme_wrappers' => ['region'],
'#cache' => [
'contexts' => ['user'],
'tags' => [
'tag_for_block_1',
'tag_for_block_2',
],
'max-age' => 123,
],
];
self::assertSame($expected_build, $build);
}
/**
* Test callback.
*/
public function testDrupalImage() {
// @codingStandardsIgnoreStart
$create_image = function ($uri) {
$file = new class(['uri' => $uri], 'file') extends File {
public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
$is_public = parse_url($this->getFileUri(), PHP_URL_SCHEME) == 'public';
$result = AccessResult::allowedIf($is_public);
$result->cachePerUser();
$result->addCacheTags(['tag_for_' . $this->getFileUri()]);
$result->setCacheMaxAge(123);
return $return_as_object ? $result : $result->isAllowed();
}
public function getPlugin() {
return NULL;
}
};
$file->setFileUri($uri);
return $file;
};
// @codingStandardsIgnoreEnd
$storage = $this->createMock(EntityStorageInterface::class);
$map = [
[
['uri' => 'public://ocean.jpg'],
[$create_image('public://ocean.jpg')],
],
[
['uri' => 'private://sea.jpg'],
[$create_image('private://sea.jpg')],
],
];
$storage->method('loadByProperties')
->will($this->returnValueMap($map));
$entity_type_manager = $this->createMock(EntityTypeManagerInterface::class);
$entity_type_manager->method('getStorage')->willReturn($storage);
$this->container->set('entity_type.manager', $entity_type_manager);
// -- Public image with access check.
$build = $this->twigExtension->drupalImage('public://ocean.jpg');
$expected_build = [
'#uri' => 'public://ocean.jpg',
'#attributes' => [],
'#theme' => 'image',
'#cache' => [
'contexts' => ['user'],
'tags' => ['tag_for_public://ocean.jpg'],
'max-age' => 123,
],
];
self::assertSame($expected_build, $build);
// -- Public image without access check.
$build = $this->twigExtension->drupalImage('public://ocean.jpg', NULL, [], NULL, FALSE);
$expected_build = [
'#uri' => 'public://ocean.jpg',
'#attributes' => [],
'#theme' => 'image',
'#cache' => [
'contexts' => [],
'tags' => [],
'max-age' => Cache::PERMANENT,
],
];
self::assertSame($expected_build, $build);
// -- Private image with access check.
$build = $this->twigExtension->drupalImage('private://sea.jpg');
$expected_build = [
'#cache' => [
'contexts' => ['user'],
'tags' => ['tag_for_private://sea.jpg'],
'max-age' => 123,
],
];
self::assertSame($expected_build, $build);
// -- Private image without access check.
$build = $this->twigExtension->drupalImage('private://sea.jpg', NULL, [], NULL, FALSE);
$expected_build = [
'#uri' => 'private://sea.jpg',
'#attributes' => [],
'#theme' => 'image',
'#cache' => [
'contexts' => [],
'tags' => [],
'max-age' => Cache::PERMANENT,
],
];
self::assertSame($expected_build, $build);
}
/**
* Test callback.
*/
public function testView() {
// -- Unprivileged user with access check.
$this->setUpCurrentUser(['name' => 'User 1']);
$build = $this->twigExtension->view($this->node);
self::assertArrayNotHasKey('#node', $build);
$expected_cache = [
'contexts' => ['user.permissions'],
'tags' => ['node:1'],
'max-age' => Cache::PERMANENT,
];
self::assertSame($expected_cache, $build['#cache']);
// -- Unprivileged user without access check.
$build = $this->twigExtension->view($this->node, NULL, NULL, FALSE);
self::assertArrayHasKey('#node', $build);
$expected_cache = [
'tags' => [
'node:1',
'node_view',
],
'contexts' => [],
'max-age' => Cache::PERMANENT,
];
self::assertSame($expected_cache, $build['#cache']);
// -- Privileged user with access check.
$this->setUpCurrentUser(['name' => 'User 2'], ['access content']);
$build = $this->twigExtension->view($this->node, NULL);
self::assertArrayHasKey('#node', $build);
$expected_cache = [
'tags' => [
'node:1',
'node_view',
'tag_from_twig_tweak_test_node_access',
],
'contexts' => [
'user',
'user.permissions',
],
'max-age' => 50,
];
self::assertSame($expected_cache, $build['#cache']);
// -- Privileged user without access check.
$build = $this->twigExtension->view($this->node, NULL, NULL, FALSE);
self::assertArrayHasKey('#node', $build);
$expected_cache = [
'tags' => [
'node:1',
'node_view',
],
'contexts' => [],
'max-age' => Cache::PERMANENT,
];
self::assertSame($expected_cache, $build['#cache']);
}
}

119
tests/src/Kernel/BlockViewBuilderTest.php

@ -0,0 +1,119 @@
<?php
namespace Drupal\Tests\twig_tweak\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
/**
* A test for BlockViewBuilder.
*
* @group twig_tweak
*/
final class BlockViewBuilderTest extends KernelTestBase {
use UserCreationTrait;
/**
* {@inheritdoc}
*/
public static $modules = [
'twig_tweak',
'twig_tweak_test',
'user',
'system',
'block',
];
/**
* Test callback.
*
* @see \Drupal\twig_tweak_test\Plugin\Block\FooBlock
*/
public function testBlockViewBuilder(): void {
$view_builder = $this->container->get('twig_tweak.block_view_builder');
// -- Default output.
$this->setUpCurrentUser(['name' => 'User 1']);
$build = $view_builder->build('twig_tweak_test_foo');
$expected_build = [
'content' => [
'#markup' => 'Foo',
'#cache' => [
'contexts' => ['url'],
'tags' => ['tag_from_build'],
],
],
'#theme' => 'block',
'#attributes' => [],
'#contextual_links' => [],
'#configuration' => [
'id' => 'twig_tweak_test_foo',
'label' => '',
'provider' => 'twig_tweak_test',
'label_display' => 'visible',
'content' => 'Foo',
],
'#plugin_id' => 'twig_tweak_test_foo',
'#base_plugin_id' => 'twig_tweak_test_foo',
'#derivative_plugin_id' => NULL,
'#cache' => [
'contexts' => ['user'],
'tags' => ['tag_from_blockAccess'],
'max-age' => 35,
],
];
self::assertSame($expected_build, $build);
self::assertSame('<div>Foo</div>', $this->renderPlain($build));
// -- Non-default configuration.
$build = $view_builder->build('twig_tweak_test_foo', ['content' => 'Bar', 'label' => 'Example']);
$expected_build['content']['#markup'] = 'Bar';
$expected_build['#configuration']['label'] = 'Example';
$expected_build['#configuration']['content'] = 'Bar';
self::assertSame($expected_build, $build);
self::assertSame('<div><h2>Example</h2>Bar</div>', $this->renderPlain($build));
// -- Without wrapper.
$build = $view_builder->build('twig_tweak_test_foo', [], FALSE);
$expected_build = [
'content' => [
'#markup' => 'Foo',
'#cache' => [
'contexts' => ['url'],
'tags' => ['tag_from_build'],
],
],
'#cache' => [
'contexts' => ['user'],
'tags' => ['tag_from_blockAccess'],
'max-age' => 35,
],
];
self::assertSame($expected_build, $build);
self::assertSame('Foo', $this->renderPlain($build));
// -- Unprivileged user.
$this->setUpCurrentUser(['name' => 'User 2']);
$build = $view_builder->build('twig_tweak_test_foo');
$expected_build = [
'#cache' => [
'contexts' => ['user'],
'tags' => ['tag_from_blockAccess'],
'max-age' => 35,
],
];
self::assertSame($expected_build, $build);
self::assertSame('', $this->renderPlain($build));
}
/**
* Renders a render array.
*/
private function renderPlain(array $build): string {
$renderer = $this->container->get('renderer');
return rtrim(preg_replace('#\s{2,}#', '', $renderer->renderPlain($build)));
}
}

128
tests/src/Kernel/EntityFormViewBuilderTest.php

@ -0,0 +1,128 @@
<?php
namespace Drupal\Tests\twig_tweak\Kernel;
use Drupal\Core\Cache\Cache;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
/**
* A test for EntityFormViewBuilder.
*
* @group twig_tweak
*/
final class EntityFormViewBuilderTest extends KernelTestBase {
use UserCreationTrait;
/**
* {@inheritdoc}
*/
public static $modules = [
'twig_tweak',
'twig_tweak_test',
'user',
'system',
'node',
'field',
'text',
];
/**
* {@inheritdoc}
*/
public function setUp(): void {
parent::setUp();
$this->installConfig(['system']);
$this->installEntitySchema('node');
NodeType::create(['type' => 'article'])->save();
$this->setUpCurrentUser(['name' => 'User 1'], ['edit any article content', 'access content']);
}
/**
* Test callback.
*/
public function testEntityViewBuilder(): void {
$view_builder = $this->container->get('twig_tweak.entity_form_view_builder');
$values = [
'type' => 'article',
'title' => 'Public node',
];
$public_node = Node::create($values);
$public_node->save();
$values = [
'type' => 'article',
'title' => 'Private node',
];
$private_node = Node::create($values);
$private_node->save();
// -- Default mode.
$build = $view_builder->build($public_node);
self::assertArrayHasKey('#form_id', $build);
$expected_cache = [
'contexts' => [
'user',
'user.permissions',
'user.roles:authenticated',
],
'tags' => [
'config:core.entity_form_display.node.article.default',
'node:1',
'tag_from_twig_tweak_test_node_access',
],
'max-age' => 50,
];
self::assertSame($expected_cache, $build['#cache']);
self::assertContains('<form class="node-article-form node-form" ', $this->renderPlain($build));
// -- Private node with access check.
$build = $view_builder->build($private_node);
self::assertArrayNotHasKey('#form_id', $build);
$expected_cache = [
'contexts' => [
'user',
'user.permissions',
],
'tags' => [
'node:2',
'tag_from_twig_tweak_test_node_access',
],
'max-age' => 50,
];
self::assertSame($expected_cache, $build['#cache']);
self::assertSame('', $this->renderPlain($build));
// -- Private node without access check.
$build = $view_builder->build($private_node, 'default', FALSE);
self::assertArrayHasKey('#form_id', $build);
$expected_cache = [
'contexts' => [
'user.roles:authenticated',
],
'tags' => [
'config:core.entity_form_display.node.article.default',
'node:2',
],
'max-age' => Cache::PERMANENT,
];
self::assertSame($expected_cache, $build['#cache']);
self::assertContains('<form class="node-article-form node-form" ', $this->renderPlain($build));
}
/**
* Renders a render array.
*/
private function renderPlain(array $build): string {
return $this->container->get('renderer')->renderPlain($build);
}
}

203
tests/src/Kernel/EntityViewBuilderTest.php

@ -0,0 +1,203 @@
<?php
namespace Drupal\Tests\twig_tweak\Kernel;
use Drupal\Core\Cache\Cache;
use Drupal\KernelTests\KernelTestBase;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\user\Traits\UserCreationTrait;
/**
* A test for EntityViewBuilder.
*
* @group twig_tweak
*/
final class EntityViewBuilderTest extends KernelTestBase {
use UserCreationTrait;
/**
* {@inheritdoc}
*/
public static $modules = [
'twig_tweak',
'twig_tweak_test',
'user',
'system',
'node',
'field',
'text',
];
/**
* {@inheritdoc}
*/
public function setUp(): void {
parent::setUp();
$this->installConfig(['system', 'node']);
$this->installEntitySchema('node');
NodeType::create(['type' => 'article'])->save();
$this->setUpCurrentUser([], ['access content']);
}
/**
* Test callback.
*/
public function testEntityViewBuilder(): void {
$view_builder = $this->container->get('twig_tweak.entity_view_builder');
$values = [
'type' => 'article',
'title' => 'Public node',
];
$public_node = Node::create($values);
$public_node->save();
$values = [
'type' => 'article',
'title' => 'Private node',
];
$private_node = Node::create($values);
$private_node->save();
// -- Full mode.
$build = $view_builder->build($public_node);
self::assertArrayHasKey('#node', $build);
$expected_cache = [
'tags' => [
'node:1',
'node_view',
'tag_from_twig_tweak_test_node_access',
],
'contexts' => [
'user',
'user.permissions',
],
'max-age' => 50,
'keys' => [
'entity_view',
'node',
'1',
'full',
],
'bin' => 'render',
];
self::assertSame($expected_cache, $build['#cache']);
$expected_html = <<< 'HTML'
<article role="article">
<h2><a href="/node/1" rel="bookmark"><span>Public node</span></a></h2>
<div></div>
</article>
HTML;
$actual_html = $this->renderPlain($build);
self::assertSame(self::normalizeHtml($expected_html), self::normalizeHtml($actual_html));
// -- Teaser mode.
$build = $view_builder->build($public_node, 'teaser');
self::assertArrayHasKey('#node', $build);
$expected_cache = [
'tags' => [
'node:1',
'node_view',
'tag_from_twig_tweak_test_node_access',
],
'contexts' => [
'user',
'user.permissions',
],
'max-age' => 50,
'keys' => [
'entity_view',
'node',
'1',
'teaser',
],
'bin' => 'render',
];
self::assertSame($expected_cache, $build['#cache']);
$expected_html = <<< 'HTML'
<article role="article">
<h2><a href="/node/1" rel="bookmark"><span>Public node</span></a></h2>
<div>
<ul class="links inline">
<li>
<a href="/node/1" rel="tag" title="Public node" hreflang="en">
Read more<span class="visually-hidden"> about Public node</span>
</a>
</li>
</ul>
</div>
</article>
HTML;
$actual_html = $this->renderPlain($build);
self::assertSame(self::normalizeHtml($expected_html), self::normalizeHtml($actual_html));
// -- Private node with access check.
$build = $view_builder->build($private_node);
self::assertArrayNotHasKey('#node', $build);
$expected_cache = [
'contexts' => [
'user',
'user.permissions',
],
'tags' => [
'node:2',
'tag_from_twig_tweak_test_node_access',
],
'max-age' => 50,
];
self::assertSame($expected_cache, $build['#cache']);
self::assertSame('', $this->renderPlain($build));
// -- Private node without access check.
$build = $view_builder->build($private_node, 'full', NULL, FALSE);
self::assertArrayHasKey('#node', $build);
$expected_cache = [
'tags' => [
'node:2',
'node_view',
],
'contexts' => [],
'max-age' => Cache::PERMANENT,
'keys' => [
'entity_view',
'node',
'2',
'full',
],
'bin' => 'render',
];
self::assertSame($expected_cache, $build['#cache']);
$expected_html = <<< 'HTML'
<article role="article">
<h2><a href="/node/2" rel="bookmark"><span>Private node</span></a></h2>
<div></div>
</article>
HTML;
$actual_html = $this->renderPlain($build);
self::assertSame(self::normalizeHtml($expected_html), self::normalizeHtml($actual_html));
}
/**
* Renders a render array.
*/
private function renderPlain(array $build): string {
$actual_html = $this->container->get('renderer')->renderPlain($build);
$actual_html = preg_replace('#<footer>.+</footer>#s', '', $actual_html);
return $actual_html;
}
/**
* Normalizes the provided HTML.
*/
private static function normalizeHtml(string $html): string {
return rtrim(preg_replace(['#\s{2,}#', '#\n#'], '', $html));
}
}

141
tests/src/Kernel/FieldViewBuilderTest.php

@ -0,0 +1,141 @@
<?php
namespace Drupal\Tests\twig_tweak\Kernel;
use Drupal\Core\Cache\Cache;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
/**
* A test for FieldViewBuilder.
*
* @group twig_tweak
*/
final class FieldViewBuilderTest extends KernelTestBase {
use UserCreationTrait;
/**
* {@inheritdoc}
*/
public static $modules = [
'twig_tweak',
'twig_tweak_test',
'user',
'system',
'node',
];
/**
* {@inheritdoc}
*/
public function setUp(): void {
parent::setUp();
$this->installEntitySchema('node');
$this->setUpCurrentUser(['name' => 'User 1'], ['access content']);
NodeType::create(['type' => 'article'])->save();
}
/**
* Test callback.
*/
public function testFieldViewBuilder(): void {
$view_builder = $this->container->get('twig_tweak.field_view_builder');
$values = [
'type' => 'article',
'title' => 'Public node',
];
$public_node = Node::create($values);
$public_node->save();
$values = [
'type' => 'article',
'title' => 'Private node',
];
$private_node = Node::create($values);
$private_node->save();
// -- Full mode.
$build = $view_builder->build($public_node, 'title');
self::assertArrayHasKey(0, $build);
$expected_cache = [
'contexts' => [
'user',
'user.permissions',
],
'tags' => [
'node:1',
'tag_from_twig_tweak_test_node_access',
],
'max-age' => 50,
];
self::assertSame($expected_cache, $build['#cache']);
self::assertSame('<span>Public node</span>', $this->renderPlain($build));
// -- Custom mode.
$build = $view_builder->build($public_node, 'title', ['settings' => ['link_to_entity' => TRUE]]);
self::assertArrayHasKey(0, $build);
$expected_cache = [
'contexts' => [
'user',
'user.permissions',
],
'tags' => [
'node:1',
'tag_from_twig_tweak_test_node_access',
],
'max-age' => 50,
];
self::assertSame($expected_cache, $build['#cache']);
$expected_html = '<span><a href="/node/1" hreflang="en">Public node</a></span>';
self::assertSame($expected_html, $this->renderPlain($build));
// -- Private node with access check.
$build = $view_builder->build($private_node, 'title');
self::assertArrayNotHasKey(0, $build);
$expected_cache = [
'contexts' => [
'user',
'user.permissions',
],
'tags' => [
'node:2',
'tag_from_twig_tweak_test_node_access',
],
'max-age' => 50,
];
self::assertSame($expected_cache, $build['#cache']);
self::assertSame('', $this->renderPlain($build));
// -- Private node without access check.
$build = $view_builder->build($private_node, 'title', 'full', NULL, FALSE);
self::assertArrayHasKey(0, $build);
$expected_cache = [
'contexts' => [],
'tags' => ['node:2'],
'max-age' => Cache::PERMANENT,
];
self::assertSame($expected_cache, $build['#cache']);
self::assertSame('<span>Private node</span>', $this->renderPlain($build));
}
/**
* Renders a render array.
*/
private function renderPlain(array $build): string {
$actual_html = $this->container->get('renderer')->renderPlain($build);
$actual_html = preg_replace('#<footer>.+</footer>#s', '', $actual_html);
$actual_html = preg_replace(['#\s{2,}#', '#\n#'], '', $actual_html);
return $actual_html;
}
}

163
tests/src/Kernel/ImageViewBuilderTest.php

@ -0,0 +1,163 @@
<?php
namespace Drupal\Tests\twig_tweak\Kernel;
use Drupal\Core\Cache\Cache;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\file\Entity\File;
use Drupal\image\Entity\ImageStyle;
use Drupal\KernelTests\KernelTestBase;
use Drupal\responsive_image\Entity\ResponsiveImageStyle;
/**
* A test for ImageViewBuilderTest.
*
* @group twig_tweak
*/
final class ImageViewBuilderTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'twig_tweak',
'twig_tweak_test',
'user',
'system',
'file',
'image',
'responsive_image',
'breakpoint',
];
/**
* {@inheritdoc}
*/
public function setUp(): void {
parent::setUp();
$this->installEntitySchema('file');
$this->installSchema('file', 'file_usage');
ImageStyle::create(['name' => 'large'])->save();
ResponsiveImageStyle::create(['id' => 'wide'])->save();
}
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container) {
parent::register($container);
$container->register('stream_wrapper.private', 'Drupal\Core\StreamWrapper\PrivateStream')
->addTag('stream_wrapper', ['scheme' => 'private']);
}
/**
* Test callback.
*/
public function testImageViewBuilder(): void {
$view_builder = $this->container->get('twig_tweak.image_view_builder');
/** @var \Drupal\file\FileInterface $public_image */
$public_image = File::create(['uri' => 'public://ocean.jpg']);
$public_image->save();
/** @var \Drupal\file\FileInterface $private_image */
$private_image = File::create(['uri' => 'private://sea.jpg']);
$private_image->save();
// -- Without style.
$build = $view_builder->build($public_image);
$expected_build = [
'#uri' => 'public://ocean.jpg',
'#attributes' => [],
'#theme' => 'image',
'#cache' => [
'contexts' => [
'user',
'user.permissions',
],
'tags' => ['tag_for_public://ocean.jpg'],
'max-age' => 70,
],
];
self::assertSame($expected_build, $build);
self::assertSame('<img src="/files/ocean.jpg" alt="" />', $this->renderPlain($build));
// -- With style.
$build = $view_builder->build($public_image, 'large', ['alt' => 'Ocean']);
$expected_build = [
'#uri' => 'public://ocean.jpg',
'#attributes' => ['alt' => 'Ocean'],
'#theme' => 'image_style',
'#style_name' => 'large',
'#cache' => [
'contexts' => [
'user',
'user.permissions',
],
'tags' => ['tag_for_public://ocean.jpg'],
'max-age' => 70,
],
];
self::assertSame($expected_build, $build);
self::assertSame('<img alt="Ocean" src="/files/styles/large/public/ocean.jpg?itok=abc" />', $this->renderPlain($build));
// -- With responsive style.
$build = $view_builder->build($public_image, 'wide', ['alt' => 'Ocean'], TRUE);
$expected_build = [
'#uri' => 'public://ocean.jpg',
'#attributes' => ['alt' => 'Ocean'],
'#type' => 'responsive_image',
'#responsive_image_style_id' => 'wide',
'#cache' => [
'contexts' => [
'user',
'user.permissions',
],
'tags' => ['tag_for_public://ocean.jpg'],
'max-age' => 70,
],
];
self::assertSame($expected_build, $build);
self::assertSame('<picture><img src="/files/ocean.jpg" alt="Ocean" /></picture>', $this->renderPlain($build));
// -- Private image with access check.
$build = $view_builder->build($private_image);
$expected_build = [
'#cache' => [
'contexts' => ['user'],
'tags' => ['tag_for_private://sea.jpg'],
'max-age' => 70,
],
];
self::assertSame($expected_build, $build);
self::assertSame('', $this->renderPlain($build));
// -- Private image without access check.
$build = $view_builder->build($private_image, NULL, [], FALSE, FALSE);
$expected_build = [
'#uri' => 'private://sea.jpg',
'#attributes' => [],
'#theme' => 'image',
'#cache' => [
'contexts' => [],
'tags' => [],
'max-age' => Cache::PERMANENT,
],
];
self::assertSame($expected_build, $build);
self::assertSame('<img src="/files/sea.jpg" alt="" />', $this->renderPlain($build));
}
/**
* Renders a render array.
*/
private function renderPlain(array $build): string {
$html = $this->container->get('renderer')->renderPlain($build);
$html = preg_replace('#src=".+/files/#s', 'src="/files/', $html);
$html = preg_replace('#\?itok=.+"#', '?itok=abc"', $html);
$html = preg_replace(['#\s{2,}#', '#\n#'], '', $html);
return rtrim($html);
}
}

124
tests/src/Kernel/MenuViewBuilderTest.php

@ -0,0 +1,124 @@
<?php
namespace Drupal\Tests\twig_tweak\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\menu_link_content\Entity\MenuLinkContent;
/**
* A test for MenuViewBuilder.
*
* @group twig_tweak
*/
final class MenuViewBuilderTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'twig_tweak',
'user',
'system',
'link',
'menu_link_content',
];
/**
* {@inheritdoc}
*/
public function setUp(): void {
parent::setUp();
$this->installEntitySchema('menu_link_content');
$this->container->get('entity_type.manager')
->getStorage('menu')
->create([
'id' => 'test-menu',
'label' => 'Test menu',
'description' => 'Description text.',
])
->save();
$link_1 = MenuLinkContent::create([
'expanded' => TRUE,
'title' => 'Link 1',
'link' => ['uri' => 'internal:/foo/1'],
'menu_name' => 'test-menu',
]);
$link_1->save();
MenuLinkContent::create([
'title' => 'Link 1.1',
'link' => ['uri' => 'internal:/foo/1/1'],
'menu_name' => 'test-menu',
'parent' => $link_1->getPluginId(),
])->save();
MenuLinkContent::create([
'title' => 'Link 2',
'link' => ['uri' => 'internal:/foo/2'],
'menu_name' => 'test-menu',
])->save();
}
/**
* Test callback.
*/
public function testMenuViewBuilder(): void {
$view_builder = $this->container->get('twig_tweak.menu_view_builder');
$build = $view_builder->build('test-menu');
$expected_output = <<< 'HTML'
<ul>
<li>
<a href="/foo/1">Link 1</a>
<ul>
<li>
<a href="/foo/1/1">Link 1.1</a>
</li>
</ul>
</li>
<li>
<a href="/foo/2">Link 2</a>
</li>
</ul>
HTML;
$this->assertMarkup($expected_output, $build);
$build = $view_builder->build('test-menu', 2);
$expected_output = <<< 'HTML'
<ul>
<li>
<a href="/foo/1/1">Link 1.1</a>
</li>
</ul>
HTML;
$this->assertMarkup($expected_output, $build);
$build = $view_builder->build('test-menu', 1, 1);
$expected_output = <<< 'HTML'
<ul>
<li>
<a href="/foo/1">Link 1</a>
</li>
<li>
<a href="/foo/2">Link 2</a>
</li>
</ul>
HTML;
$this->assertMarkup($expected_output, $build);
}
/**
* Asserts menu markup.
*/
private function assertMarkup(string $expected_markup, array $build): void {
$expected_markup = preg_replace('#\s{2,}#', '', $expected_markup);
$renderer = $this->container->get('renderer');
$actual_markup = preg_replace('#\s{2,}#', '', $renderer->renderPlain($build));
self::assertSame($expected_markup, $actual_markup);
}
}

145
tests/src/Kernel/RegionViewBuilderTest.php

@ -0,0 +1,145 @@
<?php
namespace Drupal\Tests\twig_tweak\Kernel;
use Drupal\block\Entity\Block;
use Drupal\Component\Utility\Html;
use Drupal\Core\Cache\Cache;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
/**
* A test for RegionViewBuilder.
*
* @group twig_tweak
*/
final class RegionViewBuilderTest extends KernelTestBase {
use UserCreationTrait;
/**
* {@inheritdoc}
*/
public static $modules = [
'twig_tweak',
'twig_tweak_test',
'user',
'system',
'block',
];
/**
* {@inheritdoc}
*/
public function setUp(): void {
parent::setUp();
$this->installEntitySchema('block');
$this->container->get('theme_installer')->install(['stable']);
$values = [
'id' => 'public_block',
'plugin' => 'system_powered_by_block',
'theme' => 'stable',
'region' => 'sidebar_first',
];
Block::create($values)->save();
$values = [
'id' => 'private_block',
'plugin' => 'system_powered_by_block',
'theme' => 'stable',
'region' => 'sidebar_first',
];
Block::create($values)->save();
}
/**
* Test callback.
*/
public function testRegionViewBuilder(): void {
$view_builder = $this->container->get('twig_tweak.region_view_builder');
$renderer = $this->container->get('renderer');
$build = $view_builder->build('sidebar_first');
// The build should be empty because 'stable' is not a default theme.
self::assertSame([], $build);
// Specify the theme name explicitly.
$build = $view_builder->build('sidebar_first', 'stable');
$expected_build = [
// Only public_block should be rendered.
// @see twig_tweak_test_block_access()
'public_block' => [
'#cache' =>
[
'keys' => [
'entity_view',
'block',
'public_block',
],
'contexts' => [],
'tags' => [
'block_view',
'config:block.block.public_block',
],
'max-age' => Cache::PERMANENT,
],
'#weight' => NULL,
'#lazy_builder' => [
'Drupal\\block\\BlockViewBuilder::lazyBuilder',
[
'public_block',
'full',
NULL,
],
],
],
'#region' => 'sidebar_first',
'#theme_wrappers' => ['region'],
// Even if the block is not accessible its cache metadata from access
// callback should be here.
'#cache' => [
'contexts' => ['user'],
'tags' => [
'config:block.block.public_block',
'tag_for_private_block',
'tag_for_public_block',
],
'max-age' => 123,
],
];
self::assertSame($expected_build, $build);
$expected_html = <<< 'HTML'
<div>
<div id="block-public-block" role="complementary">
<span>Powered by <a href="https://www.drupal.org">Drupal</a></span>
</div>
</div>
HTML;
$actual_html = $renderer->renderPlain($build);
self::assertSame(self::normalizeHtml($expected_html), self::normalizeHtml($actual_html));
// Set 'stable' as default site theme and check if the view builder without
// 'theme' argument returns the same result.
$this->container->get('config.factory')
->getEditable('system.theme')
->set('default', 'stable')
->save();
$build = $view_builder->build('sidebar_first');
self::assertSame($expected_build, $build);
Html::resetSeenIds();
$actual_html = $renderer->renderPlain($expected_build);
self::assertSame(self::normalizeHtml($expected_html), self::normalizeHtml($actual_html));
}
/**
* Normalizes the provided HTML.
*/
private static function normalizeHtml(string $html): string {
return rtrim(preg_replace(['#\s{2,}#', '#\n#'], '', $html));
}
}

19
tests/twig_tweak_test/config/install/block.block.classy_content.yml

@ -0,0 +1,19 @@
langcode: en
status: true
dependencies:
module:
- system
theme:
- classy
id: classy_content
theme: classy
region: content
weight: 0
provider: null
plugin: system_main_block
settings:
id: system_main_block
label: 'Main page content'
provider: system
label_display: '0'
visibility: { }

80
tests/twig_tweak_test/config/install/core.entity_form_display.node.page.default.yml

@ -0,0 +1,80 @@
langcode: en
status: true
dependencies:
config:
- field.field.node.page.body
- field.field.node.page.field_image
- field.field.node.page.field_media
- node.type.page
module:
- path
- text
id: node.page.default
targetEntityType: node
bundle: page
mode: default
content:
body:
type: text_textarea_with_summary
weight: 31
region: content
settings:
rows: 9
summary_rows: 3
placeholder: ''
show_summary: false
third_party_settings: { }
created:
type: datetime_timestamp
weight: 10
region: content
settings: { }
third_party_settings: { }
path:
type: path
weight: 30
region: content
settings: { }
third_party_settings: { }
promote:
type: boolean_checkbox
settings:
display_label: true
weight: 15
region: content
third_party_settings: { }
status:
type: boolean_checkbox
settings:
display_label: true
weight: 120
region: content
third_party_settings: { }
sticky:
type: boolean_checkbox
settings:
display_label: true
weight: 16
region: content
third_party_settings: { }
title:
type: string_textfield
weight: -5
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
uid:
type: entity_reference_autocomplete
weight: 5
region: content
settings:
match_operator: CONTAINS
match_limit: 10
size: 60
placeholder: ''
third_party_settings: { }
hidden:
field_image: true
field_media: true

8
tests/twig_tweak_test/config/install/core.entity_view_display.media.image.default.yml

@ -1,14 +1,12 @@
uuid: 42d67a69-5099-4c17-b27e-32080699966b
langcode: en langcode: en
status: true status: true
dependencies: dependencies:
config: config:
- field.field.media.image.field_media_image - field.field.media.image.field_media_image
- image.style.large
- media.type.image - media.type.image
module: module:
- image - image
_core:
default_config_hash: jOwnt_yq6AKAfqU6f0xKnxEkFQ2eTPJWxrk3WMLbL68
id: media.image.default id: media.image.default
targetEntityType: media targetEntityType: media
bundle: image bundle: image
@ -17,8 +15,8 @@ content:
field_media_image: field_media_image:
label: visually_hidden label: visually_hidden
settings: settings:
image_style: '' image_style: large
image_link: file image_link: ''
third_party_settings: { } third_party_settings: { }
type: image type: image
weight: 1 weight: 1

3
tests/twig_tweak_test/config/install/core.entity_view_display.media.remote_video.default.yml

@ -1,4 +1,3 @@
uuid: af776ed7-7db5-4670-b70b-4c94e06a6867
langcode: en langcode: en
status: true status: true
dependencies: dependencies:
@ -7,8 +6,6 @@ dependencies:
- media.type.remote_video - media.type.remote_video
module: module:
- media - media
_core:
default_config_hash: gUaDZKMQD3lmLKWPn727X3JHVdKJ525g4EJCCcDVAqk
id: media.remote_video.default id: media.remote_video.default
targetEntityType: media targetEntityType: media
bundle: remote_video bundle: remote_video

25
tests/twig_tweak_test/config/install/core.entity_view_display.node.page.default.yml

@ -1,4 +1,3 @@
uuid: 3fd16a0f-7a0d-46aa-8b80-2b1dc8484d09
langcode: en langcode: en
status: true status: true
dependencies: dependencies:
@ -8,11 +7,8 @@ dependencies:
- field.field.node.page.field_media - field.field.node.page.field_media
- node.type.page - node.type.page
module: module:
- image
- text - text
- user - user
_core:
default_config_hash: g1S3_GLaxq4l3I9RIca5Mlz02MxI2KmOquZpHw59akM
id: node.page.default id: node.page.default
targetEntityType: node targetEntityType: node
bundle: page bundle: page
@ -25,24 +21,9 @@ content:
region: content region: content
settings: { } settings: { }
third_party_settings: { } third_party_settings: { }
field_image:
weight: 102
label: above
settings:
image_style: ''
image_link: ''
third_party_settings: { }
type: image
region: content
field_media:
weight: 103
label: above
settings:
link: true
third_party_settings: { }
type: entity_reference_label
region: content
links: links:
weight: 101 weight: 101
region: content region: content
hidden: { } hidden:
field_image: true
field_media: true

31
tests/twig_tweak_test/config/install/core.entity_view_display.node.page.teaser.yml

@ -0,0 +1,31 @@
langcode: en
status: true
dependencies:
config:
- core.entity_view_mode.node.teaser
- field.field.node.page.body
- field.field.node.page.field_image
- field.field.node.page.field_media
- node.type.page
module:
- text
- user
id: node.page.teaser
targetEntityType: node
bundle: page
mode: teaser
content:
body:
label: hidden
type: text_summary_or_trimmed
weight: 100
region: content
settings:
trim_length: 600
third_party_settings: { }
links:
weight: 101
region: content
hidden:
field_image: true
field_media: true

3
tests/twig_tweak_test/config/install/field.field.media.image.field_media_image.yml

@ -1,4 +1,3 @@
uuid: 31cfb877-de82-4020-b0f7-9ec72592495f
langcode: en langcode: en
status: true status: true
dependencies: dependencies:
@ -10,8 +9,6 @@ dependencies:
- media - media
module: module:
- image - image
_core:
default_config_hash: pzPA-2JwyxlJ3qMb4L9viAnhNhbEhl2couH8A3FO020
id: media.image.field_media_image id: media.image.field_media_image
field_name: field_media_image field_name: field_media_image
entity_type: media entity_type: media

3
tests/twig_tweak_test/config/install/field.field.media.remote_video.field_media_oembed_video.yml

@ -1,12 +1,9 @@
uuid: 83889a69-28e6-470c-bf31-4437a5a2c1cf
langcode: en langcode: en
status: true status: true
dependencies: dependencies:
config: config:
- field.storage.media.field_media_oembed_video - field.storage.media.field_media_oembed_video
- media.type.remote_video - media.type.remote_video
_core:
default_config_hash: Eo4HHenV5iZat_kEWgr_wydD3TgwURMCzwt-7qIEyoM
id: media.remote_video.field_media_oembed_video id: media.remote_video.field_media_oembed_video
field_name: field_media_oembed_video field_name: field_media_oembed_video
entity_type: media entity_type: media

4
tests/twig_tweak_test/config/install/field.field.node.page.body.yml

@ -1,4 +1,3 @@
uuid: 42500952-ebb3-46c2-9a86-ed2f6c4000c1
langcode: en langcode: en
status: true status: true
dependencies: dependencies:
@ -7,8 +6,6 @@ dependencies:
- node.type.page - node.type.page
module: module:
- text - text
_core:
default_config_hash: rUop-8b6hvxxDYbv-KobTfNIBNbPY9qOPl8f6kBNSpw
id: node.page.body id: node.page.body
field_name: body field_name: body
entity_type: node entity_type: node
@ -21,4 +18,5 @@ default_value: { }
default_value_callback: '' default_value_callback: ''
settings: settings:
display_summary: true display_summary: true
required_summary: false
field_type: text_with_summary field_type: text_with_summary

1
tests/twig_tweak_test/config/install/field.field.node.page.field_image.yml

@ -1,4 +1,3 @@
uuid: 0f21b708-4832-4777-91ea-236dddea0236
langcode: en langcode: en
status: true status: true
dependencies: dependencies:

1
tests/twig_tweak_test/config/install/field.field.node.page.field_media.yml

@ -1,4 +1,3 @@
uuid: cc6b99d7-25b6-4d90-a65e-4021861ec6af
langcode: en langcode: en
status: true status: true
dependencies: dependencies:

3
tests/twig_tweak_test/config/install/field.storage.media.field_media_image.yml

@ -1,4 +1,3 @@
uuid: e8eb0ecd-b286-4d09-9152-fcc1e7868f17
langcode: en langcode: en
status: true status: true
dependencies: dependencies:
@ -9,8 +8,6 @@ dependencies:
- file - file
- image - image
- media - media
_core:
default_config_hash: 7ZBrcl87ZXaw42v952wwcw_9cQgTBq5_5tgyUkE-VV0
id: media.field_media_image id: media.field_media_image
field_name: field_media_image field_name: field_media_image
entity_type: media entity_type: media

3
tests/twig_tweak_test/config/install/field.storage.media.field_media_oembed_video.yml

@ -1,11 +1,8 @@
uuid: d879f1f4-2730-47bb-9f38-df6c265855b9
langcode: en langcode: en
status: true status: true
dependencies: dependencies:
module: module:
- media - media
_core:
default_config_hash: SJgxR5XWOesQbEKqp8VgInPyJjCFU_t2pi7UzYB78xg
id: media.field_media_oembed_video id: media.field_media_oembed_video
field_name: field_media_oembed_video field_name: field_media_oembed_video
entity_type: media entity_type: media

3
tests/twig_tweak_test/config/install/field.storage.node.field_image.yml

@ -1,4 +1,3 @@
uuid: 7d45d63c-3a03-4a65-8103-b12e70c69948
langcode: en langcode: en
status: true status: true
dependencies: dependencies:
@ -6,8 +5,6 @@ dependencies:
- file - file
- image - image
- node - node
_core:
default_config_hash: SkXIPKZYiIMMtnBmfnxk58RYfbZ8cHSw5NZPY_JByME
id: node.field_image id: node.field_image
field_name: field_image field_name: field_image
entity_type: node entity_type: node

1
tests/twig_tweak_test/config/install/field.storage.node.field_media.yml

@ -1,4 +1,3 @@
uuid: 8301cf4c-fba5-440b-8323-9a0019c12d34
langcode: en langcode: en
status: true status: true
dependencies: dependencies:

2
tests/twig_tweak_test/config/install/filter.format.twig_tweak_test.yml

@ -1,7 +1,7 @@
langcode: en langcode: en
status: true status: true
dependencies: { } dependencies: { }
name: Twig tweak test name: 'Twig tweak test'
format: twig_tweak_test format: twig_tweak_test
weight: 0 weight: 0
filters: filters:

3
tests/twig_tweak_test/config/install/media.type.image.yml

@ -1,9 +1,6 @@
uuid: 6ef5b8e7-988c-4057-8a44-55fa195fef14
langcode: en langcode: en
status: true status: true
dependencies: { } dependencies: { }
_core:
default_config_hash: 6Qope5wG7HUpV0tPOBMtDI_GZkHFcF1Xj4hgD9Cu_hM
id: image id: image
label: Image label: Image
description: 'Use local images for reusable media.' description: 'Use local images for reusable media.'

5
tests/twig_tweak_test/config/install/media.type.remote_video.yml

@ -1,15 +1,12 @@
uuid: 1b4d7544-7b5e-47d2-b79e-d50d6b2442a0
langcode: en langcode: en
status: true status: true
dependencies: { } dependencies: { }
_core:
default_config_hash: d_nPD2eMknkYAnSTV4FkaqijceyFJPwT5i_Ih0lEEtc
id: remote_video id: remote_video
label: 'Remote video' label: 'Remote video'
description: 'A remotely hosted video from YouTube or Vimeo.' description: 'A remotely hosted video from YouTube or Vimeo.'
source: 'oembed:video' source: 'oembed:video'
queue_thumbnail_downloads: false queue_thumbnail_downloads: false
new_revision: false new_revision: true
source_configuration: source_configuration:
thumbnails_directory: 'public://oembed_thumbnails' thumbnails_directory: 'public://oembed_thumbnails'
providers: providers:

1
tests/twig_tweak_test/config/install/node.type.page.yml

@ -1,4 +1,3 @@
uuid: 963c690a-b7c3-48dd-bc35-351857b9a7ce
langcode: en langcode: en
status: true status: true
dependencies: { } dependencies: { }

2
tests/twig_tweak_test/config/install/system.menu.twig-tweak-test.yml

@ -2,6 +2,6 @@ langcode: en
status: true status: true
dependencies: { } dependencies: { }
id: twig-tweak-test id: twig-tweak-test
label: Twig tweak test label: 'Twig tweak test'
description: '' description: ''
locked: false locked: false

3
tests/twig_tweak_test/config/install/views.view.twig_tweak_test.yml

@ -11,7 +11,6 @@ description: ''
tag: '' tag: ''
base_table: node_field_data base_table: node_field_data
base_field: nid base_field: nid
core: 8.x
display: display:
default: default:
display_plugin: default display_plugin: default
@ -181,7 +180,7 @@ display:
position: 1 position: 1
display_options: display_options:
display_extenders: { } display_extenders: { }
path: twig-tweak-test path: example
cache_metadata: cache_metadata:
max-age: -1 max-age: -1
contexts: contexts:

17
tests/twig_tweak_test/src/Controller/TwigTweakTestController.php

@ -0,0 +1,17 @@
<?php
namespace Drupal\twig_tweak_test\Controller;
/**
* Returns responses for Twig Tweak Test routes.
*/
final class TwigTweakTestController {
/**
* Builds the response.
*/
public function build(): array {
return ['#theme' => 'twig_tweak_test'];
}
}

15
tests/twig_tweak_test/src/Plugin/Block/FooBlock.php

@ -15,12 +15,19 @@ use Drupal\Core\Session\AccountInterface;
* category = @Translation("Twig Tweak") * category = @Translation("Twig Tweak")
* ) * )
*/ */
class FooBlock extends BlockBase { final class FooBlock extends BlockBase {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
protected function blockAccess(AccountInterface $account) { public function defaultConfiguration(): array {
return ['content' => 'Foo'];
}
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account): AccessResult {
$result = AccessResult::allowedIf($account->getAccountName() == 'User 1'); $result = AccessResult::allowedIf($account->getAccountName() == 'User 1');
$result->addCacheTags(['tag_from_' . __FUNCTION__]); $result->addCacheTags(['tag_from_' . __FUNCTION__]);
$result->setCacheMaxAge(35); $result->setCacheMaxAge(35);
@ -31,9 +38,9 @@ class FooBlock extends BlockBase {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function build() { public function build(): array {
return [ return [
'#markup' => 'Foo', '#markup' => $this->getConfiguration()['content'],
'#cache' => [ '#cache' => [
'contexts' => ['url'], 'contexts' => ['url'],
'tags' => ['tag_from_' . __FUNCTION__], 'tags' => ['tag_from_' . __FUNCTION__],

12
tests/twig_tweak_test/templates/twig-tweak-test.html.twig

@ -1,9 +1,16 @@
{% set image_attributes = {style: 'width: 30px; height 30px;'} %} {% set image_attributes = {style: 'width: 30px; height 30px;'} %}
<style> <style>
main {
background-color: lightyellow;
padding: 15px;
border: double 3px darkgrey;
}
.tt-test > div { .tt-test > div {
margin: 15px; margin: 15px auto;
padding: 10px; padding: 10px;
outline: solid 2px dodgerblue; outline: solid 2px dodgerblue;
max-width: 1200px;
background-color: white;
} }
.tt-test > div::before { .tt-test > div::before {
content: attr(class); content: attr(class);
@ -26,7 +33,6 @@
<div class="tt-region">{{ drupal_region('sidebar_first') }}</div> <div class="tt-region">{{ drupal_region('sidebar_first') }}</div>
<div class="tt-entity-default">{{ drupal_entity('node', 1) }}</div> <div class="tt-entity-default">{{ drupal_entity('node', 1) }}</div>
<div class="tt-entity-teaser">{{ drupal_entity('node', 1, 'teaser') }}</div> <div class="tt-entity-teaser">{{ drupal_entity('node', 1, 'teaser') }}</div>
<div class="tt-entity-from-url">{{ drupal_entity('node') }}</div>
<div class="tt-entity-add-form">{{ drupal_entity_form('node', values={type: 'page'}) }}</div> <div class="tt-entity-add-form">{{ drupal_entity_form('node', values={type: 'page'}) }}</div>
<div class="tt-entity-edit-form">{{ drupal_entity_form('node', 1) }}</div> <div class="tt-entity-edit-form">{{ drupal_entity_form('node', 1) }}</div>
<div class="tt-field">{{ drupal_field('body', 'node', 1) }}</div> <div class="tt-field">{{ drupal_field('body', 'node', 1) }}</div>
@ -52,7 +58,7 @@
<div class="tt-messages">{{ drupal_messages() }}</div> <div class="tt-messages">{{ drupal_messages() }}</div>
<div class="tt-breadcrumb">{{ drupal_breadcrumb() }}</div> <div class="tt-breadcrumb">{{ drupal_breadcrumb() }}</div>
<div class="tt-link-access">{{ drupal_link('Administration', 'admin', {absolute: true}, true) }}</div> <div class="tt-link-access">{{ drupal_link('Administration', 'admin', {absolute: true}, true) }}</div>
<div class="tt-contextual-links">{{ contextual_links('node:node=1') }}</div> <div class="tt-contextual-links">{{ drupal_contextual_links('node:node=1') }}</div>
<div class="tt-token-replace">{{ 'Site name: [site:name]'|token_replace }}</div> <div class="tt-token-replace">{{ 'Site name: [site:name]'|token_replace }}</div>
<div class="tt-preg-replace">{{ 'FOO'|preg_replace('/(foo)/i', '$1-bar') }}</div> <div class="tt-preg-replace">{{ 'FOO'|preg_replace('/(foo)/i', '$1-bar') }}</div>
<div class="tt-image-style">{{ 'public://images/ocean.jpg'|image_style('thumbnail') }}</div> <div class="tt-image-style">{{ 'public://images/ocean.jpg'|image_style('thumbnail') }}</div>

9
tests/twig_tweak_test/twig_tweak_test.info.yml

@ -2,10 +2,11 @@ name: Twig tweak test
type: module type: module
description: Support module for Twig tweak testing. description: Support module for Twig tweak testing.
package: Testing package: Testing
core: 8.x core_version_requirement: ^9
core_version_requirement: ^8 || ^9
dependencies: dependencies:
- drupal:system (>= 8.7.0) - drupal:system (>= 9.0)
- drupal:node - drupal:block
- drupal:media - drupal:media
- drupal:node
- drupal:path
- drupal:twig_tweak - drupal:twig_tweak

48
tests/twig_tweak_test/twig_tweak_test.module

@ -5,42 +5,70 @@
* Primary module hooks for Twig Tweak test module. * Primary module hooks for Twig Tweak test module.
*/ */
use Drupal\block\BlockInterface;
use Drupal\Core\Access\AccessResult; use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\file\FileInterface;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface; use Drupal\node\NodeInterface;
/** /**
* Implements hook_page_bottom(). * Implements hook_page_bottom().
*/ */
function twig_tweak_test_page_bottom(array &$page_bottom) { function twig_tweak_test_page_bottom(): void {
\Drupal::service('messenger')->addMessage('Hello world!'); Drupal::service('messenger')->addMessage('Hello world!');
$page_bottom['twig_tweak_test']['#theme'] = 'twig_tweak_test';
} }
/** /**
* Implements hook_theme(). * Implements hook_theme().
*/ */
function twig_tweak_test_theme() { function twig_tweak_test_theme(): array {
return ['twig_tweak_test' => ['variables' => []]]; return ['twig_tweak_test' => ['variables' => []]];
} }
/** /**
* Prepares variables for twig-tweak-test template. * Prepares variables for twig-tweak-test template.
*/ */
function template_preprocess_twig_tweak_test(&$vars) { function template_preprocess_twig_tweak_test(array &$vars): void {
$vars['node'] = Drupal::routeMatch()->getParameter('node'); $vars['node'] = Node::load(1);
}
/**
* Implements hook_block_access().
*
* @see \Drupal\Tests\twig_tweak\Kernel\RegionViewBuilderTest
*/
function twig_tweak_test_block_access(BlockInterface $block): AccessResultInterface {
$result = AccessResult::forbiddenIf($block->id() == 'private_block');
$result->cachePerUser();
$result->addCacheTags(['tag_for_' . $block->id()]);
$result->setCacheMaxAge(123);
return $result;
} }
/** /**
* Implements hook_node_access(). * Implements hook_node_access().
* *
* @see \Drupal\Tests\twig_tweak\Kernel\AccessTest * @see \Drupal\Tests\twig_tweak\Kernel\EntityViewBuilderTest
*/ */
function twig_tweak_test_node_access(NodeInterface $node) { function twig_tweak_test_node_access(NodeInterface $node): AccessResultInterface {
if ($node->getTitle() == 'Entity access test') { $result = AccessResult::forbiddenIf($node->getTitle() == 'Private node');
$result = AccessResult::allowed();
$result->addCacheTags(['tag_from_' . __FUNCTION__]); $result->addCacheTags(['tag_from_' . __FUNCTION__]);
$result->cachePerUser(); $result->cachePerUser();
$result->setCacheMaxAge(50); $result->setCacheMaxAge(50);
return $result; return $result;
} }
/**
* Implements hook_file_access().
*
* @see \Drupal\Tests\twig_tweak\Kernel\ImageViewBuilderTest
*/
function twig_tweak_test_file_access(FileInterface $file): AccessResultInterface {
$is_public = parse_url($file->getFileUri(), PHP_URL_SCHEME) == 'public';
$result = AccessResult::allowedIf($is_public);
$result->cachePerUser();
$result->addCacheTags(['tag_for_' . $file->getFileUri()]);
$result->setCacheMaxAge(70);
return $result;
} }

7
tests/twig_tweak_test/twig_tweak_test.routing.yml

@ -0,0 +1,7 @@
twig_tweak_test.test:
path: 'twig-tweak-test'
defaults:
_title: 'Twig Tweak Test'
_controller: '\Drupal\twig_tweak_test\Controller\TwigTweakTestController::build'
requirements:
_permission: 'access content'

5
twig_tweak.info.yml

@ -1,7 +1,6 @@
name: Twig Tweak name: Twig Tweak
type: module type: module
description: Provides some extra Twig functions and filters. description: Provides some extra Twig functions and filters.
core: 8.x core_version_requirement: ^9
core_version_requirement: ^8 || ^9
dependencies: dependencies:
- drupal:system (>=8.7) - drupal:system (>=9.0)

29
twig_tweak.services.yml

@ -1,5 +1,32 @@
services: services:
twig_tweak.twig_extension: twig_tweak.twig_extension:
class: Drupal\twig_tweak\TwigExtension class: Drupal\twig_tweak\TwigTweakExtension
tags: tags:
- { name: twig.extension } - { name: twig.extension }
twig_tweak.block_view_builder:
class: Drupal\twig_tweak\View\BlockViewBuilder
arguments: ['@plugin.manager.block', '@context.repository', '@context.handler', '@current_user', '@request_stack', '@current_route_match', '@title_resolver']
twig_tweak.region_view_builder:
class: Drupal\twig_tweak\View\RegionViewBuilder
arguments: ['@entity_type.manager', '@config.factory', '@request_stack', '@title_resolver']
twig_tweak.entity_view_builder:
class: Drupal\twig_tweak\View\EntityViewBuilder
arguments: ['@entity_type.manager']
twig_tweak.entity_form_view_builder:
class: Drupal\twig_tweak\View\EntityFormViewBuilder
arguments: ['@entity.form_builder']
twig_tweak.field_view_builder:
class: Drupal\twig_tweak\View\FieldViewBuilder
arguments: ['@entity.repository']
twig_tweak.menu_view_builder:
class: Drupal\twig_tweak\View\MenuViewBuilder
arguments: ['@menu.link_tree']
twig_tweak.image_view_builder:
class: Drupal\twig_tweak\View\ImageViewBuilder

Loading…
Cancel
Save