Repository: twigphp/Twig (php)

Issue:
Twig functions becoming internal
Since 3.9 it appears all of Twigs filter/functions underlying PHP implementations are marked as internal, could this be reconsidered in some cases? I'm assuming internal means the signature might change in non backwards compatible ways, if it doesn't I guess this isn't really a problem.

The two cases that affect myself are `twig_escape_filter` and `twig_date_converter`.

For example I have a filter which generates some HTML:

```php
new TwigFilter('phone_number_link', function (Environment $env, $number) {
    $anchor = $this->libphonenumber->format($number, PhoneNumberFormat::RFC3966);

    return sprintf(
        '<a href="%s">%s</a>',
        \twig_escape_filter($env, $anchor, 'html_attr'),
        \twig_escape_filter($env, $this->formatDisplay($number), 'html'),
    );
}, ['needs_environment' => true, 'is_safe' => ['html']]),
```

I've consulted the documentation but it doesn't seem to offer any examples on how to deal with more complicated situations such as this, so perhaps this is the wrong way of doing it but it's worked since Twig 1.x.

Likewise with dates I have some custom filters to limit developers to a limited set of predefined formats, the filters look a bit like this:

```php
new TwigFilter('date_formal_format', function (Environment $env, $date, ?string $locale = null) {
    if (null === $date) {
        return null;
    }

    return DateFormat::formalDate(twig_date_converter($env, $date), $locale ?? $this->locale);
}, ['needs_environment' => true]),
```

Being able to use `twig_date_converter` means the Twig timezone setting works as expected.

I did some searching on GitHub and found numerous other cases.

https://github.com/theoboldt/juvem/blob/98af699c95c46e919dc1690ab1730754ebd65141/app/src/AppBundle/Twig/Extension/CustomFieldValue.php#L244
https://github.com/njh/twig-html-helpers/blob/d3dc45f6a9ac67512126c02a8d11c7a09889d942/lib/Twig/Extension/HTMLHelpers.php#L53
https://github.com/symfony/ux/blob/d4df61465571381ffa8692268fca6acd09b32feb/src/StimulusBundle/src/Dto/StimulusAttributes.php#L223
https://github.com/symfony/ux/blob/d4df61465571381ffa8692268fca6acd09b32feb/src/LiveComponent/src/Util/LiveAttributesCollection.php#L121
https://github.com/studiometa/twig-toolkit/blob/9e73108d84c11171ef69fd34dd8946afee8e9a5c/src/Helpers/Html.php#L209
https://github.com/mautic/mautic/blob/175ceac9098f0b9515f34820a0eafd924877864b/plugins/MauticFocusBundle/Model/FocusModel.php#L183
https://github.com/symfony/symfony/blob/a2d03c548c2e897be78504fd4a389bb7eb41e756/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php#L116
https://github.com/drupal/drupal/blob/449f6fbf7c058988ba6f18a8e70cba4bf6639941/core/lib/Drupal/Core/Template/TwigExtension.php#L464
https://github.com/phpbb/phpbb/blob/4c721de243967ed4cf372caef4e54142ee6bcf6c/phpBB/phpbb/template/twig/extension.php#L215
https://github.com/IISHF/web2019/blob/0ca849abf7f247abfcf01521b6cdeee22e1d637b/src/Infrastructure/Twig/DateExtension.php#L108-L115
https://github.com/imarc/cms/blob/ef349e03202628638247a71158ff04c49da2068a/src/web/twig/Extension.php#L482
https://github.com/twigphp/intl-extra/blob/00cd46d7860cf7e814f73b290edf32801a47cafc/IntlExtension.php#L282

Pull request:
Move some static extension methods to non-static
A first step to resolve #4040

