Compare commits
115 Commits
Author | SHA1 | Date |
---|---|---|
Thomas Frobieter | c096aece9f | 9 months ago |
Chi | 4b72a2c3da | 9 months ago |
Julian Pustkuchen | 898b4521d1 | 9 months ago |
James Shields | b3947e26b7 | 9 months ago |
Chi | ed7ae10bc6 | 10 months ago |
Chi | 34ce474c21 | 10 months ago |
Chi | 9e1594ce42 | 10 months ago |
Chi | 81ac9123dd | 10 months ago |
Jeff Mann | 5ad73a7494 | 10 months ago |
Chi | 740ce17fd2 | 10 months ago |
Kyrylo Loboda | 4111163f51 | 10 months ago |
Chi | ce34d528ae | 10 months ago |
prashantdsala | 91992ea10d | 10 months ago |
Chi | 41144dddb5 | 10 months ago |
Chi | 271e723d67 | 10 months ago |
Julian Pustkuchen | 248ea4d7db | 10 months ago |
Reinhard Hutter | f2dbdeaa24 | 1 year ago |
Bill Seremetis | 4a4cf380c5 | 1 year ago |
Akshay Singh | a165bdd94e | 1 year ago |
Julian Pustkuchen | d7c8b28690 | 1 year ago |
Julian Pustkuchen | 2e11f9b7ad | 1 year ago |
Julian Pustkuchen | adcf39754b | 1 year ago |
Alison Jo | 832a021ccb | 1 year ago |
Julian Pustkuchen | 0aea215552 | 1 year ago |
Chi | 25155dde83 | 1 year ago |
Chi | d77a4f7b8a | 2 years ago |
Samata soni | a0c6fdc056 | 2 years ago |
Chi | 2c70fa668f | 2 years ago |
Daniel Korte | 75c512a6ca | 2 years ago |
Chi | 6073d18a46 | 2 years ago |
Chi | 8b33f99a8f | 2 years ago |
Chi | 8aa63ea1f2 | 2 years ago |
JeroenT | 3e1a888711 | 2 years ago |
Chi | b13ba8b707 | 2 years ago |
Barry Fisher | b36c208cc2 | 2 years ago |
Ranjith Kumar K U | 2de249ba8c | 2 years ago |
Chi | f2805a1cc1 | 2 years ago |
Anybody | 848e2a8a63 | 2 years ago |
Chi | b25684f98a | 2 years ago |
Chi | 0883b1594e | 2 years ago |
Chi | f7a9c7d997 | 2 years ago |
Jasper Lammens | 27b50a6cb6 | 3 years ago |
kimberllyAmaral | 3fd384e455 | 3 years ago |
Chi | 530adc97b8 | 3 years ago |
Chi | 8dedaf4e61 | 3 years ago |
hudri | 63947e19ad | 3 years ago |
gpotter | fd7ee18be2 | 3 years ago |
Chi | 4037c5f7cf | 3 years ago |
kristiaanvandeneynde | 7a8e01b9b9 | 3 years ago |
Chi | d4a7cf8af5 | 3 years ago |
Chi | c853e4027a | 3 years ago |
Chi | 0014fee04c | 3 years ago |
Chi | 700d6c4ef9 | 3 years ago |
Chi | 3b17ed3500 | 3 years ago |
Juan Peña | 1aad751621 | 3 years ago |
sonfd | 58ca88b667 | 3 years ago |
Chi | 22adcf19b9 | 4 years ago |
xandeadx | c336ca8a19 | 4 years ago |
Chi | 36bb935644 | 4 years ago |
WilfredWaltman | 04a07da602 | 4 years ago |
Chi | 4bbf7f4786 | 4 years ago |
JeroenT | a4531aefe3 | 4 years ago |
JeroenT | 7eb023dd00 | 4 years ago |
Chi | b0d20f011d | 4 years ago |
git | f00f28dd05 | 4 years ago |
Chi | d3ac495203 | 4 years ago |
Chi | e364f07c26 | 4 years ago |
Chi | c6ce6aeea9 | 4 years ago |
Chi | bf226278b0 | 4 years ago |
Chi | 7ec37ae520 | 4 years ago |
Chi | d1f528277c | 4 years ago |
Chi | 2e6361e496 | 4 years ago |
Chi | 2345de8977 | 4 years ago |
Chi | 6b71d8e16d | 4 years ago |
Chi | 9bee786c06 | 4 years ago |
Chi | cde44d9979 | 4 years ago |
Chi | c50c2dba28 | 4 years ago |
Chi | 36ec6eedc0 | 4 years ago |
Chi | f2ba590e83 | 4 years ago |
Chi | d4d6f763b4 | 4 years ago |
Chi | ecb1fd81d3 | 4 years ago |
Chi | 7b45fedc79 | 4 years ago |
Chi | 2149c32ed2 | 4 years ago |
Chi | f3576306f3 | 4 years ago |
Chi | 326c45b0fa | 4 years ago |
Chi | 993f69540d | 4 years ago |
Niklan | 2fd6b6c5b9 | 4 years ago |
Chi | e6f49368b6 | 4 years ago |
Panchuk | 1f5da25547 | 4 years ago |
Chi | 62e3cdd7b7 | 4 years ago |
aspilicious | 30472b7b1c | 4 years ago |
Chi | 3aad1eb11a | 4 years ago |
Chi | 78856ccea6 | 4 years ago |
Chi | 7cf8595b49 | 4 years ago |
Chi | 8ab073bf37 | 4 years ago |
Chi | 826cbd5e4c | 4 years ago |
Chi | 596951b0ef | 4 years ago |
mdupont | f3bfc6d7ce | 5 years ago |
Chi | 47e2509afc | 5 years ago |
Chi | 001f4a3657 | 5 years ago |
mdupont | 54aed14429 | 5 years ago |
Chi | 786963c0ed | 5 years ago |
Chi | 904b13614e | 5 years ago |
Chi | a45e6182b5 | 5 years ago |
Chi | ca644ba769 | 5 years ago |
Chi | 3f3633dcfe | 5 years ago |
Chi | 790cc4c261 | 5 years ago |
Chi | 14e6be65b2 | 5 years ago |
Chi | fa48bc4895 | 5 years ago |
Chi | a1bd01c6df | 5 years ago |
NesleeCanilPinto | 2610bbfcff | 5 years ago |
Chi | d20dc4e2b8 | 5 years ago |
Chi | 05f6c338a7 | 5 years ago |
Chi | 3f80912345 | 5 years ago |
Chi | d15f246bd9 | 5 years ago |
72 changed files with 5175 additions and 1449 deletions
@ -0,0 +1,9 @@ |
|||||||
|
include: |
||||||
|
- project: $_GITLAB_TEMPLATES_REPO |
||||||
|
ref: $_GITLAB_TEMPLATES_REF |
||||||
|
file: |
||||||
|
- '/includes/include.drupalci.main.yml' |
||||||
|
- '/includes/include.drupalci.variables.yml' |
||||||
|
- '/includes/include.drupalci.workflows.yml' |
||||||
|
variables: |
||||||
|
CORE_PHP_MIN: '8.1' |
@ -0,0 +1,23 @@ |
|||||||
|
# Twig Tweak |
||||||
|
|
||||||
|
The Twig Tweak module provides a Twig extension with some useful functions |
||||||
|
and filters that can improve development experience. |
||||||
|
|
||||||
|
Some functions and filters are built in the Twig Tweak module, while others are |
||||||
|
simple wrappers to integrate Drupal functions and services into Twig (for |
||||||
|
example: `format_size`, `drupal_view_result`). |
||||||
|
|
||||||
|
## Requirements |
||||||
|
|
||||||
|
This module requires no modules outside of Drupal core. |
||||||
|
|
||||||
|
## Installation |
||||||
|
|
||||||
|
- Install as you would normally install a contributed Drupal module. For further |
||||||
|
information, see [Installing Drupal Modules](https://www.drupal.org/docs/extending-drupal/installing-drupal-modules). |
||||||
|
|
||||||
|
## Links |
||||||
|
|
||||||
|
- Project page: https://www.drupal.org/project/twig_tweak |
||||||
|
- Twig home page: https://twig.symfony.com/ |
||||||
|
- Drupal Twig documentation: https://www.drupal.org/docs/theming-drupal/twig-in-drupal |
@ -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 |
|
@ -1,18 +1,29 @@ |
|||||||
{ |
{ |
||||||
"name": "drupal/twig_tweak", |
"name": "drupal/twig_tweak", |
||||||
"type": "drupal-module", |
"type": "drupal-module", |
||||||
"description": "A Twig extension with some useful functions and filters for Drupal development.", |
"description": "A Twig extension with some useful functions and filters for Drupal development.", |
||||||
"keywords": ["Drupal", "Twig"], |
"keywords": ["Drupal", "Twig"], |
||||||
"license": "GPL-2.0+", |
"license": "GPL-2.0-or-later", |
||||||
"homepage": "https://www.drupal.org/project/twig_tweak", |
"homepage": "https://www.drupal.org/project/twig_tweak", |
||||||
"support": { |
"support": { |
||||||
"issues": "https://www.drupal.org/project/issues/twig_tweak", |
"issues": "https://www.drupal.org/project/issues/twig_tweak", |
||||||
"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", |
||||||
}, |
"ext-json": "*", |
||||||
"suggest": { |
"drupal/core": "^9.3 || ^10.0", |
||||||
"symfony/var-dumper": "Better dump() function for debugging Twig variables" |
"twig/twig": "^2.15.3 || ^3.4.3", |
||||||
} |
"symfony/polyfill-php80": "^1.17" |
||||||
|
}, |
||||||
|
"suggest": { |
||||||
|
"symfony/var-dumper": "Better dump() function for debugging Twig variables" |
||||||
|
}, |
||||||
|
"extra": { |
||||||
|
"drush": { |
||||||
|
"services": { |
||||||
|
"drush.services.yml": "^9 || ^10 || ^11" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
} |
} |
||||||
|
@ -0,0 +1,6 @@ |
|||||||
|
# Twig Tweak documentation |
||||||
|
|
||||||
|
- [Cheat sheet](cheat-sheet.md) |
||||||
|
- [Rendering blocks with Twig Tweak](blocks.md) |
||||||
|
- [Twig Tweak and Views](views.md) |
||||||
|
- [Migrating to Twig Tweak 3.x](migration-to-3.x.md) |
@ -0,0 +1,126 @@ |
|||||||
|
# Rendering blocks with Twig Tweak |
||||||
|
|
||||||
|
This subject is rather confusing because too many things in Drupal are referred |
||||||
|
to as "Blocks". So it is essential to understand what kind of block you are |
||||||
|
going to render. This guide covers three main cases you may deal when rendering |
||||||
|
blocks in a Twig template. |
||||||
|
|
||||||
|
## Block - plugin |
||||||
|
Technically speaking block plugin is a PHP class with a special annotation. See |
||||||
|
[Branding block plugin](https://git.drupalcode.org/project/drupal/-/blob/9.1.0/core/modules/system/src/Plugin/Block/SystemBrandingBlock.php#L16-22) as an example. |
||||||
|
|
||||||
|
The simplest way to render block plugin is as follows. |
||||||
|
```twig |
||||||
|
{{ drupal_block('plugin_id') }} |
||||||
|
``` |
||||||
|
|
||||||
|
Optionally you can pass block label and plugin configuration in the second |
||||||
|
parameter. |
||||||
|
```twig |
||||||
|
{{ drupal_block('plugin_id', {label: 'Example'|t, some_setting: 'example', setting_array: {value: value}}) }} |
||||||
|
``` |
||||||
|
|
||||||
|
By default, blocks are rendered using `block.html.twig` template. This can be |
||||||
|
turned off by setting wrapper parameter to false. |
||||||
|
```twig |
||||||
|
{{ drupal_block('plugin_id', wrapper=false) }} |
||||||
|
``` |
||||||
|
|
||||||
|
The tricky thing here is figuring out block plugin ID. If you know which module |
||||||
|
provides a particular plugin, you can find its PHP class under the |
||||||
|
`MODULE_NAME/src/Plugin/Block` directory and locate the ID in the class |
||||||
|
annotation. For instance the plugin ID of login block can be found in the |
||||||
|
following file: `core/modules/user/src/Plugin/Block/UserLoginBlock.php`. When |
||||||
|
using the plugin ID, convert its format to snake_case (meaning the words are |
||||||
|
lowercase and separated by underscores e.g. `system_branding_block`). |
||||||
|
|
||||||
|
To look up all core block plugins use grep search. |
||||||
|
|
||||||
|
```shell |
||||||
|
grep -r ' id = ' core/modules/*/src/Plugin/Block/;' |
||||||
|
``` |
||||||
|
|
||||||
|
However, this does not work for block types that are defined dynamically using |
||||||
|
plugin derivatives (like views blocks). |
||||||
|
|
||||||
|
The best way to get all registered plugin IDs is fetching them with block plugin |
||||||
|
manager |
||||||
|
```shell |
||||||
|
drush ev "print_r(array_keys(\Drupal::service('plugin.manager.block')->getDefinitions()));" |
||||||
|
``` |
||||||
|
|
||||||
|
Note that the plugin_id needs to be wrapped in quotes. For example, |
||||||
|
```twig |
||||||
|
{{ drupal_block('system_breadcrumb_block') }} |
||||||
|
``` |
||||||
|
|
||||||
|
## Block - configuration entity |
||||||
|
This is what we configure on `admin/structure/block` page. It's important to know |
||||||
|
that eventually these entities are rendered using block plugins described above. |
||||||
|
The purpose of the configuration entities is to store plugin IDs and |
||||||
|
configuration. Additionally, they reference theme and region where a block |
||||||
|
should be printed, but this data are not used when rendering through Twig Tweak. |
||||||
|
|
||||||
|
So having configured a block through administrative interface you can print it |
||||||
|
using the following code. |
||||||
|
```twig |
||||||
|
{{ drupal_entity('block', 'block_id') }} |
||||||
|
``` |
||||||
|
|
||||||
|
Disabled blocks won't be printed unless you suppress access control as follows. |
||||||
|
|
||||||
|
```twig |
||||||
|
{{ drupal_entity('block', 'block_id', check_access=false) }} |
||||||
|
``` |
||||||
|
|
||||||
|
Note that block_id here has nothing to do with 'block_plugin_id' we discussed |
||||||
|
before. It is an ID (machine_name) of block configuration entity. You may copy |
||||||
|
it from the block configuration form. |
||||||
|
|
||||||
|
The following Drush command will list all available block entities. |
||||||
|
```shell |
||||||
|
drush ev 'print_r(\Drupal::configFactory()->listAll("block.block."));' |
||||||
|
``` |
||||||
|
|
||||||
|
## Block - content entity |
||||||
|
Content blocks, also known as custom blocks are configured on |
||||||
|
`admin/structure/block/block-content` page. Actually they have little to do with |
||||||
|
Drupal block system. These blocks are just content entities like node, user, |
||||||
|
comment and so on. Their provider (Custom block module) also offers a plugin to |
||||||
|
display them in blocks. |
||||||
|
|
||||||
|
The primary way to display content blocks is like follows. |
||||||
|
```twig |
||||||
|
{{ drupal_entity('block_content', 'content_block_id') }} |
||||||
|
``` |
||||||
|
|
||||||
|
Though it looks similar to rendering configuration entities (Section 2), you |
||||||
|
should note two important distinctions. |
||||||
|
|
||||||
|
Entity type is 'block_content' not 'block'. |
||||||
|
Content block ID stands for an ID of respective content entity. This is a |
||||||
|
numeric value that can be found in URL when editing custom block. Getting |
||||||
|
content block IDs is as simple as executing a single SQL query. |
||||||
|
```shell |
||||||
|
drush sqlq 'SELECT id, info FROM block_content_field_data' |
||||||
|
``` |
||||||
|
|
||||||
|
Since this method does not use block template (`block.html.twig`) you may need to |
||||||
|
supply block subject and wrappers manually. |
||||||
|
```twig |
||||||
|
<div class="block"> |
||||||
|
<h2>{{ 'Example'|t }}</h2> |
||||||
|
{{ drupal_entity('block_content', content_block_id) }} |
||||||
|
</div> |
||||||
|
``` |
||||||
|
|
||||||
|
Another way to accomplish this task is using block plugin (see Section 1). |
||||||
|
```twig |
||||||
|
{{ drupal_block('block_content:<uuid>', {label: 'Example'|t}) }} |
||||||
|
``` |
||||||
|
|
||||||
|
Note that plugin ID in this case consists of entity type and entity UUID |
||||||
|
separated by a colon. |
||||||
|
|
||||||
|
It is also possible to create a configuration entity for this content block and |
||||||
|
print it as described in [Configuration entity section](#block-configuration-entity). |
@ -0,0 +1,403 @@ |
|||||||
|
# Cheat sheet |
||||||
|
|
||||||
|
## Drupal View |
||||||
|
```twig |
||||||
|
{{ drupal_view('who_s_new', 'block_1') }} |
||||||
|
``` |
||||||
|
```twig |
||||||
|
{# Specify additional parameters which map to contextual filters you have configured in your view. #} |
||||||
|
{{ drupal_view('who_s_new', 'block_1', arg_1, arg_2, arg_3) }} |
||||||
|
``` |
||||||
|
|
||||||
|
## Drupal View Result |
||||||
|
Checks results for a given view. Note that the results themselves are not printable. |
||||||
|
```twig |
||||||
|
{% if drupal_view_result('cart')|length == 0 %} |
||||||
|
{{ 'Your cart is empty.'|t }} |
||||||
|
{% endif %} |
||||||
|
``` |
||||||
|
|
||||||
|
## Drupal Block |
||||||
|
In order to figure out the plugin IDs list them using block plugin manager. |
||||||
|
With Drush, it can be done like follows: |
||||||
|
```shell |
||||||
|
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, id}) }} |
||||||
|
|
||||||
|
{# Bypass block.html.twig theming. #} |
||||||
|
{{ drupal_block('system_branding_block', wrapper=false) }} |
||||||
|
|
||||||
|
{# For block plugin that has a required context supply a context mapping to tell the block instance where to get that context from. #} |
||||||
|
{{ drupal_block('plugin_id', {context_mapping: {node: '@node.node_route_context:node'}}) }} |
||||||
|
``` |
||||||
|
|
||||||
|
See [rendering blocks with Twig Tweak](blocks.md#block-plugin) for details. |
||||||
|
|
||||||
|
## 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 |
||||||
|
Note that drupal_field() does not work for view modes powered by Layout Builder. |
||||||
|
```twig |
||||||
|
{# Render field_image from node 1 in view_mode "full" (default). #} |
||||||
|
{{ drupal_field('field_image', 'node', 1) }} |
||||||
|
|
||||||
|
{# Render field_image from node 1 in view_mode "teaser". #} |
||||||
|
{{ drupal_field('field_image', 'node', 1, 'teaser') }} |
||||||
|
|
||||||
|
{# Render field_image from node 1 and instead of a view mode, provide an array of display options. #} |
||||||
|
{# @see https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Entity!EntityViewBuilderInterface.php/function/EntityViewBuilderInterface%3A%3AviewField #} |
||||||
|
{{ drupal_field('field_image', 'node', 1, {type: 'image_url', settings: {image_style: 'large'}}) }} |
||||||
|
|
||||||
|
{# Render field_image from node 1 in view_mode "teaser" in English with access check disabled. #} |
||||||
|
{{ drupal_field('field_image', 'node', 1, 'teaser', 'en', FALSE) }} |
||||||
|
|
||||||
|
{# Render field_image from node 1 in view_mode "full" (default) with access check disabled (named argument). #} |
||||||
|
{{ drupal_field('field_image', 'node', 1, check_access=false) }} |
||||||
|
``` |
||||||
|
|
||||||
|
## Drupal Menu |
||||||
|
```twig |
||||||
|
{# Print the top level of 'main' menu. #} |
||||||
|
{{ drupal_menu('main') }} |
||||||
|
|
||||||
|
{# Expand all menu links. #} |
||||||
|
{{ drupal_menu('main', expand=true) }} |
||||||
|
``` |
||||||
|
|
||||||
|
## 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 image using 'thumbnail' image style with lazy/eager loading (by attribute). #} |
||||||
|
{{ drupal_image('public://ocean.jpg', 'thumbnail', {loading: 'lazy'}) }} |
||||||
|
{{ drupal_image('public://ocean.jpg', 'thumbnail', {loading: 'eager'}) }} |
||||||
|
|
||||||
|
{# Render responsive image (using a named argument). #} |
||||||
|
{{ 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 |
||||||
|
{# The title is cached per URL. #} |
||||||
|
{{ drupal_title() }} |
||||||
|
``` |
||||||
|
|
||||||
|
## Drupal URL |
||||||
|
```twig |
||||||
|
{# The function accepts a valid internal path, such as "/node/1", "/taxonomy/term/1", a query string like "?query," or a fragment like "#anchor". #} |
||||||
|
|
||||||
|
{# 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 to the privileged users. #} |
||||||
|
{{ drupal_link('Example'|t, '/admin', check_access=true) }} |
||||||
|
``` |
||||||
|
|
||||||
|
## Drupal Messages |
||||||
|
```twig |
||||||
|
{{ drupal_messages() }} |
||||||
|
``` |
||||||
|
|
||||||
|
## Drupal Breadcrumb |
||||||
|
```twig |
||||||
|
{{ drupal_breadcrumb() }} |
||||||
|
``` |
||||||
|
|
||||||
|
## Drupal Breakpoint |
||||||
|
```twig |
||||||
|
{# Make Xdebug break on the specific line in the compiled Twig template. #} |
||||||
|
{{ drupal_breakpoint() }} |
||||||
|
``` |
||||||
|
|
||||||
|
## Contextual Links |
||||||
|
```twig |
||||||
|
{# Basic usage. #} |
||||||
|
<div class="contextual-region"> |
||||||
|
{{ drupal_contextual_links('entity.view.edit_form:view=frontpage:display_id=feed_1') }} |
||||||
|
{{ drupal_view('frontpage') }} |
||||||
|
</div> |
||||||
|
{# Multiple links. #} |
||||||
|
<div class="contextual-region"> |
||||||
|
{{ drupal_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 |
||||||
|
{# Basic usage #} |
||||||
|
{{ 'public://images/ocean.jpg'|image_style('thumbnail') }} |
||||||
|
|
||||||
|
{# Make sure to check that the URI is valid #} |
||||||
|
{% set image_uri = node.field_media_optional_image|file_uri %} |
||||||
|
{% if image_uri is not null %} |
||||||
|
{{ image_uri|image_style('thumbnail') }} |
||||||
|
{% endif %} |
||||||
|
``` |
||||||
|
`image_style` will trigger an error on invalid or empty URIs, to avoid broken |
||||||
|
images when used in an `<img/>` tag. |
||||||
|
|
||||||
|
## Transliterate |
||||||
|
```twig |
||||||
|
{{ 'Привет!'|transliterate }} |
||||||
|
``` |
||||||
|
|
||||||
|
## Check Markup |
||||||
|
```twig |
||||||
|
{{ '<b>bold</b> <strong>strong</strong>'|check_markup('restricted_html') }} |
||||||
|
``` |
||||||
|
|
||||||
|
## Format size |
||||||
|
Generates a string representation for the given byte count. |
||||||
|
```twig |
||||||
|
{{ 12345|format_size }} |
||||||
|
``` |
||||||
|
|
||||||
|
## Truncate |
||||||
|
```twig |
||||||
|
{# Truncates a UTF-8-encoded string safely to 10 characters. #} |
||||||
|
{{ 'Some long text'|truncate(10) }} |
||||||
|
|
||||||
|
{# Same as above but with respect of words boundary. #} |
||||||
|
{{ '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 |
||||||
|
This is an opposite of core `without` filter and adds properties instead of removing it. |
||||||
|
```twig |
||||||
|
{# Set top-level value. #} |
||||||
|
{{ content.field_image|with('#title', 'Photo'|t) }} |
||||||
|
|
||||||
|
{# Set nested value. #} |
||||||
|
{{ content|with(['field_image', '#title'], 'Photo'|t) }} |
||||||
|
``` |
||||||
|
|
||||||
|
## Data URI |
||||||
|
The filter generates a URL using the data scheme as defined in [RFC 2397](https://datatracker.ietf.org/doc/html/rfc2397) |
||||||
|
```twig |
||||||
|
{# Inline image. #} |
||||||
|
<img src="{{ '<svg xmlns="http://www.w3.org/2000/svg"><rect width="100" height="50" fill="lime"/></svg>'|data_uri('image/svg+xml') }}" alt="{{ 'Rectangle'|t }}"/> |
||||||
|
{# Image from file system. #} |
||||||
|
<img src="{{ source(directory ~ '/images/logo.svg')|data_uri('image/svg+xml') }}" alt="{{ 'Logo'|t }}"/> |
||||||
|
``` |
||||||
|
|
||||||
|
## Children |
||||||
|
```twig |
||||||
|
<ul> |
||||||
|
{% for tag in content.field_tags|children %} |
||||||
|
<li>{{ tag }}</li> |
||||||
|
{% endfor %} |
||||||
|
</ul> |
||||||
|
``` |
||||||
|
|
||||||
|
## File URI |
||||||
|
When field item list is passed, the URI will be extracted from the first item. |
||||||
|
In order to get URI of specific item specify its delta explicitly using array |
||||||
|
notation. |
||||||
|
```twig |
||||||
|
{{ node.field_image|file_uri }} |
||||||
|
{{ node.field_image[0]|file_uri }} |
||||||
|
``` |
||||||
|
|
||||||
|
Media fields are fully supported including OEmbed resources, in which case |
||||||
|
it will return the URL to the resource, similar to the `file_url` filter. |
||||||
|
```twig |
||||||
|
{{ node.field_media|file_uri }} |
||||||
|
``` |
||||||
|
|
||||||
|
## File URL |
||||||
|
For string arguments it works similar to core `file_url()` Twig function. |
||||||
|
```twig |
||||||
|
{{ 'public://sea.jpg'|file_url }} |
||||||
|
``` |
||||||
|
|
||||||
|
In order to generate absolute URL set "relative" parameter to `false`. |
||||||
|
```twig |
||||||
|
{{ 'public://sea.jpg'|file_url(false) }} |
||||||
|
``` |
||||||
|
|
||||||
|
When field item list is 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 }} |
||||||
|
``` |
||||||
|
|
||||||
|
It is also possible to extract file URL directly from an entity. |
||||||
|
```twig |
||||||
|
{{ image|file_url }} |
||||||
|
{{ media|file_url }} |
||||||
|
``` |
||||||
|
|
||||||
|
## Entity URL |
||||||
|
Gets the URL object for the entity. |
||||||
|
See \Drupal\Core\Entity\EntityInterface::toUrl() |
||||||
|
```twig |
||||||
|
{# Creates canonical URL for the node. #} |
||||||
|
{{ node|entity_url }} |
||||||
|
|
||||||
|
{# Creates URL for the node edit form. #} |
||||||
|
{{ node|entity_url('edit-form') }} |
||||||
|
``` |
||||||
|
|
||||||
|
## Entity Link |
||||||
|
Generates the HTML for a link to this entity. |
||||||
|
See \Drupal\Core\Entity\EntityInterface::toLink() |
||||||
|
```twig |
||||||
|
{# Creates a link to the node using the node's label. #} |
||||||
|
{{ node|entity_link }} |
||||||
|
|
||||||
|
{# Creates link to node comment form. #} |
||||||
|
{{ node|entity_link('Add new comment'|t, 'canonical', {fragment: 'comment-form'}) }} |
||||||
|
``` |
||||||
|
|
||||||
|
## Entity translation |
||||||
|
That is typically needed when printing data from referenced entities. |
||||||
|
```twig |
||||||
|
{{ media|translation.title|view }} |
||||||
|
``` |
||||||
|
|
||||||
|
## Cache metadata |
||||||
|
When using raw values from entities or render arrays it is essential to |
||||||
|
ensure that cache metadata are bubbled up. |
||||||
|
```twig |
||||||
|
<img src="{{ node.field_media|file_url }}" alt="Logo"/> |
||||||
|
{{ content.field_media|cache_metadata }} |
||||||
|
``` |
||||||
|
|
||||||
|
## PHP |
||||||
|
PHP filter is disabled by default. You can enable it in `settings.php` file as |
||||||
|
follows: |
||||||
|
```php |
||||||
|
$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 the following. |
||||||
|
```twig |
||||||
|
{{ 'now'|date('Y') }} |
||||||
|
``` |
@ -0,0 +1,69 @@ |
|||||||
|
# Migrating to Twig Tweak 3.x |
||||||
|
|
||||||
|
Twig Tweak 3.x branch is mainly API compatible with 2.x |
||||||
|
Below are known BC breaks that may require updating your Twig templates. |
||||||
|
|
||||||
|
## Dependencies |
||||||
|
Twig Tweak 3.x requires Drupal 9, Twig 2 and PHP 7.3. |
||||||
|
|
||||||
|
## Rendering entities |
||||||
|
|
||||||
|
### Entity ID is now required |
||||||
|
Entity ID parameter in `drupal_entity()` and `drupal_field()` functions is now |
||||||
|
mandatory. Previously it was possible to load entities from current route by |
||||||
|
omitting entity ID parameter. However, that was making Twig templates coupled |
||||||
|
with routes and could cause caching issues. |
||||||
|
|
||||||
|
Before: |
||||||
|
```twig |
||||||
|
{{ drupal_entity('node', null, 'teaser') }} |
||||||
|
``` |
||||||
|
|
||||||
|
After: |
||||||
|
```twig |
||||||
|
{{ drupal_entity('node', node.id, 'teaser') }} |
||||||
|
``` |
||||||
|
|
||||||
|
In case a template does not contain a variable with entity object you may |
||||||
|
prepare it in a preprocess hook. |
||||||
|
```php |
||||||
|
/** |
||||||
|
* Implements hook_preprocess_page(). |
||||||
|
*/ |
||||||
|
function preprocess_page(array &$variables): void { |
||||||
|
$variables['entity'] = \Drupal::routeMatch()->getParameter('entity_type'); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### Default view mode has changed |
||||||
|
The view mode parameter in `drupal_field()` has changed from `default` to `full`. If you are using `drupal_field()` without specifying a view mode, you should update your templates to specify the `default` one. |
||||||
|
|
||||||
|
Before: |
||||||
|
``` |
||||||
|
{{ drupal_field('field_name', 'node', node.id) }} |
||||||
|
``` |
||||||
|
|
||||||
|
After: |
||||||
|
``` |
||||||
|
{{ drupal_field('field_name', 'node', node.id, 'default') }} |
||||||
|
``` |
||||||
|
|
||||||
|
## Rendering blocks |
||||||
|
`drupal_block()` now moves attributes provided by block plugin to the outer |
||||||
|
element. This may break some CSS rules. |
||||||
|
|
||||||
|
Before: |
||||||
|
```html |
||||||
|
<div> |
||||||
|
<div class="from-block-plugin">Block content</div> |
||||||
|
</div> |
||||||
|
``` |
||||||
|
|
||||||
|
After: |
||||||
|
```html |
||||||
|
<div class="from-block-plugin"> |
||||||
|
<div>Block content</div> |
||||||
|
</div> |
||||||
|
``` |
||||||
|
|
||||||
|
See https://www.drupal.org/node/3068078 for details. |
@ -0,0 +1,63 @@ |
|||||||
|
# Using Twig Tweak to extend Views module functionality |
||||||
|
|
||||||
|
Twig Tweak's `drupal_view()` method provides access to embed views within any |
||||||
|
Twig code, including dynamically from within each row of another view. This |
||||||
|
feature provides an alternate method to accomplish the nesting provided by the |
||||||
|
[Views Field View](https://www.drupal.org/project/views_field_view) module. |
||||||
|
|
||||||
|
The most basic syntax for Twig Tweak's view embed simply specifies the view, and |
||||||
|
the machine name you wish to embed, as follows: |
||||||
|
```twig |
||||||
|
{{ drupal_view('who_s_new', 'block_1') }} |
||||||
|
``` |
||||||
|
|
||||||
|
You can, however, also specify additional parameters which map to contextual |
||||||
|
filters you have configured in your view. |
||||||
|
```twig |
||||||
|
{{ drupal_view('who_s_new', 'block_1', arg_1, arg_2, arg_3) }} |
||||||
|
``` |
||||||
|
|
||||||
|
## Nested View Example |
||||||
|
There are a lot of cases in views where you want to embed a list inside each |
||||||
|
row. For example, when you have a list of product categories (taxonomy terms) |
||||||
|
and for each category, you want to list the 3 newest products. In this example, |
||||||
|
assume your content type is 'product', and has a reference field to a taxonomy |
||||||
|
named 'product categories'. |
||||||
|
|
||||||
|
### Step1: Create your child view |
||||||
|
Create a view of the content of type 'product', and choose to create a block |
||||||
|
displaying an unformatted list of teasers, with 3 items per block. In this |
||||||
|
example, we'll use a view name of 'products_by_category'. Once the view is |
||||||
|
created, choose advanced and add a relationship to the taxonomy term that |
||||||
|
references 'product categories', and choose to require the relationship. Next, |
||||||
|
choose to add a "Contextual Filter" for "Term ID", and choose an action for when |
||||||
|
the filter value is not available (e.g. display contents of no results found). |
||||||
|
Set your desired sort and save your view. |
||||||
|
|
||||||
|
### Step 2: Create your parent view |
||||||
|
Create a view of taxonomy terms of type 'product categories', and choose to |
||||||
|
create a page which displays an unformatted list of fields. Once this is |
||||||
|
created, you should see the preview showing all the product categories. Choose |
||||||
|
to add a field of type "Term ID", and choose "Exclude from display"; this is |
||||||
|
necessary to make the term id available to the next field which uses Twig Tweak. |
||||||
|
Now, choose to add a field of type "Custom text" from the "Global" category. |
||||||
|
Inside that field, enter the Twig Tweak call to display the child view we |
||||||
|
created above, passing the tid as a contextual filter, as such: |
||||||
|
```twig |
||||||
|
{{ drupal_view('products_by_category', 'block_1', tid) }} |
||||||
|
``` |
||||||
|
|
||||||
|
You should now save your view, and be able to access the URL you assigned and |
||||||
|
see a list of product categories, each followed by the three most recent |
||||||
|
products within each. |
||||||
|
|
||||||
|
This example can be applied to any nested view scenario, including |
||||||
|
multiple-levels of nesting. |
||||||
|
|
||||||
|
## Check if the view has results |
||||||
|
```twig |
||||||
|
{% set view = drupal_view_result('related', 'block_1')|length %} |
||||||
|
{% if view > 0 %} |
||||||
|
{{ drupal_view('related', 'block_1') }} |
||||||
|
{% endif %} |
||||||
|
``` |
@ -0,0 +1,11 @@ |
|||||||
|
services: |
||||||
|
twig_tweak.validate: |
||||||
|
class: Drupal\twig_tweak\Command\ValidateCommand |
||||||
|
arguments: ['@twig'] |
||||||
|
tags: |
||||||
|
- { name: console.command } |
||||||
|
twig_tweak.debug: |
||||||
|
class: Drupal\twig_tweak\Command\DebugCommand |
||||||
|
arguments: ['@twig'] |
||||||
|
tags: |
||||||
|
- { name: console.command } |
After Width: | Height: | Size: 9.6 KiB |
@ -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> |
@ -0,0 +1,52 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\twig_tweak; |
||||||
|
|
||||||
|
use Drupal\Core\Cache\CacheableDependencyInterface; |
||||||
|
use Drupal\Core\Cache\CacheableMetadata; |
||||||
|
use Drupal\Core\Render\Element; |
||||||
|
|
||||||
|
/** |
||||||
|
* Cache metadata extractor service. |
||||||
|
*/ |
||||||
|
class CacheMetadataExtractor { |
||||||
|
|
||||||
|
/** |
||||||
|
* Extracts cache metadata from object or render array. |
||||||
|
* |
||||||
|
* @param \Drupal\Core\Cache\CacheableDependencyInterface|array $input |
||||||
|
* The cacheable object or render array. |
||||||
|
* |
||||||
|
* @return array |
||||||
|
* A render array with extracted cache metadata. |
||||||
|
*/ |
||||||
|
public function extractCacheMetadata($input): array { |
||||||
|
if ($input instanceof CacheableDependencyInterface) { |
||||||
|
$cache_metadata = CacheableMetadata::createFromObject($input); |
||||||
|
} |
||||||
|
elseif (is_array($input)) { |
||||||
|
$cache_metadata = self::extractFromArray($input); |
||||||
|
} |
||||||
|
else { |
||||||
|
$message = sprintf('The input should be either instance of %s or array. %s was given.', CacheableDependencyInterface::class, \get_debug_type($input)); |
||||||
|
throw new \InvalidArgumentException($message); |
||||||
|
} |
||||||
|
|
||||||
|
$build = []; |
||||||
|
$cache_metadata->applyTo($build); |
||||||
|
return $build; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Extracts cache metadata from renders array. |
||||||
|
*/ |
||||||
|
private static function extractFromArray(array $build): CacheableMetadata { |
||||||
|
$cache_metadata = CacheableMetadata::createFromRenderArray($build); |
||||||
|
$keys = Element::children($build); |
||||||
|
foreach (array_intersect_key($build, array_flip($keys)) as $item) { |
||||||
|
$cache_metadata->addCacheableDependency(self::extractFromArray($item)); |
||||||
|
} |
||||||
|
return $cache_metadata; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,217 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\twig_tweak\Command; |
||||||
|
|
||||||
|
use Symfony\Component\Console\Command\Command; |
||||||
|
use Symfony\Component\Console\Formatter\OutputFormatter; |
||||||
|
use Symfony\Component\Console\Input\InputInterface; |
||||||
|
use Symfony\Component\Console\Input\InputOption; |
||||||
|
use Symfony\Component\Console\Output\OutputInterface; |
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle; |
||||||
|
use Twig\Environment; |
||||||
|
use Twig\Loader\ChainLoader; |
||||||
|
use Twig\Loader\FilesystemLoader; |
||||||
|
|
||||||
|
/** |
||||||
|
* Lists twig functions, filters, and tests present in the current project. |
||||||
|
* |
||||||
|
* This is a simplified version of Symfony's Debug command. |
||||||
|
* |
||||||
|
* @see https://github.com/symfony/symfony/blob/5.x/src/Symfony/Bridge/Twig/Command/DebugCommand.php |
||||||
|
*/ |
||||||
|
final class DebugCommand extends Command { |
||||||
|
|
||||||
|
/** |
||||||
|
* Twig environment. |
||||||
|
* |
||||||
|
* @var \Twig\Environment |
||||||
|
*/ |
||||||
|
private $twig; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function __construct(Environment $twig) { |
||||||
|
parent::__construct(); |
||||||
|
$this->twig = $twig; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
protected function configure(): void { |
||||||
|
$this |
||||||
|
->setName('twig-tweak:debug') |
||||||
|
->setAliases(['twig-debug']) |
||||||
|
->addOption('filter', NULL, InputOption::VALUE_REQUIRED, 'Show details for all entries matching this filter.') |
||||||
|
->setDescription('Shows a list of twig functions, filters, globals and tests') |
||||||
|
->setHelp(<<<'EOF' |
||||||
|
The <info>%command.name%</info> command outputs a list of twig functions, |
||||||
|
filters, globals and tests. |
||||||
|
|
||||||
|
<info>drush %command.name%</info> |
||||||
|
|
||||||
|
The command lists all functions, filters, etc. |
||||||
|
|
||||||
|
<info>drush %command.name% --filter=date</info> |
||||||
|
|
||||||
|
The command lists everything that contains the word date. |
||||||
|
EOF); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int { |
||||||
|
$io = new SymfonyStyle($input, $output); |
||||||
|
$filter = $input->getOption('filter'); |
||||||
|
|
||||||
|
$types = ['functions', 'filters', 'tests']; |
||||||
|
foreach ($types as $type) { |
||||||
|
$items = []; |
||||||
|
foreach ($this->twig->{'get' . ucfirst($type)}() as $name => $entity) { |
||||||
|
if (!$filter || \strpos($name, $filter) !== FALSE) { |
||||||
|
|
||||||
|
$signature = ''; |
||||||
|
// Tests are typically implemented as Twig nodes so that it is hard |
||||||
|
// to get their signatures through reflection. |
||||||
|
if ($type == 'filters' || $type == 'functions') { |
||||||
|
try { |
||||||
|
$meta = $this->getMetadata($type, $entity); |
||||||
|
$default_signature = $type == 'functions' ? '()' : ''; |
||||||
|
$signature = $meta ? '(' . implode(', ', $meta) . ')' : $default_signature; |
||||||
|
} |
||||||
|
catch (\UnexpectedValueException $exception) { |
||||||
|
$signature = sprintf(' <error>%s</error>', OutputFormatter::escape($exception->getMessage())); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
$items[$name] = $name . $signature; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!$items) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
$io->section(\ucfirst($type)); |
||||||
|
|
||||||
|
ksort($items); |
||||||
|
$io->listing($items); |
||||||
|
} |
||||||
|
|
||||||
|
if (!$filter && $loaderPaths = $this->getLoaderPaths()) { |
||||||
|
$io->section('Loader Paths'); |
||||||
|
$rows = []; |
||||||
|
foreach ($loaderPaths as $namespace => $paths) { |
||||||
|
foreach ($paths as $path) { |
||||||
|
$rows[] = [$namespace, $path . \DIRECTORY_SEPARATOR]; |
||||||
|
} |
||||||
|
} |
||||||
|
$io->table(['Namespace', 'Paths'], $rows); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Gets loader paths. |
||||||
|
*/ |
||||||
|
private function getLoaderPaths(): array { |
||||||
|
$loaderPaths = []; |
||||||
|
foreach ($this->getFilesystemLoaders() as $loader) { |
||||||
|
foreach ($loader->getNamespaces() as $namespace) { |
||||||
|
$paths = $loader->getPaths($namespace); |
||||||
|
$namespace = FilesystemLoader::MAIN_NAMESPACE === $namespace ? |
||||||
|
'(None)' : '@' . $namespace; |
||||||
|
$loaderPaths[$namespace] = array_merge($loaderPaths[$namespace] ?? [], $paths); |
||||||
|
} |
||||||
|
} |
||||||
|
return $loaderPaths; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Gets metadata. |
||||||
|
*/ |
||||||
|
private function getMetadata(string $type, $entity): ?array { |
||||||
|
|
||||||
|
$callable = $entity->getCallable(); |
||||||
|
|
||||||
|
if (!$callable) { |
||||||
|
return NULL; |
||||||
|
} |
||||||
|
if (\is_array($callable)) { |
||||||
|
if (!method_exists($callable[0], $callable[1])) { |
||||||
|
return NULL; |
||||||
|
} |
||||||
|
$reflection = new \ReflectionMethod($callable[0], $callable[1]); |
||||||
|
} |
||||||
|
elseif (\is_object($callable) && method_exists($callable, '__invoke')) { |
||||||
|
$reflection = new \ReflectionMethod($callable, '__invoke'); |
||||||
|
} |
||||||
|
elseif (\function_exists($callable)) { |
||||||
|
$reflection = new \ReflectionFunction($callable); |
||||||
|
} |
||||||
|
elseif (\is_string($callable) && preg_match('{^(.+)::(.+)$}', $callable, $m) && method_exists($m[1], $m[2])) { |
||||||
|
$reflection = new \ReflectionMethod($m[1], $m[2]); |
||||||
|
} |
||||||
|
else { |
||||||
|
throw new \UnexpectedValueException('Unsupported callback type.'); |
||||||
|
} |
||||||
|
|
||||||
|
$args = $reflection->getParameters(); |
||||||
|
|
||||||
|
// Filter out context/environment args. |
||||||
|
if ($entity->needsEnvironment()) { |
||||||
|
array_shift($args); |
||||||
|
} |
||||||
|
if ($entity->needsContext()) { |
||||||
|
array_shift($args); |
||||||
|
} |
||||||
|
|
||||||
|
if ($type == 'filters') { |
||||||
|
// Remove the value the filter is applied on. |
||||||
|
array_shift($args); |
||||||
|
} |
||||||
|
|
||||||
|
// Format args. |
||||||
|
$args = array_map( |
||||||
|
static function (\ReflectionParameter $param): string { |
||||||
|
$arg = $param->getName(); |
||||||
|
if ($param->isDefaultValueAvailable()) { |
||||||
|
$arg .= ' = ' . \json_encode($param->getDefaultValue()); |
||||||
|
} |
||||||
|
return $arg; |
||||||
|
}, |
||||||
|
$args, |
||||||
|
); |
||||||
|
|
||||||
|
return $args; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns files system loaders. |
||||||
|
* |
||||||
|
* @return \Twig\Loader\FilesystemLoader[] |
||||||
|
* File system loaders. |
||||||
|
*/ |
||||||
|
private function getFilesystemLoaders(): array { |
||||||
|
$loaders = []; |
||||||
|
|
||||||
|
$loader = $this->twig->getLoader(); |
||||||
|
if ($loader instanceof FilesystemLoader) { |
||||||
|
$loaders[] = $loader; |
||||||
|
} |
||||||
|
elseif ($loader instanceof ChainLoader) { |
||||||
|
foreach ($loader->getLoaders() as $chained_loaders) { |
||||||
|
if ($chained_loaders instanceof FilesystemLoader) { |
||||||
|
$loaders[] = $chained_loaders; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return $loaders; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,271 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
/* |
||||||
|
* This file is part of the Symfony package. |
||||||
|
* |
||||||
|
* (c) Fabien Potencier <fabien@symfony.com> |
||||||
|
* |
||||||
|
* For the full copyright and license information, please view the LICENSE |
||||||
|
* file that was distributed with this source code. |
||||||
|
*/ |
||||||
|
|
||||||
|
// @codingStandardsIgnoreFile |
||||||
|
// This code is a literal copy of Symfony's LintCommand. |
||||||
|
// @see https://github.com/symfony/symfony/blob/5.x/src/Symfony/Bridge/Twig/Command/LintCommand.php |
||||||
|
namespace Drupal\twig_tweak\Command; |
||||||
|
|
||||||
|
use Symfony\Component\Console\Command\Command; |
||||||
|
use Symfony\Component\Console\Exception\InvalidArgumentException; |
||||||
|
use Symfony\Component\Console\Exception\RuntimeException; |
||||||
|
use Symfony\Component\Console\Input\InputArgument; |
||||||
|
use Symfony\Component\Console\Input\InputInterface; |
||||||
|
use Symfony\Component\Console\Input\InputOption; |
||||||
|
use Symfony\Component\Console\Output\OutputInterface; |
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle; |
||||||
|
use Symfony\Component\Finder\Finder; |
||||||
|
use Twig\Environment; |
||||||
|
use Twig\Error\Error; |
||||||
|
use Twig\Loader\ArrayLoader; |
||||||
|
use Twig\Loader\FilesystemLoader; |
||||||
|
use Twig\Source; |
||||||
|
|
||||||
|
/** |
||||||
|
* Command that will validate your template syntax and output encountered errors. |
||||||
|
* |
||||||
|
* @author Marc Weistroff <marc.weistroff@sensiolabs.com> |
||||||
|
* @author Jérôme Tamarelle <jerome@tamarelle.net> |
||||||
|
*/ |
||||||
|
class LintCommand extends Command |
||||||
|
{ |
||||||
|
protected static $defaultName = 'lint:twig'; |
||||||
|
|
||||||
|
private $twig; |
||||||
|
|
||||||
|
public function __construct(Environment $twig) |
||||||
|
{ |
||||||
|
parent::__construct(); |
||||||
|
|
||||||
|
$this->twig = $twig; |
||||||
|
} |
||||||
|
|
||||||
|
protected function configure() |
||||||
|
{ |
||||||
|
$this |
||||||
|
->setDescription('Lints a template and outputs encountered errors') |
||||||
|
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt') |
||||||
|
->addOption('show-deprecations', null, InputOption::VALUE_NONE, 'Show deprecations as errors') |
||||||
|
->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN') |
||||||
|
->setHelp(<<<'EOF' |
||||||
|
The <info>%command.name%</info> command lints a template and outputs to STDOUT |
||||||
|
the first encountered syntax error. |
||||||
|
|
||||||
|
You can validate the syntax of contents passed from STDIN: |
||||||
|
|
||||||
|
<info>cat filename | php %command.full_name% -</info> |
||||||
|
|
||||||
|
Or the syntax of a file: |
||||||
|
|
||||||
|
<info>php %command.full_name% filename</info> |
||||||
|
|
||||||
|
Or of a whole directory: |
||||||
|
|
||||||
|
<info>php %command.full_name% dirname</info> |
||||||
|
<info>php %command.full_name% dirname --format=json</info> |
||||||
|
|
||||||
|
EOF |
||||||
|
) |
||||||
|
; |
||||||
|
} |
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output) |
||||||
|
{ |
||||||
|
$io = new SymfonyStyle($input, $output); |
||||||
|
$filenames = $input->getArgument('filename'); |
||||||
|
$showDeprecations = $input->getOption('show-deprecations'); |
||||||
|
|
||||||
|
if (['-'] === $filenames) { |
||||||
|
return $this->display($input, $output, $io, [$this->validate(file_get_contents('php://stdin'), uniqid('sf_', true))]); |
||||||
|
} |
||||||
|
|
||||||
|
if (!$filenames) { |
||||||
|
$loader = $this->twig->getLoader(); |
||||||
|
if ($loader instanceof FilesystemLoader) { |
||||||
|
$paths = []; |
||||||
|
foreach ($loader->getNamespaces() as $namespace) { |
||||||
|
$paths[] = $loader->getPaths($namespace); |
||||||
|
} |
||||||
|
$filenames = array_merge(...$paths); |
||||||
|
} |
||||||
|
|
||||||
|
if (!$filenames) { |
||||||
|
throw new RuntimeException('Please provide a filename or pipe template content to STDIN.'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if ($showDeprecations) { |
||||||
|
$prevErrorHandler = set_error_handler(static function ($level, $message, $file, $line) use (&$prevErrorHandler) { |
||||||
|
if (\E_USER_DEPRECATED === $level) { |
||||||
|
$templateLine = 0; |
||||||
|
if (preg_match('/ at line (\d+)[ .]/', $message, $matches)) { |
||||||
|
$templateLine = $matches[1]; |
||||||
|
} |
||||||
|
|
||||||
|
throw new Error($message, $templateLine); |
||||||
|
} |
||||||
|
|
||||||
|
return $prevErrorHandler ? $prevErrorHandler($level, $message, $file, $line) : false; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
$filesInfo = $this->getFilesInfo($filenames); |
||||||
|
} finally { |
||||||
|
if ($showDeprecations) { |
||||||
|
restore_error_handler(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return $this->display($input, $output, $io, $filesInfo); |
||||||
|
} |
||||||
|
|
||||||
|
private function getFilesInfo(array $filenames): array |
||||||
|
{ |
||||||
|
$filesInfo = []; |
||||||
|
foreach ($filenames as $filename) { |
||||||
|
foreach ($this->findFiles($filename) as $file) { |
||||||
|
$filesInfo[] = $this->validate(file_get_contents($file), $file); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return $filesInfo; |
||||||
|
} |
||||||
|
|
||||||
|
protected function findFiles(string $filename) |
||||||
|
{ |
||||||
|
if (is_file($filename)) { |
||||||
|
return [$filename]; |
||||||
|
} elseif (is_dir($filename)) { |
||||||
|
return Finder::create()->files()->in($filename)->name('*.twig'); |
||||||
|
} |
||||||
|
|
||||||
|
throw new RuntimeException(sprintf('File or directory "%s" is not readable.', $filename)); |
||||||
|
} |
||||||
|
|
||||||
|
private function validate(string $template, string $file): array |
||||||
|
{ |
||||||
|
$realLoader = $this->twig->getLoader(); |
||||||
|
try { |
||||||
|
$temporaryLoader = new ArrayLoader([$file => $template]); |
||||||
|
$this->twig->setLoader($temporaryLoader); |
||||||
|
$nodeTree = $this->twig->parse($this->twig->tokenize(new Source($template, $file))); |
||||||
|
$this->twig->compile($nodeTree); |
||||||
|
$this->twig->setLoader($realLoader); |
||||||
|
} catch (Error $e) { |
||||||
|
$this->twig->setLoader($realLoader); |
||||||
|
|
||||||
|
return ['template' => $template, 'file' => $file, 'line' => $e->getTemplateLine(), 'valid' => false, 'exception' => $e]; |
||||||
|
} |
||||||
|
|
||||||
|
return ['template' => $template, 'file' => $file, 'valid' => true]; |
||||||
|
} |
||||||
|
|
||||||
|
private function display(InputInterface $input, OutputInterface $output, SymfonyStyle $io, array $files) |
||||||
|
{ |
||||||
|
switch ($input->getOption('format')) { |
||||||
|
case 'txt': |
||||||
|
return $this->displayTxt($output, $io, $files); |
||||||
|
case 'json': |
||||||
|
return $this->displayJson($output, $files); |
||||||
|
default: |
||||||
|
throw new InvalidArgumentException(sprintf('The format "%s" is not supported.', $input->getOption('format'))); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private function displayTxt(OutputInterface $output, SymfonyStyle $io, array $filesInfo) |
||||||
|
{ |
||||||
|
$errors = 0; |
||||||
|
|
||||||
|
foreach ($filesInfo as $info) { |
||||||
|
if ($info['valid'] && $output->isVerbose()) { |
||||||
|
$io->comment('<info>OK</info>'.($info['file'] ? sprintf(' in %s', $info['file']) : '')); |
||||||
|
} elseif (!$info['valid']) { |
||||||
|
++$errors; |
||||||
|
$this->renderException($io, $info['template'], $info['exception'], $info['file']); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (0 === $errors) { |
||||||
|
$io->success(sprintf('All %d Twig files contain valid syntax.', \count($filesInfo))); |
||||||
|
} else { |
||||||
|
$io->warning(sprintf('%d Twig files have valid syntax and %d contain errors.', \count($filesInfo) - $errors, $errors)); |
||||||
|
} |
||||||
|
|
||||||
|
return min($errors, 1); |
||||||
|
} |
||||||
|
|
||||||
|
private function displayJson(OutputInterface $output, array $filesInfo) |
||||||
|
{ |
||||||
|
$errors = 0; |
||||||
|
|
||||||
|
array_walk($filesInfo, function (&$v) use (&$errors) { |
||||||
|
$v['file'] = (string) $v['file']; |
||||||
|
unset($v['template']); |
||||||
|
if (!$v['valid']) { |
||||||
|
$v['message'] = $v['exception']->getMessage(); |
||||||
|
unset($v['exception']); |
||||||
|
++$errors; |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
$output->writeln(json_encode($filesInfo, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); |
||||||
|
|
||||||
|
return min($errors, 1); |
||||||
|
} |
||||||
|
|
||||||
|
private function renderException(OutputInterface $output, string $template, Error $exception, string $file = null) |
||||||
|
{ |
||||||
|
$line = $exception->getTemplateLine(); |
||||||
|
|
||||||
|
if ($file) { |
||||||
|
$output->text(sprintf('<error> ERROR </error> in %s (line %s)', $file, $line)); |
||||||
|
} else { |
||||||
|
$output->text(sprintf('<error> ERROR </error> (line %s)', $line)); |
||||||
|
} |
||||||
|
|
||||||
|
// If the line is not known (this might happen for deprecations if we fail at detecting the line for instance), |
||||||
|
// we render the message without context, to ensure the message is displayed. |
||||||
|
if ($line <= 0) { |
||||||
|
$output->text(sprintf('<error> >> %s</error> ', $exception->getRawMessage())); |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
foreach ($this->getContext($template, $line) as $lineNumber => $code) { |
||||||
|
$output->text(sprintf( |
||||||
|
'%s %-6s %s', |
||||||
|
$lineNumber === $line ? '<error> >> </error>' : ' ', |
||||||
|
$lineNumber, |
||||||
|
$code |
||||||
|
)); |
||||||
|
if ($lineNumber === $line) { |
||||||
|
$output->text(sprintf('<error> >> %s</error> ', $exception->getRawMessage())); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private function getContext(string $template, int $line, int $context = 3) |
||||||
|
{ |
||||||
|
$lines = explode("\n", $template); |
||||||
|
|
||||||
|
$position = max(0, $line - $context); |
||||||
|
$max = min(\count($lines), $line - 1 + $context); |
||||||
|
|
||||||
|
$result = []; |
||||||
|
while ($position < $max) { |
||||||
|
$result[$position + 1] = $lines[$position]; |
||||||
|
++$position; |
||||||
|
} |
||||||
|
|
||||||
|
return $result; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,37 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\twig_tweak\Command; |
||||||
|
|
||||||
|
use Symfony\Component\Finder\Finder; |
||||||
|
|
||||||
|
/** |
||||||
|
* Implements twig-tweak:lint console command. |
||||||
|
*/ |
||||||
|
final class ValidateCommand extends LintCommand { |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
protected static $defaultName = 'twig-tweak:validate'; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
protected function configure(): void { |
||||||
|
|
||||||
|
if (!\class_exists(Finder::class)) { |
||||||
|
throw new \LogicException('To validate Twig templates you must install symfony/finder component.'); |
||||||
|
} |
||||||
|
|
||||||
|
parent::configure(); |
||||||
|
$this->setAliases(['twig-validate']); |
||||||
|
$this->setHelp( |
||||||
|
$this->getHelp() . <<< 'TEXT' |
||||||
|
|
||||||
|
This command only validates Twig Syntax. For checking code style |
||||||
|
consider using <info>friendsoftwig/twigcs</info> package. |
||||||
|
TEXT |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,729 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\twig_tweak; |
||||||
|
|
||||||
|
use Drupal\Component\Utility\NestedArray; |
||||||
|
use Drupal\Component\Utility\Unicode; |
||||||
|
use Drupal\Component\Utility\UrlHelper; |
||||||
|
use Drupal\Component\Uuid\Uuid; |
||||||
|
use Drupal\Core\Cache\CacheableMetadata; |
||||||
|
use Drupal\Core\Entity\EntityInterface; |
||||||
|
use Drupal\Core\Extension\ModuleHandlerInterface; |
||||||
|
use Drupal\Core\Field\FieldItemInterface; |
||||||
|
use Drupal\Core\Field\FieldItemListInterface; |
||||||
|
use Drupal\Core\Link; |
||||||
|
use Drupal\Core\Render\Element; |
||||||
|
use Drupal\Core\Render\Markup; |
||||||
|
use Drupal\Core\Site\Settings; |
||||||
|
use Drupal\Core\Theme\ThemeManagerInterface; |
||||||
|
use Drupal\Core\Url; |
||||||
|
use Drupal\image\Entity\ImageStyle; |
||||||
|
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 { |
||||||
|
|
||||||
|
/** |
||||||
|
* The module handler to invoke alter hooks. |
||||||
|
* |
||||||
|
* @var \Drupal\Core\Extension\ModuleHandlerInterface |
||||||
|
*/ |
||||||
|
protected $moduleHandler; |
||||||
|
|
||||||
|
/** |
||||||
|
* The theme manager to invoke alter hooks. |
||||||
|
* |
||||||
|
* @var \Drupal\Core\Theme\ThemeManagerInterface |
||||||
|
*/ |
||||||
|
protected $themeManager; |
||||||
|
|
||||||
|
/** |
||||||
|
* Constructs the TwigTweakExtension object. |
||||||
|
*/ |
||||||
|
public function __construct(ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager) { |
||||||
|
$this->moduleHandler = $module_handler; |
||||||
|
$this->themeManager = $theme_manager; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getFunctions(): array { |
||||||
|
$context_options = ['needs_context' => TRUE]; |
||||||
|
$all_options = ['needs_environment' => TRUE, 'needs_context' => TRUE]; |
||||||
|
|
||||||
|
$functions = [ |
||||||
|
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), |
||||||
|
// @phpcs:ignore Drupal.Arrays.Array.LongLineDeclaration |
||||||
|
new TwigFunction('drupal_contextual_links', [self::class, 'drupalContextualLinks']), |
||||||
|
]; |
||||||
|
|
||||||
|
$this->moduleHandler->alter('twig_tweak_functions', $functions); |
||||||
|
$this->themeManager->alter('twig_tweak_functions', $functions); |
||||||
|
|
||||||
|
return $functions; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@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('format_size', 'format_size'), |
||||||
|
new TwigFilter('truncate', [Unicode::class, 'truncate']), |
||||||
|
new TwigFilter('view', [self::class, 'viewFilter']), |
||||||
|
new TwigFilter('with', [self::class, 'withFilter']), |
||||||
|
new TwigFilter('data_uri', [self::class, 'dataUriFilter']), |
||||||
|
new TwigFilter('children', [self::class, 'childrenFilter']), |
||||||
|
new TwigFilter('file_uri', [self::class, 'fileUriFilter']), |
||||||
|
new TwigFilter('file_url', [self::class, 'fileUrlFilter']), |
||||||
|
new TwigFilter('entity_url', [self::class, 'entityUrl']), |
||||||
|
new TwigFilter('entity_link', [self::class, 'entityLink']), |
||||||
|
new TwigFilter('translation', [self::class, 'entityTranslation']), |
||||||
|
new TwigFilter('cache_metadata', [self::class, 'CacheMetadata']), |
||||||
|
]; |
||||||
|
|
||||||
|
if (Settings::get('twig_tweak_enable_php_filter')) { |
||||||
|
$filters[] = new TwigFilter('php', [self::class, 'phpFilter'], ['needs_context' => TRUE]); |
||||||
|
} |
||||||
|
|
||||||
|
$this->moduleHandler->alter('twig_tweak_filters', $filters); |
||||||
|
$this->themeManager->alter('twig_tweak_filters', $filters); |
||||||
|
|
||||||
|
return $filters; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getTests(): array { |
||||||
|
$tests = []; |
||||||
|
|
||||||
|
$this->moduleHandler->alter('twig_tweak_tests', $tests); |
||||||
|
$this->themeManager->alter('twig_tweak_tests', $tests); |
||||||
|
|
||||||
|
return $tests; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 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 $selector, string $view_mode = 'full', ?string $langcode = NULL, bool $check_access = TRUE): array { |
||||||
|
|
||||||
|
$storage = \Drupal::entityTypeManager()->getStorage($entity_type); |
||||||
|
|
||||||
|
if (Uuid::isValid($selector)) { |
||||||
|
$entities = $storage->loadByProperties(['uuid' => $selector]); |
||||||
|
$entity = reset($entities); |
||||||
|
} |
||||||
|
// Fall back to entity ID. |
||||||
|
else { |
||||||
|
$entity = $storage->load($selector); |
||||||
|
} |
||||||
|
|
||||||
|
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 ? |
||||||
|
\Drupal::service('entity.repository')->getActive($entity_type, $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, $view_mode = 'full', string $langcode = NULL, bool $check_access = TRUE): array { |
||||||
|
$entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load($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 $selector, string $style = NULL, array $attributes = [], bool $responsive = FALSE, bool $check_access = TRUE): array { |
||||||
|
|
||||||
|
// Determine selector type by its value. |
||||||
|
if (preg_match('/^\d+$/', $selector)) { |
||||||
|
$selector_type = 'fid'; |
||||||
|
} |
||||||
|
elseif (Uuid::isValid($selector)) { |
||||||
|
$selector_type = 'uuid'; |
||||||
|
} |
||||||
|
else { |
||||||
|
$selector_type = 'uri'; |
||||||
|
} |
||||||
|
|
||||||
|
$files = \Drupal::entityTypeManager() |
||||||
|
->getStorage('file') |
||||||
|
->loadByProperties([$selector_type => $selector]); |
||||||
|
|
||||||
|
if (count($files) == 0) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
|
||||||
|
// To avoid ambiguity order by fid. |
||||||
|
ksort($files); |
||||||
|
|
||||||
|
$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. |
||||||
|
* |
||||||
|
* @todo Test it with NullRouteMatch |
||||||
|
*/ |
||||||
|
public static function drupalTitle(): array { |
||||||
|
$title = NULL; |
||||||
|
if ($route = \Drupal::routeMatch()->getRouteObject()) { |
||||||
|
$title = \Drupal::service('title_resolver')->getTitle(\Drupal::request(), $route); |
||||||
|
} |
||||||
|
$build['#markup'] = is_array($title) ? |
||||||
|
\Drupal::service('renderer')->render($title) : $title; |
||||||
|
$build['#cache']['contexts'] = ['url']; |
||||||
|
return $build; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Generates a URL from an internal or external 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 (UrlHelper::isExternal($user_input)) { |
||||||
|
return Url::fromUri($user_input, $options); |
||||||
|
} |
||||||
|
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. |
||||||
|
* @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 entered HTML text with tokens replaced. |
||||||
|
*/ |
||||||
|
public static function tokenReplaceFilter(string $text, array $data = [], array $options = []): string { |
||||||
|
return \Drupal::token()->replace($text, $data, $options); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 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|null $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 (!$path) { |
||||||
|
trigger_error('Image path is empty.'); |
||||||
|
return NULL; |
||||||
|
} |
||||||
|
|
||||||
|
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 \Drupal::service('file_url_generator') |
||||||
|
->transformRelative($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 object|null $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, $view_mode = 'default', string $langcode = NULL, bool $check_access = TRUE): array { |
||||||
|
$build = []; |
||||||
|
if ($object instanceof FieldItemListInterface || $object instanceof FieldItemInterface) { |
||||||
|
$build = $object->view($view_mode); |
||||||
|
/** @var \Drupal\Core\Entity\Plugin\DataType\EntityAdapter $parent */ |
||||||
|
if ($parent = $object->getParent()) { |
||||||
|
CacheableMetadata::createFromRenderArray($build) |
||||||
|
->addCacheableDependency($parent->getEntity()) |
||||||
|
->applyTo($build); |
||||||
|
} |
||||||
|
} |
||||||
|
elseif ($object instanceof EntityInterface) { |
||||||
|
$build = \Drupal::service('twig_tweak.entity_view_builder')->build($object, $view_mode, $langcode, $check_access); |
||||||
|
} |
||||||
|
return $build; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a data URI (RFC 2397). |
||||||
|
*/ |
||||||
|
public static function dataUriFilter(string $data, string $mime, array $parameters = []): string { |
||||||
|
$uri = 'data:' . $mime; |
||||||
|
foreach ($parameters as $key => $value) { |
||||||
|
$uri .= ';' . $key . '=' . rawurlencode($value); |
||||||
|
} |
||||||
|
$uri .= \str_starts_with($data, 'text/') ? |
||||||
|
',' . rawurlencode($data) : ';base64,' . base64_encode($data); |
||||||
|
return $uri; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 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 URI to the file. |
||||||
|
* |
||||||
|
* @param object $input |
||||||
|
* An object that contains the URI. |
||||||
|
* |
||||||
|
* @return string|null |
||||||
|
* A URI that may be used to access the file. |
||||||
|
*/ |
||||||
|
public static function fileUriFilter($input): ?string { |
||||||
|
return \Drupal::service('twig_tweak.uri_extractor')->extractUri($input); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a URL path to the file. |
||||||
|
* |
||||||
|
* @param string|object $input |
||||||
|
* Can be either file URI or an object that contains the URI. |
||||||
|
* @param bool $relative |
||||||
|
* (optional) Whether the URL should be root-relative, defaults to true. |
||||||
|
* |
||||||
|
* @return string|null |
||||||
|
* A URL that may be used to access the file. |
||||||
|
*/ |
||||||
|
public static function fileUrlFilter($input, bool $relative = TRUE): ?string { |
||||||
|
return \Drupal::service('twig_tweak.url_extractor')->extractUrl($input, $relative); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Gets the URL object for the entity. |
||||||
|
* |
||||||
|
* @todo Remove this once Drupal allows `toUrl` method in the sandbox policy. |
||||||
|
* |
||||||
|
* @see https://www.drupal.org/node/2907810 |
||||||
|
* @see \Drupal\Core\Entity\EntityInterface::toUrl() |
||||||
|
*/ |
||||||
|
public static function entityUrl(EntityInterface $entity, string $rel = 'canonical', array $options = []): Url { |
||||||
|
return $entity->toUrl($rel, $options); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Gets the URL object for the entity. |
||||||
|
* |
||||||
|
* @todo Remove this once Drupal allows `toLink` method in the sandbox policy. |
||||||
|
* |
||||||
|
* @see https://www.drupal.org/node/2907810 |
||||||
|
* @see \Drupal\Core\Entity\EntityInterface::toLink() |
||||||
|
*/ |
||||||
|
public static function entityLink(EntityInterface $entity, ?string $text = NULL, string $rel = 'canonical', array $options = []): Link { |
||||||
|
return $entity->toLink($text, $rel, $options); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the translation for the given entity. |
||||||
|
* |
||||||
|
* @param \Drupal\Core\Entity\EntityInterface $entity |
||||||
|
* The entity to get the translation from. |
||||||
|
* @param string $langcode |
||||||
|
* (optional) For which language the translation should be looked for, |
||||||
|
* defaults to the current language context. |
||||||
|
* |
||||||
|
* @return \Drupal\Core\Entity\EntityInterface |
||||||
|
* The appropriate translation for the given language context. |
||||||
|
*/ |
||||||
|
public static function entityTranslation(EntityInterface $entity, string $langcode = NULL): EntityInterface { |
||||||
|
return \Drupal::service('entity.repository')->getTranslationFromContext($entity, $langcode); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Extracts cache metadata from object or render array. |
||||||
|
* |
||||||
|
* @param \Drupal\Core\Cache\CacheableDependencyInterface|array $input |
||||||
|
* The cacheable object or render array. |
||||||
|
* |
||||||
|
* @return array |
||||||
|
* A render array with extracted cache metadata. |
||||||
|
*/ |
||||||
|
public static function cacheMetadata($input): array { |
||||||
|
return \Drupal::service('twig_tweak.cache_metadata_extractor')->extractCacheMetadata($input); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Evaluates a string of PHP code. |
||||||
|
* |
||||||
|
* @param array $context |
||||||
|
* Twig context. |
||||||
|
* @param string $code |
||||||
|
* Valid PHP code to be evaluated. |
||||||
|
* |
||||||
|
* @return mixed |
||||||
|
* The eval() result. |
||||||
|
*/ |
||||||
|
public static function phpFilter(array $context, string $code) { |
||||||
|
// Make Twig variables available in PHP code. |
||||||
|
extract($context, EXTR_SKIP); |
||||||
|
ob_start(); |
||||||
|
// phpcs:ignore Drupal.Functions.DiscouragedFunctions.Discouraged |
||||||
|
print eval($code); |
||||||
|
$output = ob_get_contents(); |
||||||
|
ob_end_clean(); |
||||||
|
return $output; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,87 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\twig_tweak; |
||||||
|
|
||||||
|
use Drupal\Core\Entity\ContentEntityInterface; |
||||||
|
use Drupal\Core\Entity\EntityTypeManagerInterface; |
||||||
|
use Drupal\Core\Field\EntityReferenceFieldItemListInterface; |
||||||
|
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; |
||||||
|
use Drupal\file\FileInterface; |
||||||
|
use Drupal\media\MediaInterface; |
||||||
|
use Drupal\media\Plugin\media\Source\OEmbedInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* URI extractor service. |
||||||
|
*/ |
||||||
|
class UriExtractor { |
||||||
|
|
||||||
|
/** |
||||||
|
* The entity type manager. |
||||||
|
* |
||||||
|
* @var \Drupal\Core\Entity\EntityTypeManagerInterface |
||||||
|
*/ |
||||||
|
protected $entityTypeManager; |
||||||
|
|
||||||
|
/** |
||||||
|
* Constructs a UrlExtractor object. |
||||||
|
*/ |
||||||
|
public function __construct(EntityTypeManagerInterface $entity_type_manager) { |
||||||
|
$this->entityTypeManager = $entity_type_manager; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a URI to the file. |
||||||
|
* |
||||||
|
* @param Object|null $input |
||||||
|
* An object that contains the URI. |
||||||
|
* |
||||||
|
* @return string|null |
||||||
|
* A URI that may be used to access the file. |
||||||
|
*/ |
||||||
|
public function extractUri(?object $input): ?string { |
||||||
|
$entity = $input; |
||||||
|
if ($input instanceof EntityReferenceFieldItemListInterface) { |
||||||
|
/** @var \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $item */ |
||||||
|
if ($item = $input->first()) { |
||||||
|
$entity = $item->entity; |
||||||
|
} |
||||||
|
} |
||||||
|
elseif ($input instanceof EntityReferenceItem) { |
||||||
|
$entity = $input->entity; |
||||||
|
} |
||||||
|
// Drupal does not clean up references to deleted entities. So that the |
||||||
|
// entity property might be empty while the field item might not. |
||||||
|
// @see https://www.drupal.org/project/drupal/issues/2723323 |
||||||
|
return $entity instanceof ContentEntityInterface ? |
||||||
|
$this->getUriFromEntity($entity) : NULL; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Extracts file URI from content entity. |
||||||
|
* |
||||||
|
* @param \Drupal\Core\Entity\ContentEntityInterface $entity |
||||||
|
* Entity object that contains information about the file. |
||||||
|
* |
||||||
|
* @return string|null |
||||||
|
* A URI that can be used to access the file. |
||||||
|
*/ |
||||||
|
private function getUriFromEntity(ContentEntityInterface $entity): ?string { |
||||||
|
if ($entity instanceof MediaInterface) { |
||||||
|
$source = $entity->getSource(); |
||||||
|
$value = $source->getSourceFieldValue($entity); |
||||||
|
if ($source instanceof OEmbedInterface) { |
||||||
|
return $value; |
||||||
|
} |
||||||
|
/** @var \Drupal\file\FileInterface $file */ |
||||||
|
$file = $this->entityTypeManager->getStorage('file')->load($value); |
||||||
|
if ($file) { |
||||||
|
return $file->getFileUri(); |
||||||
|
} |
||||||
|
} |
||||||
|
elseif ($entity instanceof FileInterface) { |
||||||
|
return $entity->getFileUri(); |
||||||
|
} |
||||||
|
return NULL; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,117 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\twig_tweak; |
||||||
|
|
||||||
|
use Drupal\Core\Entity\ContentEntityInterface; |
||||||
|
use Drupal\Core\Entity\EntityTypeManagerInterface; |
||||||
|
use Drupal\Core\Field\EntityReferenceFieldItemListInterface; |
||||||
|
use Drupal\Core\Field\FieldItemList; |
||||||
|
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; |
||||||
|
use Drupal\Core\File\FileUrlGeneratorInterface; |
||||||
|
use Drupal\file\FileInterface; |
||||||
|
use Drupal\link\LinkItemInterface; |
||||||
|
use Drupal\media\MediaInterface; |
||||||
|
use Drupal\media\Plugin\media\Source\OEmbedInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* URL extractor service. |
||||||
|
*/ |
||||||
|
class UrlExtractor { |
||||||
|
|
||||||
|
/** |
||||||
|
* The entity type manager. |
||||||
|
* |
||||||
|
* @var \Drupal\Core\Entity\EntityTypeManagerInterface |
||||||
|
*/ |
||||||
|
protected $entityTypeManager; |
||||||
|
|
||||||
|
/** |
||||||
|
* The file URL generator. |
||||||
|
* |
||||||
|
* @var \Drupal\Core\File\FileUrlGeneratorInterface |
||||||
|
*/ |
||||||
|
protected $fileUrlGenerator; |
||||||
|
|
||||||
|
/** |
||||||
|
* Constructs a UrlExtractor object. |
||||||
|
*/ |
||||||
|
public function __construct(EntityTypeManagerInterface $entity_type_manager, FileUrlGeneratorInterface $file_url_generator) { |
||||||
|
$this->entityTypeManager = $entity_type_manager; |
||||||
|
$this->fileUrlGenerator = $file_url_generator; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Extracts file URL from a string or object. |
||||||
|
* |
||||||
|
* @param string|object $input |
||||||
|
* Can be either file URI or an object that contains the URI. |
||||||
|
* @param bool $relative |
||||||
|
* (optional) Whether the URL should be root-relative, defaults to true. |
||||||
|
* |
||||||
|
* @return string|null |
||||||
|
* A URL that may be used to access the file. |
||||||
|
*/ |
||||||
|
public function extractUrl($input, bool $relative = TRUE): ?string { |
||||||
|
if (is_string($input)) { |
||||||
|
return $this->fileUrlGenerator->{$relative ? 'generateString' : 'generateAbsoluteString'}($input); |
||||||
|
} |
||||||
|
elseif ($input instanceof LinkItemInterface) { |
||||||
|
return $input->getUrl()->toString(); |
||||||
|
} |
||||||
|
elseif ($input instanceof FieldItemList && $input->first() instanceof LinkItemInterface) { |
||||||
|
return $input->first()->getUrl()->toString(); |
||||||
|
} |
||||||
|
|
||||||
|
$entity = $input; |
||||||
|
if ($input instanceof EntityReferenceFieldItemListInterface) { |
||||||
|
/** @var \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $item */ |
||||||
|
if ($item = $input->first()) { |
||||||
|
$entity = $item->entity; |
||||||
|
} |
||||||
|
} |
||||||
|
elseif ($input instanceof EntityReferenceItem) { |
||||||
|
$entity = $input->entity; |
||||||
|
} |
||||||
|
// Drupal does not clean up references to deleted entities. So that the |
||||||
|
// entity property might be empty while the field item might not. |
||||||
|
// @see https://www.drupal.org/project/drupal/issues/2723323 |
||||||
|
return $entity instanceof ContentEntityInterface ? |
||||||
|
$this->getUrlFromEntity($entity, $relative) : NULL; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Extracts file URL from content entity. |
||||||
|
* |
||||||
|
* @param \Drupal\Core\Entity\ContentEntityInterface $entity |
||||||
|
* Entity object that contains information about the file. |
||||||
|
* @param bool $relative |
||||||
|
* (optional) Whether the URL should be root-relative, defaults to true. |
||||||
|
* |
||||||
|
* @return string|null |
||||||
|
* A URL that may be used to access the file. |
||||||
|
*/ |
||||||
|
private function getUrlFromEntity(ContentEntityInterface $entity, bool $relative = TRUE): ?string { |
||||||
|
if ($entity instanceof MediaInterface) { |
||||||
|
$source = $entity->getSource(); |
||||||
|
$value = $source->getSourceFieldValue($entity); |
||||||
|
if (!$value) { |
||||||
|
return NULL; |
||||||
|
} |
||||||
|
elseif ($source instanceof OEmbedInterface) { |
||||||
|
return $value; |
||||||
|
} |
||||||
|
else { |
||||||
|
/** @var \Drupal\file\FileInterface $file */ |
||||||
|
$file = $this->entityTypeManager->getStorage('file')->load($value); |
||||||
|
if ($file) { |
||||||
|
return $file->createFileUrl($relative); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
elseif ($entity instanceof FileInterface) { |
||||||
|
return $entity->createFileUrl($relative); |
||||||
|
} |
||||||
|
return NULL; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,186 @@ |
|||||||
|
<?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\Context\ContextHandlerInterface; |
||||||
|
use Drupal\Core\Plugin\Context\ContextRepositoryInterface; |
||||||
|
use Drupal\Core\Plugin\ContextAwarePluginInterface; |
||||||
|
use Drupal\Core\Render\Element; |
||||||
|
use Drupal\Core\Routing\RouteMatchInterface; |
||||||
|
use Drupal\Core\Session\AccountInterface; |
||||||
|
use Symfony\Component\HttpFoundation\RequestStack; |
||||||
|
|
||||||
|
/** |
||||||
|
* Block view builder. |
||||||
|
*/ |
||||||
|
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) { |
||||||
|
// Account for the scenario that a NullRouteMatch is returned. This, for |
||||||
|
// example, is the case when Search API is indexing the site during |
||||||
|
// Drush cron. |
||||||
|
if ($route = $this->routeMatch->getRouteObject()) { |
||||||
|
$request = $this->requestStack->getCurrentRequest(); |
||||||
|
$title = $this->titleResolver->getTitle($request, $route); |
||||||
|
$block_plugin->setTitle($title); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Place the content returned by the block plugin into a 'content' child |
||||||
|
// element, as a way to allow the plugin to have complete control of its |
||||||
|
// properties and rendering (for instance, its own #theme) without |
||||||
|
// conflicting with the properties used above. |
||||||
|
$build['content'] = $block_plugin->build(); |
||||||
|
|
||||||
|
if ($block_plugin instanceof TitleBlockPluginInterface) { |
||||||
|
$build['content']['#cache']['contexts'][] = 'url'; |
||||||
|
} |
||||||
|
// Some blocks return null instead of array when empty. |
||||||
|
// @see https://www.drupal.org/project/drupal/issues/3212354 |
||||||
|
if ($wrapper && is_array($build['content']) && !Element::isEmpty($build['content'])) { |
||||||
|
$build += [ |
||||||
|
'#theme' => 'block', |
||||||
|
'#id' => $configuration['id'] ?? NULL, |
||||||
|
'#attributes' => [], |
||||||
|
'#contextual_links' => [], |
||||||
|
'#configuration' => $block_plugin->getConfiguration(), |
||||||
|
'#plugin_id' => $block_plugin->getPluginId(), |
||||||
|
'#base_plugin_id' => $block_plugin->getBaseId(), |
||||||
|
'#derivative_plugin_id' => $block_plugin->getDerivativeId(), |
||||||
|
]; |
||||||
|
// Semantically, the content returned by the plugin is the block, and in |
||||||
|
// particular, #attributes and #contextual_links is information about |
||||||
|
// the *entire* block. Therefore, we must move these properties into the |
||||||
|
// top-level element. |
||||||
|
foreach (['#attributes', '#contextual_links'] as $property) { |
||||||
|
if (isset($build['content'][$property])) { |
||||||
|
$build[$property] = $build['content'][$property]; |
||||||
|
unset($build['content'][$property]); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
CacheableMetadata::createFromRenderArray($build) |
||||||
|
->addCacheableDependency($access) |
||||||
|
->addCacheableDependency($block_plugin) |
||||||
|
->applyTo($build); |
||||||
|
|
||||||
|
if (!isset($build['#cache']['keys'])) { |
||||||
|
$build['#cache']['keys'] = [ |
||||||
|
'twig_tweak_block', |
||||||
|
$id, |
||||||
|
'[configuration]=' . hash('sha256', serialize($configuration)), |
||||||
|
'[wrapper]=' . (int) $wrapper, |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
return $build; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,61 @@ |
|||||||
|
<?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; |
||||||
|
|
||||||
|
/** |
||||||
|
* Entity form view builder. |
||||||
|
*/ |
||||||
|
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. |
||||||
|
* |
||||||
|
* @todo Add langcode parameter. |
||||||
|
* |
||||||
|
* @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) |
||||||
|
->addCacheableDependency($access) |
||||||
|
->addCacheableDependency($entity) |
||||||
|
->applyTo($build); |
||||||
|
|
||||||
|
return $build; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,57 @@ |
|||||||
|
<?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; |
||||||
|
use Drupal\Core\Entity\EntityTypeManagerInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* Entity view builder. |
||||||
|
*/ |
||||||
|
class EntityViewBuilder { |
||||||
|
|
||||||
|
/** |
||||||
|
* The entity type manager. |
||||||
|
* |
||||||
|
* @var \Drupal\Core\Entity\EntityTypeManagerInterface |
||||||
|
*/ |
||||||
|
protected $entityTypeManager; |
||||||
|
|
||||||
|
/** |
||||||
|
* The entity repository service. |
||||||
|
* |
||||||
|
* @var \Drupal\Core\Entity\EntityRepositoryInterface |
||||||
|
*/ |
||||||
|
protected $entityRepository; |
||||||
|
|
||||||
|
/** |
||||||
|
* Constructs an EntityViewBuilder object. |
||||||
|
*/ |
||||||
|
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository) { |
||||||
|
$this->entityTypeManager = $entity_type_manager; |
||||||
|
$this->entityRepository = $entity_repository; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 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 = []; |
||||||
|
$entity = $this->entityRepository->getTranslationFromContext($entity, $langcode); |
||||||
|
$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) |
||||||
|
->addCacheableDependency($access) |
||||||
|
->addCacheableDependency($entity) |
||||||
|
->applyTo($build); |
||||||
|
return $build; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,76 @@ |
|||||||
|
<?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; |
||||||
|
|
||||||
|
/** |
||||||
|
* Field view builder. |
||||||
|
*/ |
||||||
|
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 or display options. |
||||||
|
* @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. |
||||||
|
* |
||||||
|
* @see \Drupal\Core\Entity\EntityViewBuilderInterface::viewField() |
||||||
|
*/ |
||||||
|
public function build( |
||||||
|
EntityInterface $entity, |
||||||
|
string $field_name, |
||||||
|
$view_mode = 'full', |
||||||
|
string $langcode = NULL, |
||||||
|
bool $check_access = TRUE |
||||||
|
): array { |
||||||
|
|
||||||
|
$build = []; |
||||||
|
|
||||||
|
$entity = $this->entityRepository->getTranslationFromContext($entity, $langcode); |
||||||
|
$access = $check_access ? $entity->access('view', NULL, TRUE) : AccessResult::allowed(); |
||||||
|
if ($access->isAllowed()) { |
||||||
|
if (!isset($entity->{$field_name})) { |
||||||
|
// @todo Trigger error here. |
||||||
|
return []; |
||||||
|
} |
||||||
|
$build = $entity->{$field_name}->view($view_mode); |
||||||
|
} |
||||||
|
|
||||||
|
CacheableMetadata::createFromRenderArray($build) |
||||||
|
->addCacheableDependency($access) |
||||||
|
->addCacheableDependency($entity) |
||||||
|
->applyTo($build); |
||||||
|
|
||||||
|
return $build; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,98 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\twig_tweak\View; |
||||||
|
|
||||||
|
use Drupal\Core\Access\AccessResult; |
||||||
|
use Drupal\Core\Cache\CacheableMetadata; |
||||||
|
use Drupal\Core\Image\ImageFactory; |
||||||
|
use Drupal\file\FileInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* Image view builder. |
||||||
|
*/ |
||||||
|
class ImageViewBuilder { |
||||||
|
|
||||||
|
/** |
||||||
|
* The provider image factory. |
||||||
|
* |
||||||
|
* @var \Drupal\Core\Image\ImageFactory |
||||||
|
*/ |
||||||
|
protected $imageFactory; |
||||||
|
|
||||||
|
/** |
||||||
|
* Constructs an ImageViewBuilder object. |
||||||
|
*/ |
||||||
|
public function __construct(ImageFactory $imageFactory) { |
||||||
|
$this->imageFactory = $imageFactory; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 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 = $access->isAllowed() ? $this->doBuild($file, $style, $attributes, $responsive) : []; |
||||||
|
|
||||||
|
CacheableMetadata::createFromRenderArray($build) |
||||||
|
->addCacheableDependency($access) |
||||||
|
->addCacheableDependency($file) |
||||||
|
->applyTo($build); |
||||||
|
|
||||||
|
return $build; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Actually builds the image. |
||||||
|
*/ |
||||||
|
private function doBuild(FileInterface $file, string $style = NULL, array $attributes = [], bool $responsive = FALSE): array { |
||||||
|
$build['#uri'] = $file->getFileUri(); |
||||||
|
$build['#attributes'] = $attributes; |
||||||
|
|
||||||
|
if (!$style) { |
||||||
|
$build['#theme'] = 'image'; |
||||||
|
return $build; |
||||||
|
} |
||||||
|
|
||||||
|
$build['#width'] = $attributes['width'] ?? NULL; |
||||||
|
$build['#height'] = $attributes['height'] ?? NULL; |
||||||
|
|
||||||
|
if (!$build['#width'] && !$build['#height']) { |
||||||
|
// If an image style is given, image module needs the original image |
||||||
|
// dimensions to calculate image style's width and height and set the |
||||||
|
// attributes. |
||||||
|
// @see https://www.drupal.org/project/twig_tweak/issues/3356042 |
||||||
|
$image = $this->imageFactory->get($file->getFileUri()); |
||||||
|
if ($image->isValid()) { |
||||||
|
$build['#width'] = $image->getWidth(); |
||||||
|
$build['#height'] = $image->getHeight(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if ($responsive) { |
||||||
|
$build['#type'] = 'responsive_image'; |
||||||
|
$build['#responsive_image_style_id'] = $style; |
||||||
|
} |
||||||
|
else { |
||||||
|
$build['#theme'] = 'image_style'; |
||||||
|
$build['#style_name'] = $style; |
||||||
|
} |
||||||
|
|
||||||
|
return $build; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,84 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\twig_tweak\View; |
||||||
|
|
||||||
|
use Drupal\Core\Menu\MenuLinkTreeInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* Menu view builder. |
||||||
|
*/ |
||||||
|
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); |
||||||
|
$build = $this->menuLinkTree->build($tree); |
||||||
|
|
||||||
|
if (!isset($build['#cache']['keys'])) { |
||||||
|
$build['#cache']['keys'] = [ |
||||||
|
'twig_tweak_menu', |
||||||
|
$menu_name, |
||||||
|
'[level]=' . $level, |
||||||
|
'[depth]=' . $depth, |
||||||
|
'[expand]=' . (int) $expand, |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
$build['#cache']['contexts'][] = 'route.menu_active_trails:' . $menu_name; |
||||||
|
|
||||||
|
return $build; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,114 @@ |
|||||||
|
<?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 Drupal\Core\Routing\RouteObjectInterface; |
||||||
|
use Symfony\Component\HttpFoundation\RequestStack; |
||||||
|
|
||||||
|
/** |
||||||
|
* Region view builder. |
||||||
|
*/ |
||||||
|
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 = []; |
||||||
|
|
||||||
|
$entity_type = $this->entityTypeManager->getDefinition('block'); |
||||||
|
$cache_metadata = (new CacheableMetadata()) |
||||||
|
->addCacheTags($entity_type->getListCacheTags()) |
||||||
|
->addCacheContexts($entity_type->getListCacheContexts()); |
||||||
|
|
||||||
|
/** @var \Drupal\block\BlockInterface[] $blocks */ |
||||||
|
foreach ($blocks as $id => $block) { |
||||||
|
$access = $block->access('view', NULL, TRUE); |
||||||
|
$cache_metadata = $cache_metadata->addCacheableDependency($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; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,93 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\Tests\twig_tweak\Kernel; |
||||||
|
|
||||||
|
use Drupal\file\Entity\File; |
||||||
|
use Drupal\file\FileInterface; |
||||||
|
use Drupal\KernelTests\KernelTestBase; |
||||||
|
use Drupal\media\Entity\Media; |
||||||
|
use Drupal\node\Entity\Node; |
||||||
|
use Drupal\Tests\TestFileCreationTrait; |
||||||
|
|
||||||
|
/** |
||||||
|
* A base class of URL and URI extractor tests. |
||||||
|
*/ |
||||||
|
abstract class AbstractExtractorTestCase extends KernelTestBase { |
||||||
|
|
||||||
|
use TestFileCreationTrait; |
||||||
|
|
||||||
|
/** |
||||||
|
* A node to test. |
||||||
|
* |
||||||
|
* @var \Drupal\node\NodeInterface |
||||||
|
*/ |
||||||
|
protected $node; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
protected static $modules = [ |
||||||
|
'twig_tweak', |
||||||
|
'twig_tweak_test', |
||||||
|
'system', |
||||||
|
'views', |
||||||
|
'node', |
||||||
|
'block', |
||||||
|
'image', |
||||||
|
'field', |
||||||
|
'text', |
||||||
|
'media', |
||||||
|
'file', |
||||||
|
'user', |
||||||
|
'filter', |
||||||
|
]; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function setUp(): void { |
||||||
|
parent::setUp(); |
||||||
|
|
||||||
|
$this->installConfig(['node', 'twig_tweak_test']); |
||||||
|
$this->installSchema('file', 'file_usage'); |
||||||
|
$this->installEntitySchema('user'); |
||||||
|
$this->installEntitySchema('file'); |
||||||
|
$this->installEntitySchema('media'); |
||||||
|
|
||||||
|
$test_files = $this->getTestFiles('image'); |
||||||
|
|
||||||
|
$image_file = File::create([ |
||||||
|
'uri' => $test_files[0]->uri, |
||||||
|
'uuid' => 'a2cb2b6f-7bf8-4da4-9de5-316e93487518', |
||||||
|
'status' => FileInterface::STATUS_PERMANENT, |
||||||
|
]); |
||||||
|
$image_file->save(); |
||||||
|
|
||||||
|
$media_file = File::create([ |
||||||
|
'uri' => $test_files[2]->uri, |
||||||
|
'uuid' => '5dd794d0-cb75-4130-9296-838aebc1fe74', |
||||||
|
'status' => FileInterface::STATUS_PERMANENT, |
||||||
|
]); |
||||||
|
$media_file->save(); |
||||||
|
|
||||||
|
$media = Media::create([ |
||||||
|
'bundle' => 'image', |
||||||
|
'name' => 'Image 1', |
||||||
|
'field_media_image' => ['target_id' => $media_file->id()], |
||||||
|
]); |
||||||
|
$media->save(); |
||||||
|
|
||||||
|
$node_values = [ |
||||||
|
'title' => 'Alpha', |
||||||
|
'type' => 'page', |
||||||
|
'field_image' => [ |
||||||
|
'target_id' => $image_file->id(), |
||||||
|
], |
||||||
|
'field_media' => [ |
||||||
|
'target_id' => $media->id(), |
||||||
|
], |
||||||
|
]; |
||||||
|
$this->node = Node::create($node_values); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,45 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\Tests\twig_tweak\Kernel; |
||||||
|
|
||||||
|
use Drupal\KernelTests\KernelTestBase; |
||||||
|
use PHPUnit\Framework\Assert as PHPUnitAssert; |
||||||
|
|
||||||
|
/** |
||||||
|
* A base class for Twig Tweak kernel tests. |
||||||
|
*/ |
||||||
|
abstract class AbstractTestCase extends KernelTestBase { |
||||||
|
|
||||||
|
/** |
||||||
|
* Asserts cache metadata. |
||||||
|
*/ |
||||||
|
protected static function assertCache(array $expected_cache, array $actual_cache): void { |
||||||
|
self::sortCache($expected_cache); |
||||||
|
self::sortCache($actual_cache); |
||||||
|
PHPUnitAssert::assertSame($expected_cache, $actual_cache); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Asserts render array. |
||||||
|
*/ |
||||||
|
protected static function assertRenderArray(array $expected_build, array $actual_build): void { |
||||||
|
self::sortCache($expected_build['#cache']); |
||||||
|
self::sortCache($actual_build['#cache']); |
||||||
|
PHPUnitAssert::assertSame($expected_build, $actual_build); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Sort cache metadata. |
||||||
|
* |
||||||
|
* @see https://www.drupal.org/node/3230171 |
||||||
|
*/ |
||||||
|
private static function sortCache(array &$cache): void { |
||||||
|
if (\array_key_exists('tags', $cache)) { |
||||||
|
sort($cache['tags']); |
||||||
|
} |
||||||
|
if (\array_key_exists('contexts', $cache)) { |
||||||
|
sort($cache['contexts']); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,171 @@ |
|||||||
|
<?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} |
||||||
|
*/ |
||||||
|
protected 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', |
||||||
|
'#id' => NULL, |
||||||
|
'#attributes' => [ |
||||||
|
'id' => 'foo', |
||||||
|
], |
||||||
|
'#contextual_links' => [], |
||||||
|
'#configuration' => [ |
||||||
|
'id' => 'twig_tweak_test_foo', |
||||||
|
'label' => '', |
||||||
|
'label_display' => 'visible', |
||||||
|
'provider' => 'twig_tweak_test', |
||||||
|
'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, |
||||||
|
'keys' => [ |
||||||
|
'twig_tweak_block', |
||||||
|
'twig_tweak_test_foo', |
||||||
|
'[configuration]=04c46ea912d2866a3a36c67326da1ef38f1c93cc822d6c45e1639f3decdebbdc', |
||||||
|
'[wrapper]=1', |
||||||
|
], |
||||||
|
], |
||||||
|
]; |
||||||
|
|
||||||
|
// @todo Remove this once we drop support for Drupal 9.2. |
||||||
|
// @see https://www.drupal.org/node/3230199 |
||||||
|
if (\version_compare(\Drupal::VERSION, '9.3.0-dev', '<')) { |
||||||
|
$expected_build['#configuration'] = [ |
||||||
|
'id' => 'twig_tweak_test_foo', |
||||||
|
'label' => '', |
||||||
|
'provider' => 'twig_tweak_test', |
||||||
|
'label_display' => 'visible', |
||||||
|
'content' => 'Foo', |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
self::assertSame($expected_build, $build); |
||||||
|
self::assertSame('<div id="foo">Foo</div>', $this->renderPlain($build)); |
||||||
|
|
||||||
|
// -- Non-default configuration. |
||||||
|
$configuration = [ |
||||||
|
'content' => 'Bar', |
||||||
|
'label' => 'Example', |
||||||
|
'id' => 'example', |
||||||
|
]; |
||||||
|
$build = $view_builder->build('twig_tweak_test_foo', $configuration); |
||||||
|
$expected_build['content']['#markup'] = 'Bar'; |
||||||
|
$expected_build['#configuration']['label'] = 'Example'; |
||||||
|
$expected_build['#configuration']['content'] = 'Bar'; |
||||||
|
$expected_build['#configuration']['id'] = 'example'; |
||||||
|
$expected_build['#id'] = 'example'; |
||||||
|
$expected_build['#cache']['keys'] = [ |
||||||
|
'twig_tweak_block', |
||||||
|
'twig_tweak_test_foo', |
||||||
|
'[configuration]=8e53716fcf7e5d5c45effd55e9b2a267bbaf333f7253766f572d58e4f7991b36', |
||||||
|
'[wrapper]=1', |
||||||
|
]; |
||||||
|
self::assertSame($expected_build, $build); |
||||||
|
self::assertSame('<div id="block-example"><h2>Example</h2>Bar</div>', $this->renderPlain($build)); |
||||||
|
|
||||||
|
// -- Without wrapper. |
||||||
|
$build = $view_builder->build('twig_tweak_test_foo', [], FALSE); |
||||||
|
$expected_build = [ |
||||||
|
'content' => [ |
||||||
|
'#markup' => 'Foo', |
||||||
|
// Since the block is built without wrapper #attributes must remain in |
||||||
|
// 'content' element. |
||||||
|
'#attributes' => [ |
||||||
|
'id' => 'foo', |
||||||
|
], |
||||||
|
'#cache' => [ |
||||||
|
'contexts' => ['url'], |
||||||
|
'tags' => ['tag_from_build'], |
||||||
|
], |
||||||
|
], |
||||||
|
'#cache' => [ |
||||||
|
'contexts' => ['user'], |
||||||
|
'tags' => ['tag_from_blockAccess'], |
||||||
|
'max-age' => 35, |
||||||
|
'keys' => [ |
||||||
|
'twig_tweak_block', |
||||||
|
'twig_tweak_test_foo', |
||||||
|
'[configuration]=04c46ea912d2866a3a36c67326da1ef38f1c93cc822d6c45e1639f3decdebbdc', |
||||||
|
'[wrapper]=0', |
||||||
|
], |
||||||
|
], |
||||||
|
]; |
||||||
|
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, |
||||||
|
'keys' => [ |
||||||
|
'twig_tweak_block', |
||||||
|
'twig_tweak_test_foo', |
||||||
|
'[configuration]=04c46ea912d2866a3a36c67326da1ef38f1c93cc822d6c45e1639f3decdebbdc', |
||||||
|
'[wrapper]=1', |
||||||
|
], |
||||||
|
], |
||||||
|
]; |
||||||
|
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))); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,99 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\Tests\twig_tweak\Kernel; |
||||||
|
|
||||||
|
use Drupal\Core\Cache\CacheableMetadata; |
||||||
|
|
||||||
|
/** |
||||||
|
* A test for Cache Metadata Extractor service. |
||||||
|
* |
||||||
|
* @group twig_tweak |
||||||
|
*/ |
||||||
|
final class CacheMetadataExtractorTest extends AbstractTestCase { |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
protected static $modules = [ |
||||||
|
'twig_tweak', |
||||||
|
]; |
||||||
|
|
||||||
|
/** |
||||||
|
* Test callback. |
||||||
|
*/ |
||||||
|
public function testCacheMetadataExtractor(): void { |
||||||
|
|
||||||
|
$extractor = $this->container->get('twig_tweak.cache_metadata_extractor'); |
||||||
|
|
||||||
|
// -- Object. |
||||||
|
$input = new CacheableMetadata(); |
||||||
|
$input->setCacheMaxAge(5); |
||||||
|
$input->setCacheContexts(['url', 'user.permissions']); |
||||||
|
$input->setCacheTags(['node', 'node.view']); |
||||||
|
|
||||||
|
$build = $extractor->extractCacheMetadata($input); |
||||||
|
$expected_build['#cache'] = [ |
||||||
|
'contexts' => ['url', 'user.permissions'], |
||||||
|
'tags' => ['node', 'node.view'], |
||||||
|
'max-age' => 5, |
||||||
|
]; |
||||||
|
self::assertSame($expected_build, $build); |
||||||
|
|
||||||
|
// -- Render array. |
||||||
|
$input = [ |
||||||
|
'foo' => [ |
||||||
|
'#cache' => [ |
||||||
|
'tags' => ['foo', 'foo.view'], |
||||||
|
], |
||||||
|
'bar' => [ |
||||||
|
0 => [ |
||||||
|
'#cache' => [ |
||||||
|
'tags' => ['bar-0'], |
||||||
|
], |
||||||
|
], |
||||||
|
1 => [ |
||||||
|
'#cache' => [ |
||||||
|
'tags' => ['bar-1'], |
||||||
|
], |
||||||
|
], |
||||||
|
'#cache' => [ |
||||||
|
'tags' => ['bar', 'bar.view'], |
||||||
|
'contexts' => ['url.path'], |
||||||
|
'max-age' => 10, |
||||||
|
], |
||||||
|
], |
||||||
|
], |
||||||
|
'#cache' => [ |
||||||
|
'contexts' => ['url', 'user.permissions'], |
||||||
|
'tags' => ['node', 'node.view'], |
||||||
|
'max-age' => 20, |
||||||
|
], |
||||||
|
]; |
||||||
|
$build = $extractor->extractCacheMetadata($input); |
||||||
|
|
||||||
|
$expected_build = [ |
||||||
|
'#cache' => [ |
||||||
|
'contexts' => ['url', 'url.path', 'user.permissions'], |
||||||
|
'tags' => [ |
||||||
|
'bar', |
||||||
|
'bar-0', |
||||||
|
'bar-1', |
||||||
|
'bar.view', |
||||||
|
'foo', |
||||||
|
'foo.view', |
||||||
|
'node', |
||||||
|
'node.view', |
||||||
|
], |
||||||
|
'max-age' => 10, |
||||||
|
], |
||||||
|
]; |
||||||
|
self::assertRenderArray($expected_build, $build); |
||||||
|
|
||||||
|
// -- Wrong type. |
||||||
|
$exception = new \InvalidArgumentException('The input should be either instance of Drupal\Core\Cache\CacheableDependencyInterface or array. stdClass was given.'); |
||||||
|
self::expectExceptionObject($exception); |
||||||
|
/* @noinspection PhpParamsInspection */ |
||||||
|
$extractor->extractCacheMetadata(new \stdClass()); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,130 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\Tests\twig_tweak\Kernel; |
||||||
|
|
||||||
|
use Drupal\Core\Cache\Cache; |
||||||
|
use Drupal\node\Entity\Node; |
||||||
|
use Drupal\node\Entity\NodeType; |
||||||
|
use Drupal\Tests\user\Traits\UserCreationTrait; |
||||||
|
|
||||||
|
/** |
||||||
|
* A test for EntityFormViewBuilder. |
||||||
|
* |
||||||
|
* @group twig_tweak |
||||||
|
*/ |
||||||
|
final class EntityFormViewBuilderTest extends AbstractTestCase { |
||||||
|
|
||||||
|
use UserCreationTrait; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
protected 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 testEntityFormViewBuilder(): 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::assertCache($expected_cache, $build['#cache']); |
||||||
|
self::assertStringContainsString('<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::assertCache($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::assertCache($expected_cache, $build['#cache']); |
||||||
|
self::assertStringContainsString('<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); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,202 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\Tests\twig_tweak\Kernel; |
||||||
|
|
||||||
|
use Drupal\Core\Cache\Cache; |
||||||
|
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 AbstractTestCase { |
||||||
|
|
||||||
|
use UserCreationTrait; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
protected 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::assertCache($expected_cache, $build['#cache']); |
||||||
|
|
||||||
|
$expected_html = <<< 'HTML' |
||||||
|
<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::assertCache($expected_cache, $build['#cache']); |
||||||
|
|
||||||
|
$expected_html = <<< 'HTML' |
||||||
|
<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::assertCache($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::assertCache($expected_cache, $build['#cache']); |
||||||
|
|
||||||
|
$expected_html = <<< 'HTML' |
||||||
|
<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)); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,140 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\Tests\twig_tweak\Kernel; |
||||||
|
|
||||||
|
use Drupal\Core\Cache\Cache; |
||||||
|
use Drupal\node\Entity\Node; |
||||||
|
use Drupal\node\Entity\NodeType; |
||||||
|
use Drupal\Tests\user\Traits\UserCreationTrait; |
||||||
|
|
||||||
|
/** |
||||||
|
* A test for FieldViewBuilder. |
||||||
|
* |
||||||
|
* @group twig_tweak |
||||||
|
*/ |
||||||
|
final class FieldViewBuilderTest extends AbstractTestCase { |
||||||
|
|
||||||
|
use UserCreationTrait; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
protected 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::assertCache($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::assertCache($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::assertCache($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; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,257 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\Tests\twig_tweak\Kernel; |
||||||
|
|
||||||
|
use Drupal\Core\Cache\Cache; |
||||||
|
use Drupal\Core\DependencyInjection\ContainerBuilder; |
||||||
|
use Drupal\Core\File\FileSystemInterface; |
||||||
|
use Drupal\file\Entity\File; |
||||||
|
use Drupal\file\FileInterface; |
||||||
|
use Drupal\image\Entity\ImageStyle; |
||||||
|
use Drupal\responsive_image\Entity\ResponsiveImageStyle; |
||||||
|
|
||||||
|
/** |
||||||
|
* A test class for testing the image view builder. |
||||||
|
* |
||||||
|
* @group twig_tweak |
||||||
|
*/ |
||||||
|
final class ImageViewBuilderTest extends AbstractTestCase { |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
protected static $modules = [ |
||||||
|
'twig_tweak', |
||||||
|
'twig_tweak_test', |
||||||
|
'user', |
||||||
|
'system', |
||||||
|
'file', |
||||||
|
'image', |
||||||
|
'responsive_image', |
||||||
|
'breakpoint', |
||||||
|
]; |
||||||
|
|
||||||
|
/** |
||||||
|
* The public image URI. |
||||||
|
* |
||||||
|
* @var string |
||||||
|
*/ |
||||||
|
protected string $publicImageUri; |
||||||
|
|
||||||
|
/** |
||||||
|
* The private image URI. |
||||||
|
* |
||||||
|
* @var string |
||||||
|
*/ |
||||||
|
protected string $privateImageUri; |
||||||
|
|
||||||
|
/** |
||||||
|
* The public image file. |
||||||
|
* |
||||||
|
* @var \Drupal\file\FileInterface |
||||||
|
*/ |
||||||
|
protected $publicImage; |
||||||
|
|
||||||
|
/** |
||||||
|
* The private image file. |
||||||
|
* |
||||||
|
* @var \Drupal\file\FileInterface |
||||||
|
*/ |
||||||
|
protected $privateImage; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function setUp(): void { |
||||||
|
parent::setUp(); |
||||||
|
|
||||||
|
$this->installEntitySchema('file'); |
||||||
|
$this->installSchema('file', 'file_usage'); |
||||||
|
|
||||||
|
$file_system = $this->container->get('file_system'); |
||||||
|
|
||||||
|
$file_system->prepareDirectory($this->siteDirectory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); |
||||||
|
$private_directory = $this->siteDirectory . '/private'; |
||||||
|
|
||||||
|
$file_system->prepareDirectory($private_directory, FileSystemInterface::CREATE_DIRECTORY); |
||||||
|
$this->setSetting('file_private_path', $private_directory); |
||||||
|
|
||||||
|
$image_style = ImageStyle::create([ |
||||||
|
'name' => 'small', |
||||||
|
'label' => 'Small', |
||||||
|
]); |
||||||
|
// Add a crop effect: |
||||||
|
$image_style->addImageEffect([ |
||||||
|
'id' => 'image_resize', |
||||||
|
'data' => [ |
||||||
|
'width' => 10, |
||||||
|
'height' => 10, |
||||||
|
], |
||||||
|
'weight' => 0, |
||||||
|
]); |
||||||
|
$image_style->save(); |
||||||
|
|
||||||
|
$responsive_image_style = ResponsiveImageStyle::create([ |
||||||
|
'id' => 'wide', |
||||||
|
'label' => 'Wide', |
||||||
|
'breakpoint_group' => 'twig_tweak_image_view_builder', |
||||||
|
'fallback_image_style' => 'small', |
||||||
|
]); |
||||||
|
$responsive_image_style->save(); |
||||||
|
|
||||||
|
// Create a copy of a test image file in root. Original sizes: 40x20px. |
||||||
|
$this->publicImageUri = 'public://image-test-do.jpg'; |
||||||
|
$file_system->copy('core/tests/fixtures/files/image-test.jpg', $this->publicImageUri, FileSystemInterface::EXISTS_REPLACE); |
||||||
|
$this->assertFileExists($this->publicImageUri); |
||||||
|
$this->publicImage = File::create([ |
||||||
|
'uri' => $this->publicImageUri, |
||||||
|
'status' => FileInterface::STATUS_PERMANENT, |
||||||
|
]); |
||||||
|
$this->publicImage->save(); |
||||||
|
|
||||||
|
// Create a copy of a test image file in root. Original sizes: 40x20px. |
||||||
|
$this->privateImageUri = 'private://image-test-do.png'; |
||||||
|
$file_system->copy('core/tests/fixtures/files/image-test.png', $this->privateImageUri, FileSystemInterface::EXISTS_REPLACE); |
||||||
|
$this->assertFileExists($this->privateImageUri); |
||||||
|
$this->privateImage = File::create([ |
||||||
|
'uri' => $this->privateImageUri, |
||||||
|
'status' => FileInterface::STATUS_PERMANENT, |
||||||
|
]); |
||||||
|
$this->privateImage->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'); |
||||||
|
|
||||||
|
$uri = $this->publicImage->getFileUri(); |
||||||
|
$image = \Drupal::service('image.factory')->get($uri); |
||||||
|
$imageOriginalWidth = $image->getWidth(); |
||||||
|
$imageOriginalHeight = $image->getHeight(); |
||||||
|
self::assertTrue($image->isValid()); |
||||||
|
self::assertEquals(40, $imageOriginalWidth); |
||||||
|
self::assertEquals(20, $imageOriginalHeight); |
||||||
|
|
||||||
|
// -- Without style. |
||||||
|
$build = $view_builder->build($this->publicImage); |
||||||
|
$expected_build = [ |
||||||
|
'#uri' => $this->publicImageUri, |
||||||
|
'#attributes' => [], |
||||||
|
'#theme' => 'image', |
||||||
|
'#cache' => [ |
||||||
|
'contexts' => [ |
||||||
|
'user', |
||||||
|
'user.permissions', |
||||||
|
], |
||||||
|
'tags' => [ |
||||||
|
'file:1', |
||||||
|
'tag_for_' . $this->publicImageUri, |
||||||
|
], |
||||||
|
'max-age' => 70, |
||||||
|
], |
||||||
|
]; |
||||||
|
self::assertRenderArray($expected_build, $build); |
||||||
|
self::assertSame('<img src="/files/image-test-do.jpg" alt="" />', $this->renderPlain($build)); |
||||||
|
|
||||||
|
// -- With style. |
||||||
|
$build = $view_builder->build($this->publicImage, 'small', ['alt' => 'Image Test Do']); |
||||||
|
$expected_build = [ |
||||||
|
'#uri' => $this->publicImageUri, |
||||||
|
'#attributes' => ['alt' => 'Image Test Do'], |
||||||
|
'#width' => $imageOriginalWidth, |
||||||
|
'#height' => $imageOriginalHeight, |
||||||
|
'#theme' => 'image_style', |
||||||
|
'#style_name' => 'small', |
||||||
|
'#cache' => [ |
||||||
|
'contexts' => [ |
||||||
|
'user', |
||||||
|
'user.permissions', |
||||||
|
], |
||||||
|
'tags' => [ |
||||||
|
'file:1', |
||||||
|
'tag_for_' . $this->publicImageUri, |
||||||
|
], |
||||||
|
'max-age' => 70, |
||||||
|
], |
||||||
|
]; |
||||||
|
self::assertRenderArray($expected_build, $build); |
||||||
|
self::assertSame('<img alt="Image Test Do" src="/files/styles/small/public/image-test-do.jpg?itok=abc" width="10" height="10" loading="lazy" />', $this->renderPlain($build)); |
||||||
|
|
||||||
|
// -- With responsive style. |
||||||
|
$build = $view_builder->build($this->publicImage, 'wide', ['alt' => 'Image Test Do'], TRUE); |
||||||
|
$expected_build = [ |
||||||
|
'#uri' => $this->publicImageUri, |
||||||
|
'#attributes' => ['alt' => 'Image Test Do'], |
||||||
|
'#width' => $imageOriginalWidth, |
||||||
|
'#height' => $imageOriginalHeight, |
||||||
|
'#type' => 'responsive_image', |
||||||
|
'#responsive_image_style_id' => 'wide', |
||||||
|
'#cache' => [ |
||||||
|
'contexts' => [ |
||||||
|
'user', |
||||||
|
'user.permissions', |
||||||
|
], |
||||||
|
'tags' => [ |
||||||
|
'file:1', |
||||||
|
'tag_for_' . $this->publicImageUri, |
||||||
|
], |
||||||
|
'max-age' => 70, |
||||||
|
], |
||||||
|
]; |
||||||
|
self::assertRenderArray($expected_build, $build); |
||||||
|
self::assertSame('<picture><img src="/files/styles/small/public/image-test-do.jpg?itok=abc" width="10" height="10" alt="Image Test Do" loading="lazy" /></picture>', $this->renderPlain($build)); |
||||||
|
|
||||||
|
// -- Private image with access check. |
||||||
|
$build = $view_builder->build($this->privateImage); |
||||||
|
$expected_build = [ |
||||||
|
'#cache' => [ |
||||||
|
'contexts' => ['user'], |
||||||
|
'tags' => [ |
||||||
|
'file:2', |
||||||
|
'tag_for_' . $this->privateImageUri, |
||||||
|
], |
||||||
|
'max-age' => 70, |
||||||
|
], |
||||||
|
]; |
||||||
|
self::assertRenderArray($expected_build, $build); |
||||||
|
self::assertSame('', $this->renderPlain($build)); |
||||||
|
|
||||||
|
// -- Private image without access check. |
||||||
|
$build = $view_builder->build($this->privateImage, NULL, [], FALSE, FALSE); |
||||||
|
$expected_build = [ |
||||||
|
'#uri' => $this->privateImageUri, |
||||||
|
'#attributes' => [], |
||||||
|
'#theme' => 'image', |
||||||
|
'#cache' => [ |
||||||
|
'contexts' => [], |
||||||
|
'tags' => ['file:2'], |
||||||
|
'max-age' => Cache::PERMANENT, |
||||||
|
], |
||||||
|
]; |
||||||
|
self::assertRenderArray($expected_build, $build); |
||||||
|
self::assertSame('<img src="/files/image-test-do.png" 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); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -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} |
||||||
|
*/ |
||||||
|
protected 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); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,154 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\Tests\twig_tweak\Kernel; |
||||||
|
|
||||||
|
use Drupal\block\Entity\Block; |
||||||
|
use Drupal\Component\Utility\Html; |
||||||
|
use Drupal\Core\Cache\Cache; |
||||||
|
use Drupal\Tests\user\Traits\UserCreationTrait; |
||||||
|
|
||||||
|
/** |
||||||
|
* A test for RegionViewBuilder. |
||||||
|
* |
||||||
|
* @group twig_tweak |
||||||
|
*/ |
||||||
|
final class RegionViewBuilderTest extends AbstractTestCase { |
||||||
|
|
||||||
|
use UserCreationTrait; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
protected 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(['stark']); |
||||||
|
|
||||||
|
$values = [ |
||||||
|
'id' => 'public_block', |
||||||
|
'plugin' => 'system_powered_by_block', |
||||||
|
'theme' => 'stark', |
||||||
|
'region' => 'sidebar_first', |
||||||
|
]; |
||||||
|
Block::create($values)->save(); |
||||||
|
|
||||||
|
$values = [ |
||||||
|
'id' => 'private_block', |
||||||
|
'plugin' => 'system_powered_by_block', |
||||||
|
'theme' => 'stark', |
||||||
|
'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 'stark' is not a default theme. |
||||||
|
$expected_build = [ |
||||||
|
'#cache' => [ |
||||||
|
'contexts' => [], |
||||||
|
'tags' => ['config:block_list'], |
||||||
|
'max-age' => Cache::PERMANENT, |
||||||
|
], |
||||||
|
]; |
||||||
|
self::assertSame($expected_build, $build); |
||||||
|
|
||||||
|
// Specify the theme name explicitly. |
||||||
|
$build = $view_builder->build('sidebar_first', 'stark'); |
||||||
|
$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', |
||||||
|
'config:block_list', |
||||||
|
'tag_for_private_block', |
||||||
|
'tag_for_public_block', |
||||||
|
], |
||||||
|
'max-age' => 123, |
||||||
|
], |
||||||
|
]; |
||||||
|
|
||||||
|
self::assertRenderArray($expected_build, $build); |
||||||
|
|
||||||
|
$expected_html = <<< 'HTML' |
||||||
|
<div> |
||||||
|
<div id="block-public-block"> |
||||||
|
<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 'stark' 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', 'stark') |
||||||
|
->save(); |
||||||
|
|
||||||
|
$build = $view_builder->build('sidebar_first'); |
||||||
|
self::assertRenderArray($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)); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,61 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\Tests\twig_tweak\Kernel; |
||||||
|
|
||||||
|
/** |
||||||
|
* A test for URI extractor service. |
||||||
|
* |
||||||
|
* @group twig_tweak |
||||||
|
*/ |
||||||
|
final class UriExtractorTest extends AbstractExtractorTestCase { |
||||||
|
|
||||||
|
/** |
||||||
|
* Test callback. |
||||||
|
*/ |
||||||
|
public function testUriExtractor(): void { |
||||||
|
|
||||||
|
$extractor = $this->container->get('twig_tweak.uri_extractor'); |
||||||
|
|
||||||
|
$url = $extractor->extractUri(NULL); |
||||||
|
self::assertNull($url); |
||||||
|
|
||||||
|
$url = $extractor->extractUri($this->node); |
||||||
|
self::assertNull($url); |
||||||
|
|
||||||
|
$url = $extractor->extractUri($this->node->get('title')); |
||||||
|
self::assertNull($url); |
||||||
|
|
||||||
|
$url = $extractor->extractUri($this->node->get('field_image')[0]); |
||||||
|
self::assertSame('public://image-test.png', $url); |
||||||
|
|
||||||
|
$url = $extractor->extractUri($this->node->get('field_image')[1]); |
||||||
|
self::assertNull($url); |
||||||
|
|
||||||
|
$url = $extractor->extractUri($this->node->get('field_image')); |
||||||
|
self::assertSame('public://image-test.png', $url); |
||||||
|
|
||||||
|
$url = $extractor->extractUri($this->node->get('field_image')->entity); |
||||||
|
self::assertSame('public://image-test.png', $url); |
||||||
|
|
||||||
|
$this->node->get('field_image')->removeItem(0); |
||||||
|
$url = $extractor->extractUri($this->node->get('field_image')); |
||||||
|
self::assertNull($url); |
||||||
|
|
||||||
|
$url = $extractor->extractUri($this->node->get('field_media')[0]); |
||||||
|
self::assertSame('public://image-test.gif', $url); |
||||||
|
|
||||||
|
$url = $extractor->extractUri($this->node->get('field_media')[1]); |
||||||
|
self::assertNull($url); |
||||||
|
|
||||||
|
$url = $extractor->extractUri($this->node->get('field_media')); |
||||||
|
self::assertSame('public://image-test.gif', $url); |
||||||
|
|
||||||
|
$url = $extractor->extractUri($this->node->get('field_media')->entity); |
||||||
|
self::assertSame('public://image-test.gif', $url); |
||||||
|
|
||||||
|
$this->node->get('field_media')->removeItem(0); |
||||||
|
$url = $extractor->extractUri($this->node->get('field_media')); |
||||||
|
self::assertNull($url); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,87 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\Tests\twig_tweak\Kernel; |
||||||
|
|
||||||
|
/** |
||||||
|
* A test for URL Extractor service. |
||||||
|
* |
||||||
|
* @group twig_tweak |
||||||
|
*/ |
||||||
|
final class UrlExtractorTest extends AbstractExtractorTestCase { |
||||||
|
|
||||||
|
/** |
||||||
|
* Test callback. |
||||||
|
*/ |
||||||
|
public function testUrlExtractor(): void { |
||||||
|
|
||||||
|
$extractor = $this->container->get('twig_tweak.url_extractor'); |
||||||
|
$base_url = \Drupal::service('file_url_generator')->generateAbsoluteString(''); |
||||||
|
|
||||||
|
$request = \Drupal::request(); |
||||||
|
$absolute_url = "{$request->getScheme()}://{$request->getHost()}/foo/bar.txt"; |
||||||
|
$url = $extractor->extractUrl($absolute_url); |
||||||
|
self::assertSame('/foo/bar.txt', $url); |
||||||
|
|
||||||
|
$url = $extractor->extractUrl($absolute_url, FALSE); |
||||||
|
self::assertSame($base_url . 'foo/bar.txt', $url); |
||||||
|
|
||||||
|
$url = $extractor->extractUrl('foo/bar.jpg'); |
||||||
|
self::assertSame('/foo/bar.jpg', $url); |
||||||
|
|
||||||
|
$url = $extractor->extractUrl('foo/bar.jpg', FALSE); |
||||||
|
self::assertSame($base_url . 'foo/bar.jpg', $url); |
||||||
|
|
||||||
|
$url = $extractor->extractUrl(''); |
||||||
|
self::assertSame('/', $url); |
||||||
|
|
||||||
|
$url = $extractor->extractUrl('', FALSE); |
||||||
|
self::assertSame($base_url, $url); |
||||||
|
|
||||||
|
$url = $extractor->extractUrl(NULL); |
||||||
|
self::assertNull($url); |
||||||
|
|
||||||
|
$url = $extractor->extractUrl($this->node); |
||||||
|
self::assertNull($url); |
||||||
|
|
||||||
|
$url = $extractor->extractUrl($this->node->get('title')); |
||||||
|
self::assertNull($url); |
||||||
|
|
||||||
|
$url = $extractor->extractUrl($this->node->get('field_image')[0]); |
||||||
|
self::assertStringEndsWith('/files/image-test.png', $url); |
||||||
|
self::assertStringNotContainsString($base_url, $url); |
||||||
|
|
||||||
|
$url = $extractor->extractUrl($this->node->get('field_image')[0], FALSE); |
||||||
|
self::assertStringStartsWith($base_url, $url); |
||||||
|
self::assertStringEndsWith('/files/image-test.png', $url); |
||||||
|
|
||||||
|
$url = $extractor->extractUrl($this->node->get('field_image')[1]); |
||||||
|
self::assertNull($url); |
||||||
|
|
||||||
|
$url = $extractor->extractUrl($this->node->get('field_image')); |
||||||
|
self::assertStringEndsWith('/files/image-test.png', $url); |
||||||
|
|
||||||
|
$url = $extractor->extractUrl($this->node->get('field_image')->entity); |
||||||
|
self::assertStringEndsWith('/files/image-test.png', $url); |
||||||
|
|
||||||
|
$this->node->get('field_image')->removeItem(0); |
||||||
|
$url = $extractor->extractUrl($this->node->get('field_image')); |
||||||
|
self::assertNull($url); |
||||||
|
|
||||||
|
$url = $extractor->extractUrl($this->node->get('field_media')[0]); |
||||||
|
self::assertStringEndsWith('/files/image-test.gif', $url); |
||||||
|
|
||||||
|
$url = $extractor->extractUrl($this->node->get('field_media')[1]); |
||||||
|
self::assertNull($url); |
||||||
|
|
||||||
|
$url = $extractor->extractUrl($this->node->get('field_media')); |
||||||
|
self::assertStringEndsWith('/files/image-test.gif', $url); |
||||||
|
|
||||||
|
$url = $extractor->extractUrl($this->node->get('field_media')->entity); |
||||||
|
self::assertStringEndsWith('/files/image-test.gif', $url); |
||||||
|
|
||||||
|
$this->node->get('field_media')->removeItem(0); |
||||||
|
$url = $extractor->extractUrl($this->node->get('field_media')); |
||||||
|
self::assertNull($url); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -1,17 +0,0 @@ |
|||||||
langcode: en |
|
||||||
status: true |
|
||||||
dependencies: |
|
||||||
theme: |
|
||||||
- classy |
|
||||||
id: classy_page_title |
|
||||||
theme: classy |
|
||||||
region: sidebar_first |
|
||||||
weight: 0 |
|
||||||
provider: null |
|
||||||
plugin: page_title_block |
|
||||||
settings: |
|
||||||
id: page_title_block |
|
||||||
label: 'Page title' |
|
||||||
provider: core |
|
||||||
label_display: '0' |
|
||||||
visibility: { } |
|
@ -1,19 +0,0 @@ |
|||||||
langcode: en |
|
||||||
status: true |
|
||||||
dependencies: |
|
||||||
module: |
|
||||||
- system |
|
||||||
theme: |
|
||||||
- classy |
|
||||||
id: classy_status_messages |
|
||||||
theme: classy |
|
||||||
region: sidebar_first |
|
||||||
weight: 10 |
|
||||||
provider: null |
|
||||||
plugin: system_messages_block |
|
||||||
settings: |
|
||||||
id: system_messages_block |
|
||||||
label: 'Status messages' |
|
||||||
provider: system |
|
||||||
label_display: '0' |
|
||||||
visibility: { } |
|
@ -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 |
@ -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 |
@ -1,7 +1,7 @@ |
|||||||
langcode: en |
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 |
||||||
|
@ -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']; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,54 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace Drupal\twig_tweak_test\Plugin\Block; |
||||||
|
|
||||||
|
use Drupal\Core\Access\AccessResult; |
||||||
|
use Drupal\Core\Block\BlockBase; |
||||||
|
use Drupal\Core\Session\AccountInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* Provides a foo block. |
||||||
|
* |
||||||
|
* @Block( |
||||||
|
* id = "twig_tweak_test_foo", |
||||||
|
* admin_label = @Translation("Foo"), |
||||||
|
* category = @Translation("Twig Tweak") |
||||||
|
* ) |
||||||
|
*/ |
||||||
|
final class FooBlock extends BlockBase { |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function defaultConfiguration(): array { |
||||||
|
return ['content' => 'Foo']; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
protected function blockAccess(AccountInterface $account): AccessResult { |
||||||
|
$result = AccessResult::allowedIf($account->getAccountName() == 'User 1'); |
||||||
|
$result->addCacheTags(['tag_from_' . __FUNCTION__]); |
||||||
|
$result->setCacheMaxAge(35); |
||||||
|
$result->cachePerUser(); |
||||||
|
return $result; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function build(): array { |
||||||
|
return [ |
||||||
|
'#markup' => $this->getConfiguration()['content'], |
||||||
|
'#attributes' => [ |
||||||
|
'id' => 'foo', |
||||||
|
], |
||||||
|
'#cache' => [ |
||||||
|
'contexts' => ['url'], |
||||||
|
'tags' => ['tag_from_' . __FUNCTION__], |
||||||
|
], |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -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' |
@ -0,0 +1,64 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
/** |
||||||
|
* @file |
||||||
|
* Hooks specific to the Twig Tweak module. |
||||||
|
*/ |
||||||
|
|
||||||
|
use Drupal\Component\Utility\Unicode; |
||||||
|
use Drupal\node\NodeInterface; |
||||||
|
use Twig\TwigFilter; |
||||||
|
use Twig\TwigFunction; |
||||||
|
use Twig\TwigTest; |
||||||
|
|
||||||
|
/** |
||||||
|
* @addtogroup hooks |
||||||
|
* @{ |
||||||
|
*/ |
||||||
|
|
||||||
|
/** |
||||||
|
* Alters Twig Tweak functions. |
||||||
|
* |
||||||
|
* @param \Twig\TwigFunction[] $functions |
||||||
|
* Twig functions to alter. |
||||||
|
*/ |
||||||
|
function hook_twig_tweak_functions_alter(array &$functions): void { |
||||||
|
// @phpcs:disable |
||||||
|
// A simple way to implement lazy loaded global variables. |
||||||
|
$callback = static fn (string $name): ?string => |
||||||
|
match ($name) { |
||||||
|
'foo' => 'Foo', |
||||||
|
'bar' => 'Bar', |
||||||
|
default => NULL, |
||||||
|
}; |
||||||
|
$functions[] = new TwigFunction('var', $callback); |
||||||
|
// @phpcs:enable |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Alters Twig Tweak filters. |
||||||
|
* |
||||||
|
* @param \Twig\TwigFilter[] $filters |
||||||
|
* Twig filters to alter. |
||||||
|
*/ |
||||||
|
function hook_twig_tweak_filters_alter(array &$filters): void { |
||||||
|
$filters[] = new TwigFilter('str_pad', 'str_pad'); |
||||||
|
$filters[] = new TwigFilter('ucfirst', [Unicode::class, 'ucfirst']); |
||||||
|
$filters[] = new TwigFilter('lcfirst', [Unicode::class, 'lcfirst']); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Alters Twig Tweak tests. |
||||||
|
* |
||||||
|
* @param \Twig\TwigTest[] $tests |
||||||
|
* Twig tests to alter. |
||||||
|
*/ |
||||||
|
function hook_twig_tweak_tests_alter(array &$tests): void { |
||||||
|
$callback = static fn (NodeInterface $node): bool => |
||||||
|
\Drupal::time()->getRequestTime() - $node->getCreatedTime() > 3600 * 24 * 365; |
||||||
|
$tests[] = new TwigTest('outdated', $callback); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @} End of "addtogroup hooks". |
||||||
|
*/ |
@ -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 || ^10 |
||||||
core_version_requirement: ^8 || ^9 |
|
||||||
dependencies: |
dependencies: |
||||||
- drupal:system (>=8.7) |
- drupal:system (>=9.0) |
||||||
|
@ -1,5 +1,45 @@ |
|||||||
services: |
services: |
||||||
twig_tweak.twig_extension: |
twig_tweak.twig_extension: |
||||||
class: Drupal\twig_tweak\TwigExtension |
class: Drupal\twig_tweak\TwigTweakExtension |
||||||
|
arguments: ['@module_handler', '@theme.manager'] |
||||||
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', '@entity.repository'] |
||||||
|
|
||||||
|
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 |
||||||
|
arguments: ['@image.factory'] |
||||||
|
|
||||||
|
twig_tweak.url_extractor: |
||||||
|
class: Drupal\twig_tweak\UrlExtractor |
||||||
|
arguments: ['@entity_type.manager', '@file_url_generator'] |
||||||
|
|
||||||
|
twig_tweak.uri_extractor: |
||||||
|
class: Drupal\twig_tweak\UriExtractor |
||||||
|
arguments: ['@entity_type.manager'] |
||||||
|
|
||||||
|
twig_tweak.cache_metadata_extractor: |
||||||
|
class: Drupal\twig_tweak\CacheMetadataExtractor |
||||||
|
Loading…
Reference in new issue