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

willy68 / pg-router / 16205018743

10 Jul 2025 08:08PM UTC coverage: 94.928% (-0.05%) from 94.978%
16205018743

push

github

willy68
First commit

3 of 3 new or added lines in 1 file covered. (100.0%)

17 existing lines in 4 files now uncovered.

917 of 966 relevant lines covered (94.93%)

8.68 hits per line

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

97.01
/src/Parser/NamedParser.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Pg\Router\Parser;
6

7
use Pg\Router\Exception\DuplicateAttributeException;
8
use Pg\Router\Regex\Regex;
9

10
class NamedParser implements ParserInterface
11
{
12
    protected string $regex;
13
    /** @var array  Default tokens ["tokenName" => "regex"]*/
14
    protected array $tokens = [];
15

16
    public function parse(string $path, array $tokens = []): string|array
23✔
17
    {
18
        if (empty($path)) {
23✔
UNCOV
19
            return '/';
×
20
        }
21

22
        if ($path === '/') {
23✔
UNCOV
23
            return '/';
×
24
        }
25

26
        // Quick check for simple static routes
27
        if (!$this->containsVariables($path) && !$this->containsOptionalSegments($path)) {
23✔
28
            return $path;
3✔
29
        }
30

31
        $this->regex = $path;
20✔
32
        $this->tokens = $tokens;
20✔
33

34
        $this->parseOptionalParts();
20✔
35
        $this->parseVariableParts();
20✔
36

37
        return $this->regex;
19✔
38
    }
39

40
    /**
41
     * Check if a path contains variable patterns
42
     *
43
     * @param string $path
44
     * @return bool
45
     */
46
    protected function containsVariables(string $path): bool
23✔
47
    {
48
        return str_contains($path, '{');
23✔
49
    }
50

51
    /**
52
     * Check if a path contains optional segments
53
     *
54
     * @param string $path
55
     * @return bool
56
     */
57
    protected function containsOptionalSegments(string $path): bool
3✔
58
    {
59
        return str_contains($path, '[!');
3✔
60
    }
61

62
    /**
63
     * Split in different pattern route with optional parts.
64
     *
65
     * @return void
66
     */
67
    protected function parseOptionalParts(): void
20✔
68
    {
69
        $optionalParts = $this->extractOptionalParts();
20✔
70
        if ($optionalParts) {
20✔
71
            $partsWithoutBrackets = rtrim($optionalParts, ']');
13✔
72
            $optionalParts = '[!' . $optionalParts;
13✔
73
            $parts = explode(';', $partsWithoutBrackets);
13✔
74
            $repl = $this->getRegexOptionalAttributesReplacement($parts);
13✔
75
            $this->regex = str_replace($optionalParts, $repl, $this->regex);
13✔
76
        }
77
    }
78

79
    protected function extractOptionalParts(): ?string
20✔
80
    {
81
        $parts = preg_split('~' . Regex::OPT_REGEX . '~x', $this->regex);
20✔
82
        return $parts[1] ?? null;
20✔
83
    }
84

85
    /**
86
     *
87
     * Gets the replacement for optional attributes in the regex.
88
     *
89
     * @param array $parts The optional attributes.
90
     *
91
     * @return string
92
     *
93
     */
94
    protected function getRegexOptionalAttributesReplacement(array $parts): string
13✔
95
    {
96
        $head = $this->getRegexOptionalAttributesReplacementHead($parts);
13✔
97
        $tail = $head !== '' ? ')?' : '';
13✔
98
        foreach ($parts as $name) {
13✔
99
            $head .= '(?:' . trim($name);
13✔
100
            $tail .= ')?';
13✔
101
        }
102

103
        return $head . $tail;
13✔
104
    }
105

106
    /**
107
     *
108
     * Gets the leading portion of the optional attributes replacement.
109
     *
110
     * @param array $parts The optional attributes.
111
     *
112
     * @return string
113
     *
114
     */
115
    protected function getRegexOptionalAttributesReplacementHead(array &$parts): string
13✔
116
    {
117
        // if the optional set is the first part of the path, make sure there
118
        // is a leading slash in the replacement before the optional attribute.
119
        $head = '';
13✔
120
        if (str_starts_with($this->regex, '[!')) {
13✔
121
            $name = array_shift($parts);
2✔
122
            $name = ltrim($name, '/');
2✔
123
            $head = '/(?:' . trim($name);
2✔
124
        }
125
        return $head;
13✔
126
    }
127

128
    /**
129
     * Generate the regex for all routes needed by the path.
130
     *
131
     * @return void
132
     */
133
    protected function parseVariableParts(): void
20✔
134
    {
135
        $vars = [];
20✔
136
        $searchPatterns = [];
20✔
137
        $replacements = [];
20✔
138

139
        preg_match_all('~' . Regex::REGEX . '\s*~x', $this->regex, $matches, PREG_SET_ORDER);
20✔
140
        foreach ($matches as $match) {
20✔
141
            [$full, $name, $token] = array_pad($match, 3, null);
20✔
142

143
            if (isset($vars[$name])) {
20✔
144
                throw new DuplicateAttributeException(
1✔
145
                    sprintf(
1✔
146
                        'Cannot use the same attribute twice [%s]',
1✔
147
                        $name
1✔
148
                    )
1✔
149
                );
1✔
150
            }
151

152
            $subPattern = $this->getSubpattern($name, $token);
20✔
153
            $searchPatterns[] = $full;
20✔
154
            $replacements[] = $subPattern;
20✔
155
            $vars[$name] = $name;
20✔
156
        }
157

158
        // Single str_replace call with arrays for all replacements
159
        if (!empty($searchPatterns)) {
19✔
160
            $this->regex = str_replace($searchPatterns, $replacements, $this->regex);
19✔
161
        }
162
    }
163

164
    /**
165
     * Return the subpattern for a token with the attribute name.
166
     *
167
     * @param string $name
168
     * @param string|null $token
169
     * @return string
170
     */
171
    protected function getSubpattern(string $name, ?string $token = null): string
20✔
172
    {
173
        // is there a custom subpattern for the name?
174
        if (isset($this->tokens[$name]) && is_string($this->tokens[$name])) {
20✔
175
            // if $token is null use route token
176
            $token = $token ?: $this->tokens[$name];
1✔
177
        }
178

179
        // is there a custom subpattern for the name?
180
        if ($token) {
20✔
181
            return '(?P<' . $name . '>' . trim($token) . ')';
17✔
182
        }
183

184
        // use a default subpattern
185
        return '(?P<' . $name . '>[^/]+)';
3✔
186
    }
187
}
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

© 2026 Coveralls, Inc