6
\$\begingroup\$

I'm coding an algorithm to remove a parameter (let's call it foo) from URL strings.

Of course, after the foo parameter removal, the query string should remain valid (with a leading ? and remaining parameters separated by &).

I'd also like to remove the leading ? if foo was the only parameter.

Details:

  • Domain and pathname should be preserved.
  • The URLs may not contain a query string. Expected output is the same as input.
  • The URLs may contain a query string which does not contain the foo parameter. Expected output is the same as input.
  • The URLs are already properly URL-encoded.
  • Fragments (hashes) don't necessarily need to be kept, but it would be a nice little extra.

Input examples:

http://example.com/?foo=42
http://example.com/?foo=42&bar=43
http://example.com/?bar=43&foo=42
http://example.com/?bar=43&foo=42&baz=44
http://domain.com.uk/pathname?foo=42&bar=bar%20value
http://yahoo.com/mail
http://nofoo.com/?bar=43

Expected output:

http://example.com/
http://example.com/?bar=43
http://example.com/?bar=43
http://example.com/?bar=43&baz=44
http://domain.com.uk/pathname?bar=bar%20value
http://yahoo.com/mail
http://nofoo.com/?bar=43

My initial attempt:

preg_replace_callback('/([?&])foo=[^&]+(&|$)/', function($matches) {
    return $matches[2] ? $matches[1] : '';
}, $url);

The regex itself is rather simple. The callback logic is as follows:

  • If foo is not the last parameter (2nd capturing group is not end of string), then the whole match is replaced by the first capturing group (? or &). This handles:
    • ?foo=valuefoo&bar -> ?bar
    • &foo=valuefoo&bar -> &bar
  • If foo is the last parameter then the whole match is replaced by an empty string. This handles:
    • ?bar=valuebar&foo=valuefoo -> ?bar=valuebar
    • ?foo=valuefoo -> (empty string)

This logic seemed rather complicated, hence I rewrote it into a single regex:

preg_replace('/[?&]foo=[^&]+$|([?&])foo=[^&]+&/', '$1', $url);

Now both logic branches are separated by the regex OR | and the 1st capturing group only occurs in the "foo is not the last parameter" branch.

I've looked at regex conditionals but those would just overcomplicate an otherwise simple regex.

This seemed like a simple task at first glance, but now I'm wondering whether I should even use Regex for this.

Right now I'm thinking about substr'ing from the first ?, explodeing the query string at &, array_filter based on the parameters names, implode and concatenate it to the URL again, but this looks overly verbose.

Is there a better approach (mainly in terms of readability and maintainability) to remove a query string parameter?


New approach using native functions and borrowing some code from PHP docs' comments:

//http://www.php.net/manual/en/function.parse-url.php#106731
function unparse_url($parsed_url) {
    $scheme   = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
    $host     = isset($parsed_url['host']) ? $parsed_url['host'] : '';
    $port     = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
    $user     = isset($parsed_url['user']) ? $parsed_url['user'] : '';
    $pass     = isset($parsed_url['pass']) ? ':' . $parsed_url['pass']  : '';
    $pass     = ($user || $pass) ? "$pass@" : '';
    $path     = isset($parsed_url['path']) ? $parsed_url['path'] : '';
    $query    = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
    $fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
    return "$scheme$user$pass$host$port$path$query$fragment";
}

function removeQueryParam($url, $param_to_remove) {
    $parsed = parse_url($url);
    if ($parsed && isset($parsed['query'])) {
        $parsed['query'] = implode('&', array_filter(explode('&', $parsed['query']), function($param) use ($param_to_remove) {
            return explode('=', $param)[0] !== $param_to_remove;
        }));
        if ($parsed['query'] === '') unset($parsed['query']);
        return unparse_url($parsed);
    } else {
        return $url;
    }
}

It works fine even with hashes/fragments now. Is there anything else to be improved? As far as I can see, there's no native method to parse a query string into an array, hence the explode, array_filter and implode method is the more maintainable I could get.

\$\endgroup\$
  • \$\begingroup\$ dear lord, I'm sorry to say this but: Gloves... yes gloves exist, that's why the market for electrically heated bike handles won't be massive \$\endgroup\$ – Elias Van Ootegem Dec 26 '13 at 9:59
  • \$\begingroup\$ @EliasVanOotegem heh I haven't read that one before, nice one. So on-topic, I should've kept my first revision...? Mhm, guess I'm a bit of a complicator for making this question. =] \$\endgroup\$ – Fabrício Matté Dec 26 '13 at 10:49
  • \$\begingroup\$ @EliasVanOotegem you can submit "gloves" as an answer if you'd like. \$\endgroup\$ – Fabrício Matté Dec 26 '13 at 11:17
  • \$\begingroup\$ No, that's just a side-note. It doesn't reflect on the overall quality of the code or possible redundancies (haven't looked at the code, just glanced at it, and thought gloves). If you'd look at my previous answers here, you'd know that I'm incapable of posting short answers. I have to go through the code, or at least a sizable piece of it, line by line... \$\endgroup\$ – Elias Van Ootegem Dec 26 '13 at 11:24
  • \$\begingroup\$ @EliasVanOotegem anyway, thank you, I often over-complicate things and need someone to make me realize that. \$\endgroup\$ – Fabrício Matté Dec 26 '13 at 11:28
2
\$\begingroup\$

URL's are a nightmare to process. Even though, in your case, it appears that you can use regex to isolate your specific problem, I would still recommend that you avoid regex for URL manipulation.

PHP has native URL parsing methods (much better tested and controlled than your code), and you should use them where possible. Have a look at the parse_url documentation. You can use this, as well as some of the other example code on that page to parse, and rebuild the URL with the changed parameters.

Using 'native' functions where possible is often the best solution. Even though the native function does not do the entirety of what you want done, it goes a long way to simplifying the process, and the regexes become more manageable. In this case, I think it is the right solution too.

\$\endgroup\$
  • \$\begingroup\$ I've used parse_url in the past, I just went for the quick and dirty Regex method because it seemed like a simple task but now I realize it is not that simple. Thanks for the pointers, I'll analyze this when I get home. \$\endgroup\$ – Fabrício Matté Dec 25 '13 at 16:17
5
\$\begingroup\$

I would also avoid using regexes or any kind of manual string parsing, you can do it all with parse_str() and http_build_query(), thusly:

function removeAndReturn(&$url, $toRemove)
{
    $parsed = [];
    parse_str(substr($url, strpos($url, '?') + 1), $parsed);
    $removed = $parsed[$toRemove];
    unset($parsed[$toRemove]);
    $url = 'http://example.com/';
    if(!empty($parsed))
    {
        $url .= '?' . http_build_query($parsed);
    }
    return $removed;
}

Then with a simple script to test it:

$input = ['http://example.com/?foo=42',
         'http://example.com/?foo=42&bar=43',
         'http://example.com/?bar=43&foo=42',
         'http://example.com/?bar=43&foo=42&baz=44'];

$expected = ['http://example.com/',
           'http://example.com/?bar=43',
           'http://example.com/?bar=43',
           'http://example.com/?bar=43&baz=44'];


$count = count($input);
for($i = 0; $i < $count; $i++)
{
    $foo = removeAndReturn($input[$i], 'foo');
    echo 'Foo: ' . $foo . '<br />' .
          'URL: ' . $input[$i] . '<br />';
    if($input[$i] === $expected[$i])
        echo 'Match<br />';
}

You get:

Foo: 42
URL: http://example.com/
Match
Foo: 42
URL: http://example.com/?bar=43
Match
Foo: 42
URL: http://example.com/?bar=43
Match
Foo: 42
URL: http://example.com/?bar=43&baz=44
Match
\$\endgroup\$
  • \$\begingroup\$ Thanks. It'd be better to save the part prior to ?, this way it would handle other domains and pathnames. I'd also add a check to see if the URL contains a ? - it looks like this will fail when there's no query string. There is one more thing I didn't mention in the question, my URLs are already properly encoded so http_build_query would double-encode them: codepad.viper-7.com/3vYTm4 \$\endgroup\$ – Fabrício Matté Dec 26 '13 at 10:36
2
\$\begingroup\$

For this task I suggest to use as much as possible standard PHP functions, since in complex cases they've been proved to be more reliable than a simple regexp or a 'manual' explode/implode solution. Unfortunately, there seems not to be a standard PHP 'reverse' function for parse_url(), except for those posted by users on parse_url official PHP manual page http://php.net/manual/en/function.parse-url.php.

So, my solution is a blend of 3 standard PHP functions (parse_url + parse_str + http_build_query) + 1 user contributed function found on PHP manual page (unparse_url):

function removeParam($key, $sourceURL) { // Removes parameter '$key' from '$sourceURL' query string (if present)
    $url = parse_url($sourceURL);
    if (!isset($url['query'])) return $sourceURL;
    parse_str($url['query'], $query_data);
    if (!isset($query_data[$key])) return $sourceURL;
    unset($query_data[$key]);
    $url['query'] = http_build_query($query_data);
    return unparse_url($url);
}

function unparse_url($parsed_url) { 
  $scheme   = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : ''; 
  $host     = isset($parsed_url['host']) ? $parsed_url['host'] : ''; 
  $port     = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''; 
  $user     = isset($parsed_url['user']) ? $parsed_url['user'] : ''; 
  $pass     = isset($parsed_url['pass']) ? ':' . $parsed_url['pass']  : ''; 
  $pass     = ($user || $pass) ? "$pass@" : ''; 
  $path     = isset($parsed_url['path']) ? $parsed_url['path'] : ''; 
  $query    = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : ''; 
  $fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : ''; 
  return "$scheme$user$pass$host$port$path$query$fragment"; 
}
\$\endgroup\$
-1
\$\begingroup\$

This can easily be done with in .htaccess.

Just add this to your .htaccess file:

RewriteEngine on
RewriteCond %{QUERY_STRING} ^(.*)&?foo=42?(.*)$ [NC]
RewriteRule ^/?(.*)$ /$1?%1%2 [R=301,L]
\$\endgroup\$
  • 1
    \$\begingroup\$ Welcome to Code Review! You have presented an alternative solution, but haven't reviewed the code. Please edit to show what aspects of the question code prompted you to write this version, and in what ways it's an improvement over the original. It may be worth (re-)reading How to Answer. \$\endgroup\$ – Toby Speight Oct 5 '18 at 7:55
  • \$\begingroup\$ Please delete my contribution if you think people are better off without it. \$\endgroup\$ – WendiT Oct 6 '18 at 11:09

protected by Jamal Oct 5 '18 at 4:00

Thank you for your interest in this question. Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).

Would you like to answer one of these unanswered questions instead?

Not the answer you're looking for? Browse other questions tagged or ask your own question.