• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

api-platform / core / 15629538569

13 Jun 2025 07:58AM UTC coverage: 21.874% (-0.002%) from 21.876%
15629538569

Pull #7207

github

web-flow
Merge 3fb5fafc9 into cff61eab8
Pull Request #7207: feat(metadata) customize resource & operations

16 of 92 new or added lines in 9 files covered. (17.39%)

37 existing lines in 2 files now uncovered.

11410 of 52163 relevant lines covered (21.87%)

20.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

15.79
/src/Metadata/Extractor/AbstractClosureExtractor.php
1
<?php
2

3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <dunglas@gmail.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
declare(strict_types=1);
13

14
namespace ApiPlatform\Metadata\Extractor;
15

16
use Psr\Container\ContainerInterface;
17
use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface;
18

19
/**
20
 * Base file extractor.
21
 *
22
 * @author Loïc Frémont <lc.fremont@gmail.com>
23
 */
24
abstract class AbstractClosureExtractor implements ClosureExtractorInterface
25
{
26
    protected ?array $closures = null;
27
    private array $collectedParameters = [];
28

29
    /**
30
     * @param string[] $paths
31
     */
32
    public function __construct(protected array $paths, private readonly ?ContainerInterface $container = null)
33
    {
34
    }
560✔
35

36
    /**
37
     * {@inheritdoc}
38
     */
39
    public function getClosures(): array
40
    {
41
        if (null !== $this->closures) {
94✔
42
            return $this->closures;
44✔
43
        }
44

45
        $this->closures = [];
94✔
46
        foreach ($this->paths as $path) {
94✔
NEW
47
            $closure = $this->getPHPFileClosure($path)();
×
48

NEW
49
            if (!$closure instanceof \Closure || !$this->isClosureSupported($closure)) {
×
NEW
50
                continue;
×
51
            }
52

NEW
53
            $this->closures[] = $closure;
×
54
        }
55

56
        return $this->closures;
94✔
57
    }
58

59
    /**
60
     * Check if the closure is supported
61
     */
62
    abstract protected function isClosureSupported(\Closure $closure): bool;
63

64
    /**
65
     * Recursively replaces placeholders with the service container parameters.
66
     *
67
     * @see https://github.com/symfony/symfony/blob/6fec32c/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php
68
     *
69
     * @copyright (c) Fabien Potencier <fabien@symfony.com>
70
     *
71
     * @param mixed $value The source which might contain "%placeholders%"
72
     *
73
     * @throws \RuntimeException When a container value is not a string or a numeric value
74
     *
75
     * @return mixed The source with the placeholders replaced by the container
76
     *               parameters. Arrays are resolved recursively.
77
     */
78
    protected function resolve(mixed $value): mixed
79
    {
NEW
80
        if (null === $this->container) {
×
NEW
81
            return $value;
×
82
        }
83

NEW
84
        if (\is_array($value)) {
×
NEW
85
            foreach ($value as $key => $val) {
×
NEW
86
                $value[$key] = $this->resolve($val);
×
87
            }
88

NEW
89
            return $value;
×
90
        }
91

NEW
92
        if (!\is_string($value)) {
×
NEW
93
            return $value;
×
94
        }
95

NEW
96
        $escapedValue = preg_replace_callback('/%%|%([^%\s]++)%/', function ($match) use ($value) {
×
NEW
97
            $parameter = $match[1] ?? null;
×
98

99
            // skip %%
NEW
100
            if (!isset($parameter)) {
×
NEW
101
                return '%%';
×
102
            }
103

NEW
104
            if (preg_match('/^env\(\w+\)$/', $parameter)) {
×
NEW
105
                throw new \RuntimeException(\sprintf('Using "%%%s%%" is not allowed in routing configuration.', $parameter));
×
106
            }
107

NEW
108
            if (\array_key_exists($parameter, $this->collectedParameters)) {
×
NEW
109
                return $this->collectedParameters[$parameter];
×
110
            }
111

NEW
112
            if ($this->container instanceof SymfonyContainerInterface) {
×
NEW
113
                $resolved = $this->container->getParameter($parameter);
×
114
            } else {
NEW
115
                $resolved = $this->container->get($parameter);
×
116
            }
117

NEW
118
            if (\is_string($resolved) || is_numeric($resolved)) {
×
NEW
119
                $this->collectedParameters[$parameter] = $resolved;
×
120

NEW
121
                return (string) $resolved;
×
122
            }
123

NEW
124
            throw new \RuntimeException(\sprintf('The container parameter "%s", used in the resource configuration value "%s", must be a string or numeric, but it is of type %s.', $parameter, $value, \gettype($resolved)));
×
NEW
125
        }, $value);
×
126

NEW
127
        return str_replace('%%', '%', $escapedValue);
×
128
    }
129

130
    /**
131
     * Scope isolated include.
132
     *
133
     * Prevents access to $this/self from included files.
134
     */
135
    protected function getPHPFileClosure(string $filePath): \Closure
136
    {
NEW
137
        return \Closure::bind(function () use ($filePath): mixed {
×
NEW
138
            return require $filePath;
×
NEW
139
        }, null, null);
×
140
    }
141
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc