Skip to content

CoreExtension::getAttribute: small improvement regarding getter/isser/hasser #4662

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: 3.x
Choose a base branch
from

Conversation

gharlan
Copy link
Contributor

@gharlan gharlan commented Jul 5, 2025

For a getter method like getFirstName it is common to call it in twig via person.firstName.
But at the moment twig is adding these variants to the class method cache: getFirstName, getfirstname, FirstName and firstname.
So when resolving the name, it uses the first elseif here with additional strtolower call, because firstName is missing:

if (isset($cache[$class][$item])) {
$method = $cache[$class][$item];
} elseif (isset($cache[$class][$lcItem = strtolower($item)])) {
$method = $cache[$class][$lcItem];
} elseif (isset($cache[$class]['__call'])) {

This PR replaces FirstName with firstName in the method cache.
So person.firstName is resolved via first if branch (but person.FirstName would use the elseif with strtolower now).

@fabpot fabpot requested a review from Copilot July 6, 2025 17:12
Copilot

This comment was marked as outdated.

@gharlan gharlan force-pushed the get-attribute-performance branch 2 times, most recently from b173e57 to f8d742e Compare July 11, 2025 13:54
@gharlan gharlan force-pushed the get-attribute-performance branch from f8d742e to 45cd6ff Compare July 29, 2025 08:12
@fabpot fabpot requested a review from Copilot August 2, 2025 12:32
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR optimizes the method name caching in Twig's CoreExtension by improving how getter/is/has method names are cached to reduce unnecessary string operations during attribute resolution. The main goal is to cache method names in their camelCase form (e.g., firstName instead of FirstName) so that common property access patterns like person.firstName can be resolved more efficiently.

Key changes:

  • Refactored prefix handling for getter/is/has methods to use a common $prefixLength variable
  • Modified the cache key generation to store camelCase names (e.g., firstName) instead of PascalCase names (e.g., FirstName)
  • Updated comments to reflect the improved caching strategy

@fabpot
Copy link
Contributor

fabpot commented Aug 2, 2025

I suppose we're looking for a performance improvement here.
Have you tried to measure it because I'm wondering if we could do something like this instead to simplify the code and remove the special cases?

--- a/src/Extension/CoreExtension.php
+++ b/src/Extension/CoreExtension.php
@@ -1826,17 +1826,13 @@ final class CoreExtension extends AbstractExtension
             $lcMethods = array_map('strtolower', $methods);
             $classCache = [];
             foreach ($methods as $i => $method) {
-                $classCache[$method] = $method;
                 $classCache[$lcName = $lcMethods[$i]] = $method;

                 if ('g' === $lcName[0] && str_starts_with($lcName, 'get')) {
-                    $name = substr($method, 3);
                     $lcName = substr($lcName, 3);
                 } elseif ('i' === $lcName[0] && str_starts_with($lcName, 'is')) {
-                    $name = substr($method, 2);
                     $lcName = substr($lcName, 2);
                 } elseif ('h' === $lcName[0] && str_starts_with($lcName, 'has')) {
-                    $name = substr($method, 3);
                     $lcName = substr($lcName, 3);
                     if (\in_array('is'.$lcName, $lcMethods, true)) {
                         continue;
@@ -1846,23 +1842,15 @@ final class CoreExtension extends AbstractExtension
                 }

                 // skip get() and is() methods (in which case, $name is empty)
-                if ($name) {
-                    if (!isset($classCache[$name])) {
-                        $classCache[$name] = $method;
-                    }
-
-                    if (!isset($classCache[$lcName])) {
-                        $classCache[$lcName] = $method;
-                    }
+                if ($lcName && !isset($classCache[$lcName])) {
+                    $classCache[$lcName] = $method;
                 }
             }
             $cache[$class] = $classCache;
         }

         $call = false;
-        if (isset($cache[$class][$item])) {
-            $method = $cache[$class][$item];
-        } elseif (isset($cache[$class][$lcItem = strtolower($item)])) {
+        if (isset($cache[$class][$lcItem = strtolower($item)])) {
             $method = $cache[$class][$lcItem];
         } elseif (isset($cache[$class]['__call'])) {
             $method = $item;

Memory should be a bit less and I expect the performance to be (almost) the same.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants