Compare commits
164 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 |
neclimdul | 948ce4e371 | 5 years ago |
Chi | e72e7779c9 | 5 years ago |
Chi | f910f7aa7f | 5 years ago |
Chi | d84e7e30af | 5 years ago |
Chi | fa6b362aed | 5 years ago |
Chi | 5372b821b2 | 5 years ago |
Chi | 5ab80a4c36 | 5 years ago |
Chi | 50c7bbb671 | 5 years ago |
Chi | 27237912ef | 5 years ago |
Chi | fa18205d16 | 5 years ago |
Chi | cde0be4a1e | 5 years ago |
Chi | 6234d1a487 | 5 years ago |
Chi | 6b1ae2d7cc | 5 years ago |
Chi | 327acbff49 | 6 years ago |
Chi | 019921476d | 6 years ago |
Chi | 2db5ea00a7 | 6 years ago |
eiriksm | 32e89c93a7 | 6 years ago |
git | a21b6dc597 | 6 years ago |
Chi | 5df42502d6 | 6 years ago |
blixxxa | 6fb5eeb3e4 | 6 years ago |
Chi | 185c743ab4 | 6 years ago |
Chi | dd983bf1f7 | 6 years ago |
hanoii | 6aae559040 | 6 years ago |
Chi | 9997416595 | 7 years ago |
Prashant.c | 40d9540c33 | 7 years ago |
Chi | aab4fd350a | 7 years ago |
Chi | b8e8e0a374 | 7 years ago |
Chi | 96d4333f0d | 7 years ago |
Chi | 6b36f2c266 | 7 years ago |
Chi | d5403ae274 | 7 years ago |
Chi | ec952a1cd5 | 7 years ago |
Chi | de13264a60 | 7 years ago |
Chi | 3b1bb1e239 | 7 years ago |
Chi | 6d5069ef06 | 7 years ago |
Chi | c3791bd1f3 | 7 years ago |
Chi | 4277c76990 | 7 years ago |
Chi | f8f1a94c22 | 7 years ago |
Chi | 6a024c5df3 | 7 years ago |
Chi | 2ae527f6ec | 7 years ago |
Chi | 03d0c30e8e | 7 years ago |
smk-ka | ce15ad15ba | 7 years ago |
Chi | 4507a6c1a2 | 7 years ago |
hanoii | 16b0cc1923 | 7 years ago |
Chi | 6a4ea3d943 | 7 years ago |
julien.sibi | 25d45394e7 | 7 years ago |
Chi | 147fbd900d | 7 years ago |
Chi | 0f49ad32bd | 7 years ago |
Chi | 8118336510 | 7 years ago |
Koen.Pasman | c1863a4cd7 | 7 years ago |
73 changed files with 5701 additions and 924 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 |
@ -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 |
||||
); |
||||
} |
||||
|
||||
} |
@ -1,531 +0,0 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\twig_tweak; |
||||
|
||||
use Drupal\Core\Access\AccessResult; |
||||
use Drupal\Core\Block\TitleBlockPluginInterface; |
||||
use Drupal\Core\Cache\CacheableMetadata; |
||||
use Drupal\Core\Entity\EntityInterface; |
||||
use Drupal\Core\Site\Settings; |
||||
use Drupal\Core\Url; |
||||
use Drupal\image\Entity\ImageStyle; |
||||
use Symfony\Cmf\Component\Routing\RouteObjectInterface; |
||||
|
||||
/** |
||||
* Twig extension with some useful functions and filters. |
||||
* |
||||
* As version 1.7 all dependencies are instantiated on demand for performance |
||||
* reasons. |
||||
*/ |
||||
class TwigExtension extends \Twig_Extension { |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function getFunctions() { |
||||
return [ |
||||
new \Twig_SimpleFunction('drupal_view', 'views_embed_view'), |
||||
new \Twig_SimpleFunction('drupal_block', [$this, 'drupalBlock']), |
||||
new \Twig_SimpleFunction('drupal_region', [$this, 'drupalRegion']), |
||||
new \Twig_SimpleFunction('drupal_entity', [$this, 'drupalEntity']), |
||||
new \Twig_SimpleFunction('drupal_field', [$this, 'drupalField']), |
||||
new \Twig_SimpleFunction('drupal_menu', [$this, 'drupalMenu']), |
||||
new \Twig_SimpleFunction('drupal_form', [$this, 'drupalForm']), |
||||
new \Twig_SimpleFunction('drupal_token', [$this, 'drupalToken']), |
||||
new \Twig_SimpleFunction('drupal_config', [$this, 'drupalConfig']), |
||||
new \Twig_SimpleFunction('drupal_dump', [$this, 'drupalDump']), |
||||
new \Twig_SimpleFunction('dd', [$this, 'drupalDump']), |
||||
// Wrap drupal_set_message() because it returns some value which is not |
||||
// suitable for Twig template. |
||||
new \Twig_SimpleFunction('drupal_set_message', [$this, 'drupalSetMessage']), |
||||
new \Twig_SimpleFunction('drupal_title', [$this, 'drupalTitle']), |
||||
new \Twig_SimpleFunction('drupal_url', [$this, 'drupalUrl']), |
||||
]; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function getFilters() { |
||||
$filters = [ |
||||
new \Twig_SimpleFilter('token_replace', [$this, 'tokenReplaceFilter']), |
||||
new \Twig_SimpleFilter('preg_replace', [$this, 'pregReplaceFilter']), |
||||
new \Twig_SimpleFilter('image_style', [$this, 'imageStyle']), |
||||
new \Twig_SimpleFilter('transliterate', [$this, 'transliterate']), |
||||
new \Twig_SimpleFilter('check_markup', [$this, 'checkMarkup']), |
||||
]; |
||||
// PHP filter should be enabled in settings.php file. |
||||
if (Settings::get('twig_tweak_enable_php_filter')) { |
||||
$filters[] = new \Twig_SimpleFilter('php', [$this, 'phpFilter']); |
||||
} |
||||
return $filters; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function getName() { |
||||
return 'twig_tweak'; |
||||
} |
||||
|
||||
/** |
||||
* Builds the render array for the provided block. |
||||
* |
||||
* @param mixed $id |
||||
* The ID of the block to render. |
||||
* @param bool $check_access |
||||
* (Optional) Indicates that access check is required. |
||||
* |
||||
* @return null|array |
||||
* A render array for the block or NULL if the block does not exist. |
||||
*/ |
||||
public function drupalBlock($id, $check_access = TRUE) { |
||||
$entity_type_manager = \Drupal::entityTypeManager(); |
||||
$block = $entity_type_manager->getStorage('block')->load($id); |
||||
if ($block) { |
||||
$access = $check_access ? $this->entityAccess($block) : AccessResult::allowed(); |
||||
if ($access->isAllowed()) { |
||||
$build = $entity_type_manager->getViewBuilder('block')->view($block); |
||||
CacheableMetadata::createFromRenderArray($build) |
||||
->merge(CacheableMetadata::createFromObject($block)) |
||||
->merge(CacheableMetadata::createFromObject($access)) |
||||
->applyTo($build); |
||||
return $build; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 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 drupalRegion($region, $theme = NULL) { |
||||
$entity_type_manager = \Drupal::entityTypeManager(); |
||||
$blocks = $entity_type_manager->getStorage('block')->loadByProperties([ |
||||
'region' => $region, |
||||
'theme' => $theme ?: \Drupal::config('system.theme')->get('default'), |
||||
]); |
||||
|
||||
$view_builder = $entity_type_manager->getViewBuilder('block'); |
||||
|
||||
$build = []; |
||||
|
||||
$cache_metadata = new CacheableMetadata(); |
||||
|
||||
/* @var $blocks \Drupal\block\BlockInterface[] */ |
||||
foreach ($blocks as $id => $block) { |
||||
$access = $this->entityAccess($block); |
||||
$cache_metadata = $cache_metadata->merge(CacheableMetadata::createFromObject($access)); |
||||
if ($access->isAllowed()) { |
||||
$block_plugin = $block->getPlugin(); |
||||
if ($block_plugin instanceof TitleBlockPluginInterface) { |
||||
$request = \Drupal::request(); |
||||
if ($route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)) { |
||||
$block_plugin->setTitle(\Drupal::service('title_resolver')->getTitle($request, $route)); |
||||
} |
||||
} |
||||
$build[$id] = $view_builder->view($block); |
||||
} |
||||
} |
||||
|
||||
if ($build) { |
||||
$cache_metadata->applyTo($build); |
||||
} |
||||
|
||||
return $build; |
||||
} |
||||
|
||||
/** |
||||
* Returns the render array for an entity. |
||||
* |
||||
* @param string $entity_type |
||||
* The entity type. |
||||
* @param mixed $id |
||||
* The ID of the entity to render. |
||||
* @param string $view_mode |
||||
* (optional) The view mode that should be used to render the entity. |
||||
* @param string $langcode |
||||
* (optional) For which language the entity should be rendered, defaults to |
||||
* the current content language. |
||||
* |
||||
* @return null|array |
||||
* A render array for the entity or NULL if the entity does not exist. |
||||
*/ |
||||
public function drupalEntity($entity_type, $id = NULL, $view_mode = NULL, $langcode = NULL) { |
||||
$entity_type_manager = \Drupal::entityTypeManager(); |
||||
$entity = $id |
||||
? $entity_type_manager->getStorage($entity_type)->load($id) |
||||
: \Drupal::routeMatch()->getParameter($entity_type); |
||||
if ($entity) { |
||||
$access = $this->entityAccess($entity); |
||||
if ($access->isAllowed()) { |
||||
$build = $entity_type_manager |
||||
->getViewBuilder($entity_type) |
||||
->view($entity, $view_mode, $langcode); |
||||
CacheableMetadata::createFromRenderArray($build) |
||||
->merge(CacheableMetadata::createFromObject($entity)) |
||||
->merge(CacheableMetadata::createFromObject($access)) |
||||
->applyTo($build); |
||||
return $build; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Returns the render array for a single entity field. |
||||
* |
||||
* @param string $field_name |
||||
* The field name. |
||||
* @param string $entity_type |
||||
* The entity type. |
||||
* @param mixed $id |
||||
* The ID of the entity to render. |
||||
* @param string $view_mode |
||||
* (optional) The view mode that should be used to render the field. |
||||
* @param string $langcode |
||||
* (optional) Language code to load translation. |
||||
* |
||||
* @return null|array |
||||
* A render array for the field or NULL if the value does not exist. |
||||
*/ |
||||
public function drupalField($field_name, $entity_type, $id = NULL, $view_mode = 'default', $langcode = NULL) { |
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ |
||||
$entity = $id |
||||
? \Drupal::entityTypeManager()->getStorage($entity_type)->load($id) |
||||
: \Drupal::routeMatch()->getParameter($entity_type); |
||||
if ($entity) { |
||||
$access = $this->entityAccess($entity); |
||||
if ($access->isAllowed()) { |
||||
if ($langcode && $entity->hasTranslation($langcode)) { |
||||
$entity = $entity->getTranslation($langcode); |
||||
} |
||||
if (isset($entity->{$field_name})) { |
||||
$build = $entity->{$field_name}->view($view_mode); |
||||
CacheableMetadata::createFromRenderArray($build) |
||||
->merge(CacheableMetadata::createFromObject($access)) |
||||
->merge(CacheableMetadata::createFromObject($entity)) |
||||
->applyTo($build); |
||||
return $build; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Returns the render array for Drupal 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. |
||||
* |
||||
* @return array |
||||
* A render array for the menu. |
||||
*/ |
||||
public function drupalMenu($menu_name, $level = 1, $depth = 0) { |
||||
/** @var \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree */ |
||||
$menu_tree = \Drupal::service('menu.link_tree'); |
||||
$parameters = $menu_tree->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, $menu_tree->maxDepth())); |
||||
} |
||||
|
||||
$tree = $menu_tree->load($menu_name, $parameters); |
||||
$manipulators = [ |
||||
['callable' => 'menu.default_tree_manipulators:checkAccess'], |
||||
['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'], |
||||
]; |
||||
$tree = $menu_tree->transform($tree, $manipulators); |
||||
return $menu_tree->build($tree); |
||||
} |
||||
|
||||
/** |
||||
* Builds and processes a form for a given form ID. |
||||
* |
||||
* @param string $form_id |
||||
* The form ID. |
||||
* @param ... |
||||
* Additional arguments are passed to form constructor. |
||||
* |
||||
* @return array |
||||
* A render array to represent the form. |
||||
*/ |
||||
public function drupalForm($form_id) { |
||||
$form_builder = \Drupal::formBuilder(); |
||||
$args = func_get_args(); |
||||
return call_user_func_array([$form_builder, 'getForm'], $args); |
||||
} |
||||
|
||||
/** |
||||
* Replaces a given tokens 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 function drupalToken($token, array $data = [], array $options = []) { |
||||
return \Drupal::token()->replace("[$token]", $data, $options); |
||||
} |
||||
|
||||
/** |
||||
* Gets data from this configuration. |
||||
* |
||||
* @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 function drupalConfig($name, $key) { |
||||
return \Drupal::config($name)->get($key); |
||||
} |
||||
|
||||
/** |
||||
* Dumps information about variables. |
||||
*/ |
||||
public function drupalDump($var) { |
||||
$var_dumper = '\Symfony\Component\VarDumper\VarDumper'; |
||||
if (class_exists($var_dumper)) { |
||||
call_user_func($var_dumper . '::dump', $var); |
||||
} |
||||
else { |
||||
trigger_error('Could not dump the variable because symfony/var-dumper component is not installed.', E_USER_WARNING); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* An alias for self::drupalDump(). |
||||
* |
||||
* @see \Drupal\twig_tweak\TwigExtension::drupalDump(); |
||||
*/ |
||||
public function dd() { |
||||
$this->drupalDump(func_get_args()); |
||||
} |
||||
|
||||
/** |
||||
* Sets a message to display to the user. |
||||
* |
||||
* @param string|\Drupal\Component\Render\MarkupInterface $message |
||||
* (optional) The translated message to be displayed to the user. |
||||
* @param string $type |
||||
* (optional) The message's type. Defaults to 'status'. |
||||
* @param bool $repeat |
||||
* (optional) If this is FALSE and the message is already set, then the |
||||
* message will not be repeated. Defaults to FALSE. |
||||
* |
||||
* @return array |
||||
* A render array to disable caching. |
||||
*/ |
||||
public function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) { |
||||
\Drupal::messenger()->addMessage($message, $type, $repeat); |
||||
$build['#cache']['max-age'] = 0; |
||||
return $build; |
||||
} |
||||
|
||||
/** |
||||
* Returns a title for the current route. |
||||
* |
||||
* @return array |
||||
* A render array to represent page title. |
||||
*/ |
||||
public function drupalTitle() { |
||||
$title = \Drupal::service('title_resolver')->getTitle( |
||||
\Drupal::request(), |
||||
\Drupal::routeMatch()->getRouteObject() |
||||
); |
||||
$build['#markup'] = render($title); |
||||
$build['#cache']['contexts'] = ['url']; |
||||
return $build; |
||||
} |
||||
|
||||
/** |
||||
* Generates a URL from internal path. |
||||
* |
||||
* @param string $user_input |
||||
* User input for a link or path. |
||||
* @param array $options |
||||
* (optional) An array of options. |
||||
* |
||||
* @return \Drupal\Core\Url |
||||
* A new Url object based on user input. |
||||
* |
||||
* @see \Drupal\Core\Url::fromUserInput() |
||||
*/ |
||||
public function drupalUrl($user_input, array $options = []) { |
||||
if (!in_array($user_input[0], ['/', '#', '?'])) { |
||||
$user_input = '/' . $user_input; |
||||
} |
||||
return Url::fromUserInput($user_input, $options); |
||||
} |
||||
|
||||
/** |
||||
* Replaces all tokens in a given string with appropriate values. |
||||
* |
||||
* @param string $text |
||||
* An HTML string containing replaceable tokens. |
||||
* |
||||
* @return string |
||||
* The entered HTML text with tokens replaced. |
||||
*/ |
||||
public function tokenReplaceFilter($text) { |
||||
return \Drupal::token()->replace($text); |
||||
} |
||||
|
||||
/** |
||||
* Performs a regular expression search and replace. |
||||
* |
||||
* @param string $text |
||||
* The text to search and replace. |
||||
* @param string $pattern |
||||
* The pattern to search for. |
||||
* @param string $replacement |
||||
* The string to replace. |
||||
* |
||||
* @return string |
||||
* The new text if matches are found, otherwise unchanged text. |
||||
*/ |
||||
public function pregReplaceFilter($text, $pattern, $replacement) { |
||||
// BC layer. Before version 8.x-1.8 the pattern was without delimiters. |
||||
// @todo Remove this in Drupal 9. |
||||
if (strpos($pattern, '/') !== 0) { |
||||
return preg_replace("/$pattern/", $replacement, $text); |
||||
} |
||||
return preg_replace($pattern, $replacement, $text); |
||||
} |
||||
|
||||
/** |
||||
* Returns the URL of this image derivative for an original image path or URI. |
||||
* |
||||
* @param string $path |
||||
* The path or URI to the original image. |
||||
* @param string $style |
||||
* The image style. |
||||
* |
||||
* @return string |
||||
* 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 function imageStyle($path, $style) { |
||||
/** @var \Drupal\Image\ImageStyleInterface $image_style */ |
||||
if ($image_style = ImageStyle::load($style)) { |
||||
return file_url_transform_relative($image_style->buildUrl($path)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Transliterates text from Unicode to US-ASCII. |
||||
* |
||||
* @param string $string |
||||
* The string 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 function transliterate($string, $langcode = 'en', $unknown_character = '?', $max_length = NULL) { |
||||
return \Drupal::transliteration()->transliterate($string, $langcode, $unknown_character, $max_length); |
||||
} |
||||
|
||||
/** |
||||
* Runs all the enabled filters on a piece of text. |
||||
* |
||||
* @param string $text |
||||
* The text to be filtered. |
||||
* @param string|null $format_id |
||||
* (optional) The machine name of the filter format to be used to filter the |
||||
* text. Defaults to the fallback format. See filter_fallback_format(). |
||||
* @param string $langcode |
||||
* (optional) The language code of the text to be filtered. |
||||
* @param array $filter_types_to_skip |
||||
* (optional) An array of filter types to skip, or an empty array (default) |
||||
* to skip no filter types. |
||||
* |
||||
* @return \Drupal\Component\Render\MarkupInterface |
||||
* The filtered text. |
||||
* |
||||
* @see check_markup() |
||||
*/ |
||||
public function checkMarkup($text, $format_id = NULL, $langcode = '', array $filter_types_to_skip = []) { |
||||
return check_markup($text, $format_id, $langcode, $filter_types_to_skip); |
||||
} |
||||
|
||||
/** |
||||
* Evaluates a string of PHP code. |
||||
* |
||||
* @param string $code |
||||
* Valid PHP code to be evaluated. |
||||
* |
||||
* @return mixed |
||||
* The eval() result. |
||||
*/ |
||||
public function phpFilter($code) { |
||||
ob_start(); |
||||
// @codingStandardsIgnoreStart |
||||
print eval($code); |
||||
// @codingStandardsIgnoreEnd |
||||
$output = ob_get_contents(); |
||||
ob_end_clean(); |
||||
return $output; |
||||
} |
||||
|
||||
/** |
||||
* Checks view access to a given entity. |
||||
* |
||||
* @param \Drupal\Core\Entity\EntityInterface $entity |
||||
* Entity to check access. |
||||
* |
||||
* @return \Drupal\Core\Access\AccessResultInterface |
||||
* The access check result. |
||||
* |
||||
* @TODO Remove "check_access" option in 9.x. |
||||
*/ |
||||
protected function entityAccess(EntityInterface $entity) { |
||||
// Prior version 8.x-1.7 entity access was not checked. The "check_access" |
||||
// option provides a workaround for possible BC issues. |
||||
return Settings::get('twig_tweak_check_access', TRUE) ? |
||||
$entity->access('view', NULL, TRUE) : AccessResult::allowed(); |
||||
} |
||||
|
||||
} |
@ -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']); |
||||
} |
||||
} |
||||
|
||||
} |
@ -1,212 +0,0 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\Tests\twig_tweak\Kernel; |
||||
|
||||
use Drupal\block\BlockViewBuilder; |
||||
use Drupal\block\Entity\Block; |
||||
use Drupal\Core\Access\AccessResult; |
||||
use Drupal\Core\Entity\EntityStorageInterface; |
||||
use Drupal\Core\Entity\EntityTypeManagerInterface; |
||||
use Drupal\Core\Session\AccountInterface; |
||||
use Drupal\KernelTests\KernelTestBase; |
||||
use Drupal\node\Entity\Node; |
||||
use Drupal\node\Entity\NodeType; |
||||
use Drupal\node\NodeInterface; |
||||
use Drupal\Tests\user\Traits\UserCreationTrait; |
||||
|
||||
/** |
||||
* Tests for the Twig Tweak access control. |
||||
* |
||||
* @group twig_tweak |
||||
*/ |
||||
class AccessTest extends KernelTestBase { |
||||
|
||||
use UserCreationTrait; |
||||
|
||||
/** |
||||
* A node for testing. |
||||
* |
||||
* @var \Drupal\node\NodeInterface |
||||
*/ |
||||
private $node; |
||||
|
||||
/** |
||||
* The Twig extension. |
||||
* |
||||
* @var \Drupal\twig_tweak\TwigExtension |
||||
*/ |
||||
private $twigExtension; |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public static $modules = [ |
||||
'twig_tweak', |
||||
'twig_tweak_test', |
||||
'node', |
||||
'user', |
||||
'system', |
||||
]; |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
protected function setUp() { |
||||
parent::setUp(); |
||||
|
||||
$this->installEntitySchema('node'); |
||||
$this->installEntitySchema('user'); |
||||
$this->installConfig(['system']); |
||||
|
||||
$node_type = NodeType::create([ |
||||
'type' => 'article', |
||||
'name' => 'Article', |
||||
]); |
||||
$node_type->save(); |
||||
|
||||
$values = [ |
||||
'type' => 'article', |
||||
'status' => NodeInterface::PUBLISHED, |
||||
// @see twig_tweak_test_node_access() |
||||
'title' => 'Entity access test', |
||||
]; |
||||
$this->node = Node::create($values); |
||||
$this->node->save(); |
||||
|
||||
$this->twigExtension = $this->container->get('twig_tweak.twig_extension'); |
||||
} |
||||
|
||||
/** |
||||
* Test callback. |
||||
*/ |
||||
public function testDrupalEntity() { |
||||
|
||||
// -- Unprivileged user. |
||||
$this->setUpCurrentUser(['name' => 'User 1']); |
||||
|
||||
$build = $this->twigExtension->drupalEntity('node', $this->node->id()); |
||||
self::assertNull($build); |
||||
|
||||
// -- Privileged user. |
||||
$this->setUpCurrentUser(['name' => 'User 2'], ['access content']); |
||||
|
||||
$build = $this->twigExtension->drupalEntity('node', $this->node->id()); |
||||
self::assertArrayHasKey('#node', $build); |
||||
$expected_cache = [ |
||||
'tags' => [ |
||||
'node:1', |
||||
'node_view', |
||||
'tag_from_twig_tweak_test_node_access', |
||||
], |
||||
'contexts' => [ |
||||
'user', |
||||
'user.permissions', |
||||
], |
||||
'max-age' => 50, |
||||
]; |
||||
self::assertSame($expected_cache, $build['#cache']); |
||||
} |
||||
|
||||
/** |
||||
* Test callback. |
||||
*/ |
||||
public function testDrupalField() { |
||||
|
||||
// -- Unprivileged user. |
||||
$this->setUpCurrentUser(['name' => 'User 1']); |
||||
|
||||
$build = $this->twigExtension->drupalField('title', 'node', $this->node->id()); |
||||
self::assertNull($build); |
||||
|
||||
// -- Privileged user. |
||||
$this->setUpCurrentUser(['name' => 'User 2'], ['access content']); |
||||
|
||||
$build = $this->twigExtension->drupalField('title', 'node', $this->node->id()); |
||||
self::assertArrayHasKey('#items', $build); |
||||
$expected_cache = [ |
||||
'contexts' => [ |
||||
'user', |
||||
'user.permissions', |
||||
], |
||||
'tags' => [ |
||||
'node:1', |
||||
'tag_from_twig_tweak_test_node_access', |
||||
], |
||||
'max-age' => 50, |
||||
]; |
||||
self::assertSame($expected_cache, $build['#cache']); |
||||
} |
||||
|
||||
/** |
||||
* Test callback. |
||||
*/ |
||||
public function testDrupalRegion() { |
||||
|
||||
// @codingStandardsIgnoreStart |
||||
$create_block = function ($id) { |
||||
return new class(['id' => $id], 'block') extends Block { |
||||
public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { |
||||
$result = AccessResult::allowedIf($this->id == 'block_1'); |
||||
$result->cachePerUser(); |
||||
$result->addCacheTags(['tag_for_' . $this->id]); |
||||
$result->setCacheMaxAge(123); |
||||
return $return_as_object ? $result : $result->isAllowed(); |
||||
} |
||||
public function getPlugin() { |
||||
return NULL; |
||||
} |
||||
}; |
||||
}; |
||||
// @codingStandardsIgnoreEnd |
||||
|
||||
$storage = $this->createMock(EntityStorageInterface::class); |
||||
$blocks = [ |
||||
'block_1' => $create_block('block_1'), |
||||
'block_2' => $create_block('block_2'), |
||||
]; |
||||
$storage->expects($this->any()) |
||||
->method('loadByProperties') |
||||
->willReturn($blocks); |
||||
|
||||
$view_builder = $this->createMock(BlockViewBuilder::class); |
||||
$content = [ |
||||
'#markup' => 'foo', |
||||
'#cache' => [ |
||||
'tags' => ['tag_from_view'], |
||||
], |
||||
]; |
||||
$view_builder->expects($this->any()) |
||||
->method('view') |
||||
->willReturn($content); |
||||
|
||||
$entity_type_manager = $this->createMock(EntityTypeManagerInterface::class); |
||||
$entity_type_manager->expects($this->any()) |
||||
->method('getStorage') |
||||
->willReturn($storage); |
||||
$entity_type_manager->expects($this->any()) |
||||
->method('getViewBuilder') |
||||
->willReturn($view_builder); |
||||
|
||||
$this->container->set('entity_type.manager', $entity_type_manager); |
||||
|
||||
$build = $this->twigExtension->drupalRegion('bar'); |
||||
$expected_build = [ |
||||
'block_1' => [ |
||||
'#markup' => 'foo', |
||||
'#cache' => [ |
||||
'tags' => ['tag_from_view'], |
||||
], |
||||
], |
||||
'#cache' => [ |
||||
'contexts' => ['user'], |
||||
'tags' => [ |
||||
'tag_for_block_1', |
||||
'tag_for_block_2', |
||||
], |
||||
'max-age' => 123, |
||||
], |
||||
]; |
||||
self::assertSame($expected_build, $build); |
||||
} |
||||
|
||||
} |
@ -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,28 @@
|
||||
langcode: en |
||||
status: true |
||||
dependencies: |
||||
config: |
||||
- field.field.media.image.field_media_image |
||||
- image.style.large |
||||
- media.type.image |
||||
module: |
||||
- image |
||||
id: media.image.default |
||||
targetEntityType: media |
||||
bundle: image |
||||
mode: default |
||||
content: |
||||
field_media_image: |
||||
label: visually_hidden |
||||
settings: |
||||
image_style: large |
||||
image_link: '' |
||||
third_party_settings: { } |
||||
type: image |
||||
weight: 1 |
||||
region: content |
||||
hidden: |
||||
created: true |
||||
name: true |
||||
thumbnail: true |
||||
uid: true |
@ -0,0 +1,27 @@
|
||||
langcode: en |
||||
status: true |
||||
dependencies: |
||||
config: |
||||
- field.field.media.remote_video.field_media_oembed_video |
||||
- media.type.remote_video |
||||
module: |
||||
- media |
||||
id: media.remote_video.default |
||||
targetEntityType: media |
||||
bundle: remote_video |
||||
mode: default |
||||
content: |
||||
field_media_oembed_video: |
||||
type: oembed |
||||
weight: 0 |
||||
label: hidden |
||||
settings: |
||||
max_width: 0 |
||||
max_height: 0 |
||||
third_party_settings: { } |
||||
region: content |
||||
hidden: |
||||
created: true |
||||
name: true |
||||
thumbnail: true |
||||
uid: true |
@ -0,0 +1,29 @@
|
||||
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: |
||||
- text |
||||
- user |
||||
id: node.page.default |
||||
targetEntityType: node |
||||
bundle: page |
||||
mode: default |
||||
content: |
||||
body: |
||||
label: hidden |
||||
type: text_default |
||||
weight: 100 |
||||
region: content |
||||
settings: { } |
||||
third_party_settings: { } |
||||
links: |
||||
weight: 101 |
||||
region: content |
||||
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 |
@ -0,0 +1,40 @@
|
||||
langcode: en |
||||
status: true |
||||
dependencies: |
||||
config: |
||||
- field.storage.media.field_media_image |
||||
- media.type.image |
||||
enforced: |
||||
module: |
||||
- media |
||||
module: |
||||
- image |
||||
id: media.image.field_media_image |
||||
field_name: field_media_image |
||||
entity_type: media |
||||
bundle: image |
||||
label: Image |
||||
description: '' |
||||
required: true |
||||
translatable: true |
||||
default_value: { } |
||||
default_value_callback: '' |
||||
settings: |
||||
alt_field: true |
||||
alt_field_required: true |
||||
title_field: false |
||||
title_field_required: false |
||||
max_resolution: '' |
||||
min_resolution: '' |
||||
default_image: |
||||
uuid: null |
||||
alt: '' |
||||
title: '' |
||||
width: null |
||||
height: null |
||||
file_directory: '[date:custom:Y]-[date:custom:m]' |
||||
file_extensions: 'png gif jpg jpeg' |
||||
max_filesize: '' |
||||
handler: 'default:file' |
||||
handler_settings: { } |
||||
field_type: image |
@ -0,0 +1,18 @@
|
||||
langcode: en |
||||
status: true |
||||
dependencies: |
||||
config: |
||||
- field.storage.media.field_media_oembed_video |
||||
- media.type.remote_video |
||||
id: media.remote_video.field_media_oembed_video |
||||
field_name: field_media_oembed_video |
||||
entity_type: media |
||||
bundle: remote_video |
||||
label: 'Video URL' |
||||
description: '' |
||||
required: true |
||||
translatable: true |
||||
default_value: { } |
||||
default_value_callback: '' |
||||
settings: { } |
||||
field_type: string |
@ -0,0 +1,22 @@
|
||||
langcode: en |
||||
status: true |
||||
dependencies: |
||||
config: |
||||
- field.storage.node.body |
||||
- node.type.page |
||||
module: |
||||
- text |
||||
id: node.page.body |
||||
field_name: body |
||||
entity_type: node |
||||
bundle: page |
||||
label: Body |
||||
description: '' |
||||
required: false |
||||
translatable: true |
||||
default_value: { } |
||||
default_value_callback: '' |
||||
settings: |
||||
display_summary: true |
||||
required_summary: false |
||||
field_type: text_with_summary |
@ -0,0 +1,37 @@
|
||||
langcode: en |
||||
status: true |
||||
dependencies: |
||||
config: |
||||
- field.storage.node.field_image |
||||
- node.type.page |
||||
module: |
||||
- image |
||||
id: node.page.field_image |
||||
field_name: field_image |
||||
entity_type: node |
||||
bundle: page |
||||
label: Image |
||||
description: '' |
||||
required: false |
||||
translatable: true |
||||
default_value: { } |
||||
default_value_callback: '' |
||||
settings: |
||||
file_directory: '[date:custom:Y]-[date:custom:m]' |
||||
file_extensions: 'png gif jpg jpeg' |
||||
max_filesize: '' |
||||
max_resolution: '' |
||||
min_resolution: '' |
||||
alt_field: true |
||||
alt_field_required: true |
||||
title_field: false |
||||
title_field_required: false |
||||
default_image: |
||||
uuid: '' |
||||
alt: '' |
||||
title: '' |
||||
width: null |
||||
height: null |
||||
handler: 'default:file' |
||||
handler_settings: { } |
||||
field_type: image |
@ -0,0 +1,29 @@
|
||||
langcode: en |
||||
status: true |
||||
dependencies: |
||||
config: |
||||
- field.storage.node.field_media |
||||
- media.type.image |
||||
- media.type.remote_video |
||||
- node.type.page |
||||
id: node.page.field_media |
||||
field_name: field_media |
||||
entity_type: node |
||||
bundle: page |
||||
label: Media |
||||
description: '' |
||||
required: false |
||||
translatable: true |
||||
default_value: { } |
||||
default_value_callback: '' |
||||
settings: |
||||
handler: 'default:media' |
||||
handler_settings: |
||||
target_bundles: |
||||
image: image |
||||
remote_video: remote_video |
||||
sort: |
||||
field: _none |
||||
auto_create: false |
||||
auto_create_bundle: image |
||||
field_type: entity_reference |
@ -0,0 +1,32 @@
|
||||
langcode: en |
||||
status: true |
||||
dependencies: |
||||
enforced: |
||||
module: |
||||
- media |
||||
module: |
||||
- file |
||||
- image |
||||
- media |
||||
id: media.field_media_image |
||||
field_name: field_media_image |
||||
entity_type: media |
||||
type: image |
||||
settings: |
||||
default_image: |
||||
uuid: null |
||||
alt: '' |
||||
title: '' |
||||
width: null |
||||
height: null |
||||
target_type: file |
||||
display_field: false |
||||
display_default: false |
||||
uri_scheme: public |
||||
module: image |
||||
locked: false |
||||
cardinality: 1 |
||||
translatable: true |
||||
indexes: { } |
||||
persist_with_no_fields: false |
||||
custom_storage: false |
@ -0,0 +1,20 @@
|
||||
langcode: en |
||||
status: true |
||||
dependencies: |
||||
module: |
||||
- media |
||||
id: media.field_media_oembed_video |
||||
field_name: field_media_oembed_video |
||||
entity_type: media |
||||
type: string |
||||
settings: |
||||
max_length: 255 |
||||
is_ascii: false |
||||
case_sensitive: false |
||||
module: core |
||||
locked: false |
||||
cardinality: 1 |
||||
translatable: true |
||||
indexes: { } |
||||
persist_with_no_fields: false |
||||
custom_storage: false |
@ -0,0 +1,31 @@
|
||||
langcode: en |
||||
status: true |
||||
dependencies: |
||||
module: |
||||
- file |
||||
- image |
||||
- node |
||||
id: node.field_image |
||||
field_name: field_image |
||||
entity_type: node |
||||
type: image |
||||
settings: |
||||
uri_scheme: public |
||||
default_image: |
||||
uuid: null |
||||
alt: '' |
||||
title: '' |
||||
width: null |
||||
height: null |
||||
target_type: file |
||||
display_field: false |
||||
display_default: false |
||||
module: image |
||||
locked: false |
||||
cardinality: 1 |
||||
translatable: true |
||||
indexes: |
||||
target_id: |
||||
- target_id |
||||
persist_with_no_fields: false |
||||
custom_storage: false |
@ -0,0 +1,19 @@
|
||||
langcode: en |
||||
status: true |
||||
dependencies: |
||||
module: |
||||
- media |
||||
- node |
||||
id: node.field_media |
||||
field_name: field_media |
||||
entity_type: node |
||||
type: entity_reference |
||||
settings: |
||||
target_type: media |
||||
module: core |
||||
locked: false |
||||
cardinality: 1 |
||||
translatable: true |
||||
indexes: { } |
||||
persist_with_no_fields: false |
||||
custom_storage: false |
@ -0,0 +1,13 @@
|
||||
langcode: en |
||||
status: true |
||||
dependencies: { } |
||||
id: image |
||||
label: Image |
||||
description: 'Use local images for reusable media.' |
||||
source: image |
||||
queue_thumbnail_downloads: false |
||||
new_revision: true |
||||
source_configuration: |
||||
source_field: field_media_image |
||||
field_map: |
||||
name: name |
@ -0,0 +1,17 @@
|
||||
langcode: en |
||||
status: true |
||||
dependencies: { } |
||||
id: remote_video |
||||
label: 'Remote video' |
||||
description: 'A remotely hosted video from YouTube or Vimeo.' |
||||
source: 'oembed:video' |
||||
queue_thumbnail_downloads: false |
||||
new_revision: true |
||||
source_configuration: |
||||
thumbnails_directory: 'public://oembed_thumbnails' |
||||
providers: |
||||
- YouTube |
||||
- Vimeo |
||||
source_field: field_media_oembed_video |
||||
field_map: |
||||
title: name |
@ -0,0 +1,12 @@
|
||||
langcode: en |
||||
status: true |
||||
dependencies: { } |
||||
_core: |
||||
default_config_hash: KuyA4NHPXcmKAjRtwa0vQc2ZcyrUJy6IlS2TAyMNRbc |
||||
name: 'Basic page' |
||||
type: page |
||||
description: 'Use <em>basic pages</em> for your static content, such as an ''About us'' page.' |
||||
help: '' |
||||
new_revision: true |
||||
preview_mode: 1 |
||||
display_submitted: false |
@ -1,7 +1,7 @@
|
||||
langcode: en |
||||
status: true |
||||
dependencies: {} |
||||
dependencies: { } |
||||
id: twig-tweak-test |
||||
label: Twig tweak test |
||||
label: 'Twig tweak test' |
||||
description: '' |
||||
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__], |
||||
], |
||||
]; |
||||
} |
||||
|
||||
} |
@ -1,8 +1,12 @@
|
||||
name: Twig tweak test |
||||
type: module |
||||
description: Support module for Tweak twig testing. |
||||
description: Support module for Twig tweak testing. |
||||
package: Testing |
||||
core: 8.x |
||||
core_version_requirement: ^9 || ^10 |
||||
dependencies: |
||||
- drupa:twig_tweak |
||||
- drupal:system (>= 9.0) |
||||
- drupal:block |
||||
- drupal:media |
||||
- drupal:node |
||||
- drupal:path |
||||
- drupal:twig_tweak |
||||
|
@ -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,4 +1,6 @@
|
||||
name: Twig tweak |
||||
name: Twig Tweak |
||||
type: module |
||||
description: Provides some extra Twig functions and filters. |
||||
core: 8.x |
||||
core_version_requirement: ^9 || ^10 |
||||
dependencies: |
||||
- drupal:system (>=9.0) |
||||
|
@ -1,5 +1,45 @@
|
||||
services: |
||||
twig_tweak.twig_extension: |
||||
class: Drupal\twig_tweak\TwigExtension |
||||
class: Drupal\twig_tweak\TwigTweakExtension |
||||
arguments: ['@module_handler', '@theme.manager'] |
||||
tags: |
||||
- { 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