Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
ServiceExtension
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
2 / 2
4
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 __invoke
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5/**
6 * This file is part of php-fast-forward/container.
7 *
8 * This source file is subject to the license bundled
9 * with this source code in the file LICENSE.
10 *
11 * @copyright Copyright (c) 2025-2026 Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
12 * @license   https://opensource.org/licenses/MIT MIT License
13 *
14 * @see       https://github.com/php-fast-forward/container
15 * @see       https://github.com/php-fast-forward
16 * @see       https://datatracker.ietf.org/doc/html/rfc2119
17 */
18
19namespace FastForward\Container\Extension;
20
21use Closure;
22use ReflectionFunction;
23use ReflectionNamedType;
24use Throwable;
25use Psr\Container\ContainerInterface;
26
27/**
28 * Applies an extension's replacement value while preserving legacy void mutations.
29 *
30 * This adapter SHALL invoke the extension with the container and previous service.
31 * An explicitly declared void return type MUST preserve the previous service after
32 * the extension runs. Every other return value SHALL replace it, including null.
33 *
34 * @internal
35 */
36  class ServiceExtension
37{
38    /**
39     * @var Closure the normalized extension callable
40     */
41    private Closure $extension;
42
43    /**
44     * @var bool whether the extension explicitly declares a void return type
45     */
46    private bool $returnsVoid;
47
48    /**
49     * Constructs an adapter for the supplied service extension.
50     *
51     * The callable SHALL be normalized to a closure and its return type inspected
52     * once, allowing subsequent invocations to preserve legacy void mutations.
53     *
54     * @param callable $extension the service extension to normalize and invoke
55     */
56    public function __construct(callable $extension)
57    {
58        $this->extension = $extension(...);
59        $returnType = (new ReflectionFunction($this->extension))->getReturnType();
60        $this->returnsVoid = $returnType instanceof ReflectionNamedType && 'void' === $returnType->getName();
61    }
62
63    /**
64     * Applies the extension to the previous service value.
65     *
66     * Explicitly void extensions SHALL preserve the previous value. Other extensions
67     * SHALL return their replacement value, including an explicit null result.
68     *
69     * @param ContainerInterface $container the container used to resolve extension dependencies
70     * @param mixed $previous the previous service value, or null when no factory exists
71     *
72     * @return mixed the replacement value or the preserved value for a void extension
73     *
74     * @throws Throwable if the supplied extension fails during invocation
75     */
76    public function __invoke(ContainerInterface $container, mixed $previous): mixed
77    {
78        $result = ($this->extension)($container, $previous);
79
80        return $this->returnsVoid ? $previous : $result;
81    }
82}