This PR was merged into the 3.x branch.
Discussion
----------
Fail CI when an extension test job fails
Commits
-------
e418eb9883 Fail CI when an extension test job fails
This PR was merged into the 3.x branch.
Discussion
----------
Remove the numeric locale switch when compiling number literals
`Compiler::repr()` switches `LC_NUMERIC` to `C` and back around every number it compiles. This was added when `repr()` used `strval()`: before PHP 8, `strval()` printed floats with the locale's decimal separator, so a French locale produced `1,2` in compiled templates.
`repr()` now uses `var_export()`, which always uses `.` whatever the locale. Since PHP 8.0, float-to-string conversion no longer depends on the locale either. With Twig 3.x requiring PHP 8.1, the locale switch never changes the output, so this PR removes it.
Alternative to #5003
Commits
-------
83a4f669db Remove the numeric locale switch when compiling number literals
This PR was squashed before being merged into the 3.x branch.
Discussion
----------
Add the collate function to compare strings in the alphabetical order of a locale
Alternative to #4943
Instead of adding a new `sort_localized` filter, a new `collate()` function, part of `twig/intl-extra`, allows to compares two strings using the alphabetical order of a locale:
```twig
{# athlétisme, Boxe, Échecs, équitation, judo, Yoga #}
{{ sports|sort((a, b) => collate(a, b))|join(', ') }}
{% for city in cities|sort((a, b) => collate(a.name, b.name)) %}
{{ city.name }}
{% endfor %}
{# cs: hrad, chata, ideal #}
{{ words|sort((a, b) => collate(a, b, 'cs'))|join(', ') }}
```
That avoids the creation of a new function and reuses the existing arrow function.
The current locale is the default, as with every other intl-extra function.
The only drawback I can see is that sorting a plain list is more verbose: `sort((a, b) => collate(a, b))` instead of `sort_localized`.
Commits
-------
43753b0328 Add the collate function to compare strings in the alphabetical order of a locale
This PR was merged into the 3.x branch.
Discussion
----------
[3.x] Allow registering an extension class lazily with `AttributeExtension`
When an extension class is registered through `AttributeExtension`, `Environment::getExtension()` returns the `AttributeExtension`, not an instance of the class that was asked for. This PR makes it return the instance provided by the runtime loaders instead. An extension can then be registered lazily, so that its dependencies are created on its first call from a template, without changing what `getExtension()` returns.
`getRuntime()` also falls back to the registered extension of the requested class. An extension can then return the `[self::class, 'method']` callables that `AttributeExtension` builds from its `getFunctions()`, and still work when it's added with `addExtension(new MyExtension(...))`.
Needed for symfony/symfony#66828, which registers Symfony's own extensions this way.
Merge-up to 4.x: only the imports of `tests/EnvironmentTest.php` conflict, and the CHANGELOG entry doesn't go to 4.x.
Commits
-------
6e527a86e0 Allow registering an extension class lazily with `AttributeExtension`
This PR was squashed before being merged into the 3.x branch.
Discussion
----------
Allow marking a Markup instance as safe for some escaping strategies only
Currently, a Markup instance is safe for every escaping strategy.
A new optional third constructor argument lists the strategies the content is safe for: new Markup($html, 'UTF-8', ['html']) is printed as-is in an HTML context and escaped in any other one. The default stays ['all'], so existing code is not affected.
Commits
-------
527710bd71 Allow marking a Markup instance as safe for some escaping strategies only
This PR was merged into the 3.x branch.
Discussion
----------
Consider a class safe for html_attr also safe for html_attr_relaxed and html at runtime
The compile-time safe analysis already treats a filter or function declared safe for html_attr as also safe for html_attr_relaxed and html, and one declared safe for html_attr_relaxed as also safe for html.
`EscaperRuntime::addSafeClass()` did not apply the same rules, so an object of a class registered as safe for html_attr was still escaped when printed in an HTML context. This applies the same rules at runtime, including to classes that inherit their safety from a parent class or an interface.
Commits
-------
e0e04a1561 Align runtime safe classes with the compile-time strategy subset rules
This PR was squashed before being merged into the 3.x branch.
Discussion
----------
Fix reading an attribute through its getter initializing a lazy object when the property is not public
Fix#4986
With a partially loaded Doctrine entity (native lazy objects), `{{ project.title }}` initializes the whole object, while `{{ project.getTitle() }}` does not. `getAttribute()` first calls `isset($object->title)`. On a private property this is always false when the class has no `__isset()`, but on a lazy object it triggers the initialization.
The property checker now tells when `isset()` can be skipped: non-public or static property, and no `__isset()` method. Classes with `__isset()` keep the current behavior.
This is not a regression of 3.30: 3.28 behaves the same. It shows up once `enable_native_lazy_objects` is enabled in Doctrine.
Commits
-------
cfefc6f6b2 Fix reading an attribute through its getter initializing a lazy object when the property is not public
This PR was merged into the 3.x branch.
Discussion
----------
Speed up applying `html_cva()` recipes that resolve to classes already seen
`Cva::apply()` splits the resolved classes with `preg_split()` and deduplicates them with `array_unique()` on every call. Components call `html_cva()` on every render and apply the same few variant combinations, so most of that work is repeated: on an admin dashboard built with the Symfony UX Toolkit Shadcn kit, the 486 calls per request resolve to only 38 distinct class strings (92% repeats), around 550 bytes and 37 classes each. Splitting and deduplicating them is about 1.5 µs out of a 2 µs call.
The result now goes into a static array keyed by the resolved class string, the only thing it depends on. The array is static because `html_cva()` creates a new `Cva` on every call. It holds at most 1,000 entries and is emptied when full.
- `Cva::apply()` time per dashboard request (hrtime around the method, cache emptied between requests like PHP-FPM, 6 interleaved runs of 20 requests each side): **1.27-1.35 ms** -> **0.69-0.73 ms** (-46%); with the cache kept between requests, as in a long-running worker: **0.58-0.61 ms**
- standalone benchmark (500 buttons using the Shadcn button recipe, 24 distinct variant/size pairs, cache emptied between renders, 5 interleaved runs each side, identical output): **510-517 ms** -> **432-434 ms** (-16%)
- Blackfire (5 samples of 5 requests): wall time **2.52 s** -> **2.40 s**
On the whole dashboard request, this saves about 0.6 ms out of 46 ms (~1.3%).
```php
<?php
// Run from extra/html-extra: php bench.php
use Twig\Environment;
use Twig\Extra\Html\Cva;
use Twig\Extra\Html\HtmlExtension;
use Twig\Loader\ArrayLoader;
require __DIR__.'/vendor/autoload.php';
// The button recipe of the Symfony UX Toolkit Shadcn kit
$twig = new Environment(new ArrayLoader([
'button.html.twig' => <<<'TWIG'
{%- set style = html_cva(
base: "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
variants: {
variant: {
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
outline: 'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
ghost: 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
destructive: 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2',
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2',
icon: 'size-8',
'icon-xs': "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
'icon-sm': 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
'icon-lg': 'size-9',
},
},
) -%}
<button class="{{ style.apply({variant: variant, size: size}) }}">{{ label }}</button>
TWIG,
// 500 buttons, 24 distinct variant/size pairs
'page.html.twig' => <<<'TWIG'
{% for i in 1..500 %}{{ include('button.html.twig', {variant: ['default', 'outline', 'secondary', 'ghost', 'destructive', 'link'][i % 6], size: ['default', 'sm', 'lg', 'icon'][i % 4], label: 'Button ' ~ i}) }}{% endfor %}
TWIG,
]));
$twig->addExtension(new HtmlExtension());
// PHP-FPM empties static properties at the end of every request
$render = static function () use ($twig): string {
if (property_exists(Cva::class, 'classListCache')) {
(new \ReflectionProperty(Cva::class, 'classListCache'))->setValue(null, []);
}
return $twig->render('page.html.twig');
};
$html = $render();
$times = [];
for ($run = 0; $run < 7; ++$run) {
$start = hrtime(true);
for ($i = 0; $i < 100; ++$i) {
$render();
}
$times[] = (hrtime(true) - $start) / 1e6;
}
sort($times);
printf("100 renders of 500 buttons: median %.0f ms (min %.0f, max %.0f) | sha1 %s\n", $times[3], $times[0], $times[6], sha1($html));
```
Blackfire profiles (dashboard page):
- before: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/57d327e3-02f6-4d6d-b27c-a39d3ce0438e/graph
- after: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/f63c01a9-adab-48bd-a524-67d576ab5ebf/graph
- comparison: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/compare/57d327e3-02f6-4d6d-b27c-a39d3ce0438e...f63c01a9-adab-48bd-a524-67d576ab5ebf/graph
This dashboard is available at https://github.com/Kocal/sf-ux-perfs-twig-components.
Commits
-------
9e50ff7083 Speed up applying `html_cva()` recipes that resolve to classes already seen
`Cva::apply()` splits the resolved classes with `preg_split()` and deduplicates them with `array_unique()` on every call. Components call `html_cva()` on every render and apply the same few variant combinations, so most of that work is repeated: on an admin dashboard built with the Symfony UX Toolkit Shadcn kit, the 486 calls per request resolve to only 38 distinct class strings (92% repeats), around 550 bytes and 37 classes each. Splitting and deduplicating them is about 1.5 µs out of a 2 µs call.
The result now goes into a static array keyed by the resolved class string, the only thing it depends on. The array is static because `html_cva()` creates a new `Cva` on every call. It holds at most 1,000 entries and is emptied when full.
- `Cva::apply()` time per dashboard request (hrtime around the method, cache emptied between requests like PHP-FPM, 6 interleaved runs of 20 requests each side): **1.27-1.35 ms** -> **0.69-0.73 ms** (-46%); with the cache kept between requests, as in a long-running worker: **0.58-0.61 ms**
- standalone benchmark (500 buttons using the Shadcn button recipe, 24 distinct variant/size pairs, cache emptied between renders, 5 interleaved runs each side, identical output): **510-517 ms** -> **432-434 ms** (-16%)
- Blackfire (5 samples of 5 requests): wall time **2.52 s** -> **2.40 s**
On the whole dashboard request, this saves about 0.6 ms out of 46 ms (~1.3%).
```php
<?php
// Run from extra/html-extra: php bench.php
use Twig\Environment;
use Twig\Extra\Html\Cva;
use Twig\Extra\Html\HtmlExtension;
use Twig\Loader\ArrayLoader;
require __DIR__.'/vendor/autoload.php';
// The button recipe of the Symfony UX Toolkit Shadcn kit
$twig = new Environment(new ArrayLoader([
'button.html.twig' => <<<'TWIG'
{%- set style = html_cva(
base: "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
variants: {
variant: {
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
outline: 'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
ghost: 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
destructive: 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2',
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2',
icon: 'size-8',
'icon-xs': "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
'icon-sm': 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
'icon-lg': 'size-9',
},
},
) -%}
<button class="{{ style.apply({variant: variant, size: size}) }}">{{ label }}</button>
TWIG,
// 500 buttons, 24 distinct variant/size pairs
'page.html.twig' => <<<'TWIG'
{% for i in 1..500 %}{{ include('button.html.twig', {variant: ['default', 'outline', 'secondary', 'ghost', 'destructive', 'link'][i % 6], size: ['default', 'sm', 'lg', 'icon'][i % 4], label: 'Button ' ~ i}) }}{% endfor %}
TWIG,
]));
$twig->addExtension(new HtmlExtension());
// PHP-FPM empties static properties at the end of every request
$render = static function () use ($twig): string {
if (property_exists(Cva::class, 'classListCache')) {
(new \ReflectionProperty(Cva::class, 'classListCache'))->setValue(null, []);
}
return $twig->render('page.html.twig');
};
$html = $render();
$times = [];
for ($run = 0; $run < 7; ++$run) {
$start = hrtime(true);
for ($i = 0; $i < 100; ++$i) {
$render();
}
$times[] = (hrtime(true) - $start) / 1e6;
}
sort($times);
printf("100 renders of 500 buttons: median %.0f ms (min %.0f, max %.0f) | sha1 %s\n", $times[3], $times[0], $times[6], sha1($html));
```
Blackfire profiles (dashboard page):
- before: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/57d327e3-02f6-4d6d-b27c-a39d3ce0438e/graph
- after: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/f63c01a9-adab-48bd-a524-67d576ab5ebf/graph
- comparison: https://app.blackfire.io/envs/5f4f9a62-eaa0-45ee-b7b3-a1b879f550e9/profiles/compare/57d327e3-02f6-4d6d-b27c-a39d3ce0438e...f63c01a9-adab-48bd-a524-67d576ab5ebf/graph
This dashboard is available at https://github.com/Kocal/sf-ux-perfs-twig-components.
This PR was merged into the 3.x branch.
Discussion
----------
Fix compiled float literals depending on the serialize_precision ini setting
`Compiler::repr()` writes floats with `var_export()`, which follows the `serialize_precision` ini setting. With `serialize_precision=17`, `0.1` compiles to `0.10000000000000001`, so two servers configured differently compile the same template to different code. This pins the setting to `-1` while dumping a float, the same way the numeric locale is already pinned.
Found while checking that two builds of the same Symfony app give the same cache files.
Commits
-------
e53806e99c Fix compiled float literals depending on the serialize_precision ini setting
This PR was squashed before being merged into the 3.x branch.
Discussion
----------
Document the backward compatibility promise
Commits
-------
3f78c6fba4 Document the backward compatibility promise
This PR was merged into the 3.x branch.
Discussion
----------
Speed up loading compiled templates that OPcache already holds
`FilesystemCache::load()` calls `is_file()` before including a compiled template, so each template costs a stat() call on each request, even when OPcache already holds the file. This asks `opcache_is_script_cached()` first. On the Symfony Demo in prod, with `opcache.validate_timestamps` on or off, loading a compiled template goes from 8.7µs to 4.7µs, which saves 11µs to 36µs per page for the 3 to 9 templates it renders.
OPcache isn't asked when `FORCE_BYTECODE_INVALIDATION` is set: it can still serve a compiled file removed from disk, while that option wants it compiled again. Without the option, what gets loaded doesn't change: Twig compiles a missing file again today, then `include_once` gets the same entry from OPcache anyway. It isn't asked either when `opcache.restrict_api` is set, since the call warns then, nor when it validates timestamps on each call, since it stats the file anyway then.
Merge-up to 4.x: the CHANGELOG entry doesn't go to 4.x.
Commits
-------
ef3780c2e0 Speed up loading compiled templates that OPcache already holds
This PR was merged into the 3.x branch.
Discussion
----------
Speed up traversing nodes with node visitors
`NodeTraverser` walks the whole tree once per node visitor, and it iterated over the children of each node with `foreach ($node as ...)`, which allocates an `ArrayIterator` per node and per visitor. It now reads the children array through a new internal `Node::getNodes()` method. On the Symfony Demo, where six visitors are registered, walking the 21k nodes of its 52 templates takes half the instructions it took, and warming up its Twig cache runs 15% fewer instructions and takes 520ms instead of 566ms.
The traverser doesn't go through `getIterator()` anymore, so a subclass overriding it to hide children from visitors would be bypassed, and one declaring its own `getNodes()` would clash; I found none in Twig, Symfony or UX. Calling `getArrayCopy()` on the iterator returned by `getIterator()` would avoid the new method, but it saves 13% of the traversal instructions instead of 50%: `new \ArrayIterator()` and `getArrayCopy()` both copy the array.
<details>
<summary>Merge-up notes</summary>
To 4.x: the patch applies as is (I ran the 4.x suite with it), the CHANGELOG entry doesn't go to 4.x.
</details>
Commits
-------
69fefec125 Speed up traversing nodes with node visitors
This PR was merged into the 3.x branch.
Discussion
----------
Allow skipping the directory check when adding paths to `FilesystemLoader`
`FilesystemLoader::addPath()` and `prependPath()` call `is_dir()` on each path, so an app built from a compiled container pays for it on each request, even though the paths were checked when the container was built. This adds a third argument to skip the check:
```php
$loader->addPath($templateDir, 'admin', false);
```
It's read with `func_get_arg()` because Drupal and Contao override both methods with two arguments. On the Symfony Demo, where TwigBundle registers 14 paths, building the loader goes from 64µs to 21µs per request once TwigBundle passes `false` (symfony/symfony#66334).
<details>
<summary>Merge-up notes</summary>
To 4.x: declare the argument for real, `bool $check = true`, instead of reading it with `func_get_arg()`; the tests apply as is. The CHANGELOG and `doc/deprecated.rst` entries don't go to 4.x, and the `versionadded` block has to be removed from `doc/api.rst` since the 4.x docs have none.
</details>
Commits
-------
9e1e2e2cea Allow skipping the directory check when adding paths to `FilesystemLoader`
This PR was merged into the 3.x branch.
Discussion
----------
Fix the deprecated tag generating invalid PHP for an integer message
Closes#4978
Commits
-------
c6f1ef9521 Fix the deprecated tag generating invalid PHP for an integer message
This PR was merged into the 3.x branch.
Discussion
----------
Fix the loop variable of a for tag being undefined in the sequence of a nested loop
Commits
-------
0c90971a98 Fix the loop variable of a for tag being undefined in the sequence of a nested loop
This PR was merged into the 3.x branch.
Discussion
----------
Reject the spread operator outside sequences, mappings, and call arguments
Closes#4977
Commits
-------
a04ae6d636 Reject the spread operator outside sequences, mappings, and call arguments
This PR was merged into the 3.x branch.
Discussion
----------
Discard what the non-selected branch of a guard tag registers on the parser
Commits
-------
d5011f4654 Discard what the non-selected branch of a guard tag registers on the parser
This PR was merged into the 3.x branch.
Discussion
----------
Reset the escaping state when compiling a new template after a compilation error
Commits
-------
39642e4cdf Reset the escaping state when compiling a new template after a compilation error
This PR was merged into the 3.x branch.
Discussion
----------
Reject a Template instance created by another environment when resolving templates
Commits
-------
605b1278f7 Reject a Template instance created by another environment when resolving templates
This PR was squashed before being merged into the 3.x branch.
Discussion
----------
3.x doc fixes
Commits
-------
1478f9efcd Fix the class name and a missing parenthesis in the runtime loader example
e883536e64 Replace the deprecated twig_test_even function in the custom test example
db3aeb7379 Use the current variable node class in the node tree example
This PR was merged into the 3.x branch.
Discussion
----------
Fix the autoescape documentation about static expressions
Commits
-------
95a176f2bf Fix the autoescape documentation about static expressions
This PR was merged into the 3.x branch.
Discussion
----------
Fix the sandbox not reporting the line of a rejected guard tag
Commits
-------
b1d6b51183 Fix the sandbox not reporting the line of a rejected guard tag
This PR was merged into the 3.x branch.
Discussion
----------
Use sandbox policy examples that remain meaningful in Twig 4.0
Commits
-------
677f049e64 Use sandbox policy examples that remain meaningful in Twig 4.0
This PR was merged into the 3.x branch.
Discussion
----------
Rewrite the sandbox documentation around what developers need to know
Commits
-------
9d24327f8d Rewrite the sandbox documentation around what developers need to know