Patch:
diff --git a/extra/intl-extra/IntlExtension.php b/extra/intl-extra/IntlExtension.php
index 1e632a3a..e87fd086 100644
--- a/extra/intl-extra/IntlExtension.php
+++ b/extra/intl-extra/IntlExtension.php
@@ -369,7 +369,7 @@ final class IntlExtension extends AbstractExtension
      */
     public function formatDateTime(Environment $env, $date, ?string $dateFormat = 'medium', ?string $timeFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', ?string $locale = null): string
     {
-        $date = CoreExtension::convertDate($env, $date, $timezone);
+        $date = $env->getExtension(CoreExtension::class)->convertDate($date, $timezone);
 
         $formatterTimezone = $timezone;
         if (null === $formatterTimezone || false === $formatterTimezone) {
diff --git a/extra/intl-extra/composer.json b/extra/intl-extra/composer.json
index a46eada3..b3c3ceff 100644
--- a/extra/intl-extra/composer.json
+++ b/extra/intl-extra/composer.json
@@ -16,7 +16,7 @@
     ],
     "require": {
         "php": ">=7.2.5",
-        "twig/twig": "^3.9",
+        "twig/twig": "^3.10",
         "symfony/intl": "^5.4|^6.4|^7.0"
     },
     "require-dev": {
diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php
index c193a7f0..79e6cd76 100644
--- a/src/Extension/CoreExtension.php
+++ b/src/Extension/CoreExtension.php
@@ -185,11 +185,11 @@ final class CoreExtension extends AbstractExtension
     {
         return [
             // formatting filters
-            new TwigFilter('date', [self::class, 'formatDate'], ['needs_environment' => true]),
-            new TwigFilter('date_modify', [self::class, 'modifyDate'], ['needs_environment' => true]),
+            new TwigFilter('date', [$this, 'formatDate']),
+            new TwigFilter('date_modify', [$this, 'modifyDate']),
             new TwigFilter('format', [self::class, 'sprintf']),
             new TwigFilter('replace', [self::class, 'replace']),
-            new TwigFilter('number_format', [self::class, 'formatNumber'], ['needs_environment' => true]),
+            new TwigFilter('number_format', [$this, 'formatNumber']),
             new TwigFilter('abs', 'abs'),
             new TwigFilter('round', [self::class, 'round']),
 
@@ -241,7 +241,7 @@ final class CoreExtension extends AbstractExtension
             new TwigFunction('constant', [self::class, 'constant']),
             new TwigFunction('cycle', [self::class, 'cycle']),
             new TwigFunction('random', [self::class, 'random'], ['needs_charset' => true]),
-            new TwigFunction('date', [self::class, 'convertDate'], ['needs_environment' => true]),
+            new TwigFunction('date', [$this, 'convertDate']),
             new TwigFunction('include', [self::class, 'include'], ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]),
             new TwigFunction('source', [self::class, 'source'], ['needs_environment' => true, 'is_safe' => ['all']]),
         ];
@@ -416,10 +416,10 @@ final class CoreExtension extends AbstractExtension
      *
      * @internal
      */
-    public static function formatDate(Environment $env, $date, $format = null, $timezone = null): string
+    public function formatDate($date, $format = null, $timezone = null): string
     {
         if (null === $format) {
-            $formats = $env->getExtension(self::class)->getDateFormat();
+            $formats = $this->getDateFormat();
             $format = $date instanceof \DateInterval ? $formats[1] : $formats[0];
         }
 
@@ -427,7 +427,7 @@ final class CoreExtension extends AbstractExtension
             return $date->format($format);
         }
 
-        return self::convertDate($env, $date, $timezone)->format($format);
+        return $this->convertDate($date, $timezone)->format($format);
     }
 
     /**
@@ -442,9 +442,9 @@ final class CoreExtension extends AbstractExtension
      *
      * @internal
      */
-    public static function modifyDate(Environment $env, $date, $modifier)
+    public function modifyDate($date, $modifier)
     {
-        return self::convertDate($env, $date, false)->modify($modifier);
+        return $this->convertDate($date, false)->modify($modifier);
     }
 
     /**
@@ -460,6 +460,14 @@ final class CoreExtension extends AbstractExtension
         return sprintf($format ?? '', ...$values);
     }
 
+    /**
+     * @internal
+     */
+    public static function dateConverter(Environment $env, $date, $format = null, $timezone = null): string
+    {
+        return $env->getExtension(CoreExtension::class)->formatDate($date, $format, $timezone);
+    }
+
     /**
      * Converts an input to a \DateTime instance.
      *
@@ -474,12 +482,12 @@ final class CoreExtension extends AbstractExtension
      *
      * @internal
      */
-    public static function convertDate(Environment $env, $date = null, $timezone = null)
+    public function convertDate($date = null, $timezone = null)
     {
         // determine the timezone
         if (false !== $timezone) {
             if (null === $timezone) {
-                $timezone = $env->getExtension(self::class)->getTimezone();
+                $timezone = $this->getTimezone();
             } elseif (!$timezone instanceof \DateTimeZone) {
                 $timezone = new \DateTimeZone($timezone);
             }
@@ -504,14 +512,14 @@ final class CoreExtension extends AbstractExtension
                 $date = 'now';
             }
 
-            return new \DateTime($date, false !== $timezone ? $timezone : $env->getExtension(self::class)->getTimezone());
+            return new \DateTime($date, false !== $timezone ? $timezone : $this->getTimezone());
         }
 
         $asString = (string) $date;
         if (ctype_digit($asString) || (!empty($asString) && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) {
             $date = new \DateTime('@'.$date);
         } else {
-            $date = new \DateTime($date, $env->getExtension(self::class)->getTimezone());
+            $date = new \DateTime($date, $this->getTimezone());
         }
 
         if (false !== $timezone) {
@@ -565,7 +573,7 @@ final class CoreExtension extends AbstractExtension
     }
 
     /**
-     * Number format filter.
+     * Formats a number.
      *
      * All of the formatting options can be left null, in that case the defaults will
      * be used. Supplying any of the parameters will override the defaults set in the
@@ -578,9 +586,9 @@ final class CoreExtension extends AbstractExtension
      *
      * @internal
      */
-    public static function formatNumber(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null): string
+    public function formatNumber($number, $decimal = null, $decimalPoint = null, $thousandSep = null): string
     {
-        $defaults = $env->getExtension(self::class)->getNumberFormat();
+        $defaults = $this->getNumberFormat();
         if (null === $decimal) {
             $decimal = $defaults[0];
         }
diff --git a/src/Resources/core.php b/src/Resources/core.php
index 0d4cfc91..18a441fa 100644
--- a/src/Resources/core.php
+++ b/src/Resources/core.php
@@ -42,7 +42,7 @@ function twig_date_format_filter(Environment $env, $date, $format = null, $timez
 {
     trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
 
-    return CoreExtension::formatDate($env, $date, $format, $timezone);
+    return $env->getExtension(CoreExtension::class)->formatDate($date, $format, $timezone);
 }
 
 /**
@@ -53,7 +53,7 @@ function twig_date_modify_filter(Environment $env, $date, $modifier)
 {
     trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
 
-    return CoreExtension::modifyDate($env, $date, $modifier);
+    return $env->getExtension(CoreExtension::class)->modifyDate($date, $modifier);
 }
 
 /**
@@ -75,7 +75,7 @@ function twig_date_converter(Environment $env, $date = null, $timezone = null)
 {
     trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
 
-    return CoreExtension::convertDate($env, $date, $timezone);
+    return $env->getExtension(CoreExtension::class)->convertDate($date, $timezone);
 }
 
 /**
@@ -108,7 +108,7 @@ function twig_number_format_filter(Environment $env, $number, $decimal = null, $
 {
     trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__);
 
-    return CoreExtension::formatNumber($env, $number, $decimal, $decimalPoint, $thousandSep);
+    return $env->getExtension(CoreExtension::class)->formatNumber($number, $decimal, $decimalPoint, $thousandSep);
 }
 
 /**


Test patch:
diff --git a/tests/Node/Expression/FilterTest.php b/tests/Node/Expression/FilterTest.php
index e28dd73d..f99d6242 100644
--- a/tests/Node/Expression/FilterTest.php
+++ b/tests/Node/Expression/FilterTest.php
@@ -69,7 +69,7 @@ class FilterTest extends NodeTestCase
         $node = $this->createFilter($expr, 'upper');
         $node = $this->createFilter($node, 'number_format', [new ConstantExpression(2, 1), new ConstantExpression('.', 1), new ConstantExpression(',', 1)]);
 
-        $tests[] = [$node, 'Twig\Extension\CoreExtension::formatNumber($this->env, Twig\Extension\CoreExtension::upper($this->env->getCharset(), "foo"), 2, ".", ",")'];
+        $tests[] = [$node, '$this->extensions[\'Twig\Extension\CoreExtension\']->formatNumber(Twig\Extension\CoreExtension::upper($this->env->getCharset(), "foo"), 2, ".", ",")'];
 
         // named arguments
         $date = new ConstantExpression(0, 1);
@@ -77,14 +77,14 @@ class FilterTest extends NodeTestCase
             'timezone' => new ConstantExpression('America/Chicago', 1),
             'format' => new ConstantExpression('d/m/Y H:i:s P', 1),
         ]);
-        $tests[] = [$node, 'Twig\Extension\CoreExtension::formatDate($this->env, 0, "d/m/Y H:i:s P", "America/Chicago")'];
+        $tests[] = [$node, '$this->extensions[\'Twig\Extension\CoreExtension\']->formatDate(0, "d/m/Y H:i:s P", "America/Chicago")'];
 
         // skip an optional argument
         $date = new ConstantExpression(0, 1);
         $node = $this->createFilter($date, 'date', [
             'timezone' => new ConstantExpression('America/Chicago', 1),
         ]);
-        $tests[] = [$node, 'Twig\Extension\CoreExtension::formatDate($this->env, 0, null, "America/Chicago")'];
+        $tests[] = [$node, '$this->extensions[\'Twig\Extension\CoreExtension\']->formatDate(0, null, "America/Chicago")'];
 
         // underscores vs camelCase for named arguments
         $string = new ConstantExpression('abc', 1);
diff --git a/tests/Node/Expression/FunctionTest.php b/tests/Node/Expression/FunctionTest.php
index aeabfc3c..aa40e355 100644
--- a/tests/Node/Expression/FunctionTest.php
+++ b/tests/Node/Expression/FunctionTest.php
@@ -76,7 +76,7 @@ class FunctionTest extends NodeTestCase
             'timezone' => new ConstantExpression('America/Chicago', 1),
             'date' => new ConstantExpression(0, 1),
         ]);
-        $tests[] = [$node, 'Twig\Extension\CoreExtension::convertDate($this->env, 0, "America/Chicago")'];
+        $tests[] = [$node, '$this->extensions[\'Twig\Extension\CoreExtension\']->convertDate(0, "America/Chicago")'];
 
         // arbitrary named arguments
         $node = $this->createFunction('barbar');