3

I already have a routing method that matches this pattern:

/hello/:name

that set name to be a dynamic path, I want to know how to make it:

/hello/{name}    

with the same regex. How to add optional trailing slash to it, like this?

/hello/:name(/)

or

/hello/{name}(/)

This is the regex I use for /hello/:name

@^/hello/([a-zA-Z0-9\-\_]+)$@D

The regex is auto generated from PHP class

private function getRegex($pattern){
        $patternAsRegex = "@^" . preg_replace('/\\\:[a-zA-Z0-9\_\-]+/', '([a-zA-Z0-9\-\_]+)', preg_quote($pattern)) . "$@D";
        return $patternAsRegex;
    }

If the route is /hello/:name(/) I want it to make the match with optional thing else continue normal

2 Answers 2

10

This will create a regular expression for the $pattern route with both :name and {name} parameters, as well as the optional slash. As a bonus, it will also add a ?<name> to make the parameter easier to handle down the line.

For example, a route pattern of /hello/:name(/) will get the regular expression @^/hello/(?<name>[a-zA-Z0-9\_\-]+)/?$@D. When matched with a URL, like preg_match( <regex above>, '/hello/sarah', $matches) that would give you $matches['name'] == 'sarah'.

There are some tests to be found below the actual function.

function getRegex($pattern){
    if (preg_match('/[^-:\/_{}()a-zA-Z\d]/', $pattern))
        return false; // Invalid pattern

    // Turn "(/)" into "/?"
    $pattern = preg_replace('#\(/\)#', '/?', $pattern);

    // Create capture group for ":parameter"
    $allowedParamChars = '[a-zA-Z0-9\_\-]+';
    $pattern = preg_replace(
        '/:(' . $allowedParamChars . ')/',   # Replace ":parameter"
        '(?<$1>' . $allowedParamChars . ')', # with "(?<parameter>[a-zA-Z0-9\_\-]+)"
        $pattern
    );

    // Create capture group for '{parameter}'
    $pattern = preg_replace(
        '/{('. $allowedParamChars .')}/',    # Replace "{parameter}"
        '(?<$1>' . $allowedParamChars . ')', # with "(?<parameter>[a-zA-Z0-9\_\-]+)"
        $pattern
    );

    // Add start and end matching
    $patternAsRegex = "@^" . $pattern . "$@D";

    return $patternAsRegex;
}

// Test it
$testCases = [
    [
        'route'           => '/hello/:name',
        'url'             => '/hello/sarah',
        'expectedParam'   => ['name' => 'sarah'],
    ],
    [
        'route'           => '/bye/:name(/)',
        'url'             => '/bye/stella/',
        'expectedParam'   => ['name' => 'stella'],
    ],
    [
        'route'           => '/find/{what}(/)',
        'url'             => '/find/cat',
        'expectedParam'   => ['what' => 'cat'],
    ],
    [
        'route'           => '/pay/:when',
        'url'             => '/pay/later',
        'expectedParam'   => ['when' => 'later'],
    ],
];

printf('%-5s %-16s %-39s %-14s %s' . PHP_EOL, 'RES', 'ROUTE', 'PATTERN', 'URL', 'PARAMS');
echo str_repeat('-', 91), PHP_EOL;

foreach ($testCases as $test) {
    // Make regexp from route
    $patternAsRegex = getRegex($test['route']);

    if ($ok = !!$patternAsRegex) {
        // We've got a regex, let's parse a URL
        if ($ok = preg_match($patternAsRegex, $test['url'], $matches)) {
            // Get elements with string keys from matches
            $params = array_intersect_key(
                $matches,
                array_flip(array_filter(array_keys($matches), 'is_string'))
            );

            // Did we get the expected parameter?
            $ok = $params == $test['expectedParam'];

            // Turn parameter array into string
            list ($key, $value) = each($params);
            $params = "$key = $value";
        }
    }

    // Show result of regex generation
    printf('%-5s %-16s %-39s %-14s %s' . PHP_EOL,
        $ok ? 'PASS' : 'FAIL',
        $test['route'], $patternAsRegex,
        $test['url'],   $params
    );
}

Output:

RES   ROUTE            PATTERN                                 URL            PARAMS
-------------------------------------------------------------------------------------------
PASS  /hello/:name     @^/hello/(?<name>[a-zA-Z0-9\_\-]+)$@D   /hello/sarah   name = sarah
PASS  /bye/:name(/)    @^/bye/(?<name>[a-zA-Z0-9\_\-]+)/?$@D   /bye/stella/   name = stella
PASS  /find/{what}(/)  @^/find/(?<what>[a-zA-Z0-9\_\-]+)/?$@D  /find/cat      what = cat
PASS  /pay/:when       @^/pay/(?<when>[a-zA-Z0-9\_\-]+)$@D     /pay/later     when = later
Sign up to request clarification or add additional context in comments.

2 Comments

but if a match have been found it will need to be an array like [0 => 'sarah'] so i could be able to invoke call_user_func_array() with it
never mind that i can make by my own but i want all the string which inside ( ) get defined as optional in Regex not just /
1

Simply replace your regex with this for optional / :

@^/hello/([a-zA-Z0-9-_]+)/?$@

4 Comments

@MohamedBelal: are you saying that you want with-slash and without-slash to be different routes? If they should be the same route, then Tanmay's answer seems to be correct.
(Incidentally, don't write your own router logic - really don't. There are hundreds of different routers on GitHub, some of them very good. Use an existing one, either standalone or as part of an existing framework).
i am developing my own framework i don't want to use someone url router i just don't know RegExp well :P all i want to is make the user able to do the dynamic route with 2 ways /hello/:name or /hello/{name} and if he want an optional trailing slash then it get passed like this /hello/:name(/) () defines optional in the regex i want the part inside () is defined as optional and also make the regex able to do it for both : and { }

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.