Skip to content

Conversation

@gsteel
Copy link

@gsteel gsteel commented Nov 25, 2025

Whilst working on the Mezzio adapter for FastRoute, I noticed it's "impossible" to create dispatch results without writing to soft @readonly properties.

Psalm hates that… so I took the liberty of dropping PHP 8.1, and making the results readonly classes with constructors.

Mainly this is useful in testing…

I've also added 8.5 to CI

Sorry if this patch is doing too much 😬

Marks the classes as `readonly` and makes use of constructor property promotion.

The reason for this change is to allow consumers to safely create immutable results that can be used in tests.

Previously, without the constructor, consumers using SA tooling could not set the readonly properties after construction.
@lcobucci
Copy link
Collaborator

lcobucci commented Dec 3, 2025

I need to investigate this one... when I introduced the result object I saw negative impact on the benchmark using the constructor...

Out of curiosity, why do you need to instantiate this?

@gsteel
Copy link
Author

gsteel commented Dec 3, 2025

The main reason for this patch is so I can instantiate result values without writing to @readonly properties. This is not necessary for the mezzio-fastroute lib itself, just its tests (See mezzio-fastroute#51)

I ran the benchmarks locally with a retry threshold of 5 with:

phpbench run -l dots --report aggregate --retry-threshold=5 --ref=master

Looks to me like that's not an insignificant performance penalty.

I wonder if a static/named constructor might improve things 🤔

master vs current
benchmark subject set revs its mem_peak mode rstdev
RoutingWithManyRoutesBench staticFirstRoute group_count 250 5 6.084mb -0.03% 0.300μs +32.41% ±2.68% +103.16%
RoutingWithManyRoutesBench staticFirstRoute char_count 250 5 6.084mb -0.03% 0.306μs +39.11% ±2.78% +283.07%
RoutingWithManyRoutesBench staticFirstRoute group_pos 250 5 6.084mb -0.03% 0.292μs +32.59% ±0.67% -72.34%
RoutingWithManyRoutesBench staticFirstRoute mark 250 5 6.084mb -0.03% 0.288μs +27.32% ±0.55% -72.83%
RoutingWithManyRoutesBench dynamicFirstRoute group_count 250 5 6.084mb -0.03% 0.614μs +13.67% ±0.89% -61.35%
RoutingWithManyRoutesBench dynamicFirstRoute char_count 250 5 6.084mb -0.03% 0.773μs +15.11% ±3.01% +15.14%
RoutingWithManyRoutesBench dynamicFirstRoute group_pos 250 5 6.084mb -0.03% 0.664μs +13.31% ±2.27% +151.63%
RoutingWithManyRoutesBench dynamicFirstRoute mark 250 5 6.084mb -0.03% 0.651μs +6.34% ±1.98% +19.91%
RoutingWithManyRoutesBench staticLastRoute group_count 250 5 6.084mb -0.03% 0.297μs +32.17% ±1.07% -40.69%
RoutingWithManyRoutesBench staticLastRoute char_count 250 5 6.084mb -0.03% 0.296μs +32.51% ±1.82% +25.92%
RoutingWithManyRoutesBench staticLastRoute group_pos 250 5 6.084mb -0.03% 0.297μs +32.75% ±2.03% +16411337417706000.00%
RoutingWithManyRoutesBench staticLastRoute mark 250 5 6.084mb -0.03% 0.290μs +29.57% ±2.93% +232.16%
RoutingWithManyRoutesBench dynamicLastRoute group_count 250 5 6.084mb -0.03% 13.521μs +6.75% ±2.25% +39.39%
RoutingWithManyRoutesBench dynamicLastRoute char_count 250 5 6.084mb -0.03% 11.395μs +6.17% ±1.55% +18.38%
RoutingWithManyRoutesBench dynamicLastRoute group_pos 250 5 6.084mb -0.03% 13.263μs +0.78% ±2.86% +5.24%
RoutingWithManyRoutesBench dynamicLastRoute mark 250 5 6.084mb -0.03% 10.328μs +6.82% ±2.12% +125.98%
RoutingWithManyRoutesBench staticInvalidMethod group_count 250 5 6.084mb -0.03% 3.503μs +4.91% ±1.79% -9.82%
RoutingWithManyRoutesBench staticInvalidMethod char_count 250 5 6.084mb -0.03% 2.888μs +8.81% ±2.83% +332.31%
RoutingWithManyRoutesBench staticInvalidMethod group_pos 250 5 6.084mb -0.03% 3.564μs +5.63% ±2.22% -26.22%
RoutingWithManyRoutesBench staticInvalidMethod mark 250 5 6.084mb -0.03% 2.091μs -0.99% ±0.74% -58.71%
RoutingWithManyRoutesBench dynamicInvalidMethod group_count 250 5 6.084mb -0.03% 13.740μs +0.46% ±1.40% -37.29%
RoutingWithManyRoutesBench dynamicInvalidMethod char_count 250 5 6.084mb -0.03% 11.818μs +4.12% ±1.11% -48.87%
RoutingWithManyRoutesBench dynamicInvalidMethod group_pos 250 5 6.084mb -0.03% 13.749μs +5.86% ±1.53% +58.28%
RoutingWithManyRoutesBench dynamicInvalidMethod mark 250 5 6.084mb -0.03% 10.543μs +4.95% ±1.57% -42.45%
RoutingWithManyRoutesBench unknownRoute group_count 250 5 6.084mb -0.03% 4.066μs +2.58% ±1.50% +109.05%
RoutingWithManyRoutesBench unknownRoute char_count 250 5 6.084mb -0.03% 2.967μs +4.77% ±2.88% +312.43%
RoutingWithManyRoutesBench unknownRoute group_pos 250 5 6.084mb -0.03% 4.120μs +3.22% ±1.44% +29.66%
RoutingWithManyRoutesBench unknownRoute mark 250 5 6.084mb -0.03% 2.167μs +6.29% ±2.83% +658.48%
RouteRegistrationBench groupCount 100 5 1.920mb 0.00% 90.787μs +0.54% ±0.60% -60.05%
RouteRegistrationBench charCount 100 5 1.920mb 0.00% 92.707μs +3.96% ±2.27% +132.61%
RouteRegistrationBench groupPos 100 5 1.920mb 0.00% 88.355μs +0.30% ±0.71% +122.62%
RouteRegistrationBench mark 100 5 1.918mb 0.00% 89.584μs +1.65% ±1.12% -48.18%
RoutingWithRealLifeExampleBench staticFirstRoute group_count 250 5 1.921mb 0.00% 0.286μs +29.56% ±2.04% -2.60%
RoutingWithRealLifeExampleBench staticFirstRoute char_count 250 5 1.921mb 0.00% 0.285μs +30.58% ±2.84% +107.70%
RoutingWithRealLifeExampleBench staticFirstRoute group_pos 250 5 1.921mb 0.00% 0.287μs +28.61% ±1.36% -6.02%
RoutingWithRealLifeExampleBench staticFirstRoute mark 250 5 1.921mb 0.00% 0.284μs +26.77% ±0.89% -60.56%
RoutingWithRealLifeExampleBench dynamicFirstRoute group_count 250 5 1.921mb 0.00% 0.619μs +6.20% ±1.19% -61.04%
RoutingWithRealLifeExampleBench dynamicFirstRoute char_count 250 5 1.921mb 0.00% 0.718μs +9.82% ±1.92% +94.96%
RoutingWithRealLifeExampleBench dynamicFirstRoute group_pos 250 5 1.921mb 0.00% 0.673μs +9.05% ±2.10% +178.26%
RoutingWithRealLifeExampleBench dynamicFirstRoute mark 250 5 1.921mb 0.00% 0.638μs +10.07% ±1.28% -7.98%
RoutingWithRealLifeExampleBench staticLastRoute group_count 250 5 1.921mb 0.00% 0.286μs +27.60% ±1.65% -23.96%
RoutingWithRealLifeExampleBench staticLastRoute char_count 250 5 1.921mb 0.00% 0.284μs +30.95% ±1.99% -27.33%
RoutingWithRealLifeExampleBench staticLastRoute group_pos 250 5 1.921mb 0.00% 0.291μs +34.34% ±2.04% +8.77%
RoutingWithRealLifeExampleBench staticLastRoute mark 250 5 1.921mb 0.00% 0.286μs +24.23% ±1.52% -3.04%
RoutingWithRealLifeExampleBench dynamicLastRoute group_count 250 5 1.921mb 0.00% 0.852μs +8.91% ±2.56% +212.27%
RoutingWithRealLifeExampleBench dynamicLastRoute char_count 250 5 1.921mb 0.00% 0.828μs +4.61% ±0.61% -77.38%
RoutingWithRealLifeExampleBench dynamicLastRoute group_pos 250 5 1.921mb 0.00% 0.932μs +5.20% ±1.49% +184.14%
RoutingWithRealLifeExampleBench dynamicLastRoute mark 250 5 1.921mb 0.00% 0.738μs +7.54% ±0.95% -53.55%
RoutingWithRealLifeExampleBench staticInvalidMethod group_count 250 5 1.921mb 0.00% 1.612μs +7.73% ±1.42% +157.46%
RoutingWithRealLifeExampleBench staticInvalidMethod char_count 250 5 1.921mb 0.00% 1.766μs +1.18% ±2.38% +13.14%
RoutingWithRealLifeExampleBench staticInvalidMethod group_pos 250 5 1.921mb 0.00% 1.554μs +3.94% ±2.18% +123.42%
RoutingWithRealLifeExampleBench staticInvalidMethod mark 250 5 1.921mb 0.00% 1.514μs +3.14% ±2.62% +110.85%
RoutingWithRealLifeExampleBench dynamicInvalidMethod group_count 250 5 1.921mb 0.00% 1.851μs +4.93% ±1.76% -31.47%
RoutingWithRealLifeExampleBench dynamicInvalidMethod char_count 250 5 1.921mb 0.00% 2.154μs +6.18% ±2.97% +103.63%
RoutingWithRealLifeExampleBench dynamicInvalidMethod group_pos 250 5 1.921mb 0.00% 1.936μs +5.06% ±0.48% -43.43%
RoutingWithRealLifeExampleBench dynamicInvalidMethod mark 250 5 1.921mb 0.00% 1.840μs +4.18% ±0.73% -33.81%
RoutingWithRealLifeExampleBench unknownRoute group_count 250 5 1.921mb 0.00% 1.557μs +0.43% ±1.21% -17.17%
RoutingWithRealLifeExampleBench unknownRoute char_count 250 5 1.921mb 0.00% 1.787μs +0.01% ±1.77% -32.69%
RoutingWithRealLifeExampleBench unknownRoute group_pos 250 5 1.921mb 0.00% 1.529μs -2.72% ±1.63% +33.96%
RoutingWithRealLifeExampleBench unknownRoute mark 250 5 1.921mb 0.00% 1.520μs +0.94% ±0.87% -39.54%
RoutingWithRealLifeExampleBench longestRoute group_count 250 5 1.921mb 0.00% 0.961μs +3.74% ±1.81% -33.73%
RoutingWithRealLifeExampleBench longestRoute char_count 250 5 1.921mb 0.00% 0.957μs +3.74% ±1.61% +34.34%
RoutingWithRealLifeExampleBench longestRoute group_pos 250 5 1.921mb 0.00% 0.998μs +3.14% ±1.64% -53.89%
RoutingWithRealLifeExampleBench longestRoute mark 250 5 1.921mb 0.00% 0.942μs +7.61% ±1.35% -42.20%
UriGenerationBench staticRoutes home 250 5 1.921mb 0.00% 0.528μs -0.92% ±1.68% +53.67%
UriGenerationBench staticRoutes about-us 250 5 1.921mb 0.00% 0.536μs -2.52% ±2.39% +58.74%
UriGenerationBench staticRoutes contact-us 250 5 1.921mb 0.00% 0.543μs +1.63% ±1.51% +4.87%
UriGenerationBench staticRoutes contact-us.submit 250 5 1.921mb 0.00% 0.524μs -2.20% ±1.94% +7.85%
UriGenerationBench staticRoutes blog.index 250 5 1.921mb 0.00% 0.549μs +2.03% ±2.20% +38.36%
UriGenerationBench staticRoutes blog.recent 250 5 1.921mb 0.00% 0.537μs -2.79% ±2.88% +121.98%
UriGenerationBench staticRoutes shop.index 250 5 1.921mb 0.00% 0.534μs -1.78% ±0.56% -65.54%
UriGenerationBench staticRoutes shop.category.index 250 5 1.921mb 0.00% 0.523μs -1.29% ±1.73% +108.17%
UriGenerationBench dynamicRoutes page.show 250 5 1.921mb 0.00% 0.762μs -2.59% ±1.95% +15.69%
UriGenerationBench dynamicRoutes blog.post.show 250 5 1.921mb 0.00% 0.779μs +1.49% ±1.01% -21.13%
UriGenerationBench dynamicRoutes blog.post.comment 250 5 1.921mb 0.00% 0.823μs +1.16% ±1.29% -59.05%
UriGenerationBench dynamicRoutes blog.archive-year 250 5 1.921mb 0.00% 1.199μs +0.75% ±1.64% +230.65%
UriGenerationBench dynamicRoutes blog.archive-year-month 250 5 1.921mb 0.00% 1.244μs -0.22% ±1.22% -32.59%
UriGenerationBench dynamicRoutes blog.archive-year-day 250 5 1.921mb 0.00% 1.272μs -2.69% ±1.73% -39.23%
UriGenerationBench dynamicRoutes shop.category.product.search 250 5 1.921mb 0.00% 1.233μs +0.10% ±0.67% +412.45%

@gsteel
Copy link
Author

gsteel commented Dec 3, 2025

Update:

I tried dropping the constructor and moving instantiation to a static factory method like:

    /**
     * @param array<string, string> $variables
     * @param ExtraParameters       $extraParameters
     */
    public static function new(
        mixed $handler,
        array $variables = [],
        array $extraParameters = [],
    ): self {
        $instance = new self();
        $instance->handler = $handler;
        $instance->variables = $variables;
        $instance->extraParameters = $extraParameters;

        return $instance;
    }

This had little effect on the performance penalty, also removing ArrayAccess entirely made no difference either.

Please feel free to close 👍

@lcobucci
Copy link
Collaborator

lcobucci commented Dec 4, 2025

It's related to the additional calls happening at opcode level, which is also why Doctrine hydrates objects using serialisation 😅

I'll have a look at the patch in the adapter once I'm on a laptop 👍

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants