chris
chris

Reputation: 36957

Replace placeholders in text using data fetched from the database

I am trying to match parts of a dynamically generated string and make a replacement based on the extracted information. My problem is that I can't fathom how to craft the regex.

What I have is a few different placeholder strings like the following.:

  1. [@img:1234567890]
  2. [@user:1234567890]
  3. [@file:file_name-with.ext]

Strings like this are passed through with the intent of being replaced with links, and/or more readable names. But again, I can't come up with a regex for any given one of them.

I am looking for the format: [@word:] of which I will strip the [, ], @, and word from the string so I can then turn around an query my DB accordingly for whatever it is and work with it accordingly. Just the regex bit is holding me back.

Upvotes: 0

Views: 116

Answers (4)

mickmackusa
mickmackusa

Reputation: 48071

Making dynamic string replacements involving function calls is a job for preg_replace_callback(). Depending on the placeholder encountered, execute the necessary query or filesystem check and return the desired HTML element.

The regex pattern should be narrowly crafted to only acknowledge expected/programmed actions -- using \w+ is too vague when you know exactly which words are valid. When processing matches, use the match() construct to determine which replacement process should be executed.

Code: (PHPize Demo)

$text = <<<TEXT
Here we have some test text to show [@img:1234567890]
belonging to [@user:1234567890] and a reference
to [@file:file_name-with.ext].
TEXT;

echo preg_replace_callback(
    '/\[@(?|(user|img):(\d+)|(file):([\w.-]+))\]/',
    fn($m) => match($m[1]) {
        'user' => (
            fn($row) => $row
                ? '<a href="/user/' . $m[2] . '">' . htmlentities($row['name']) . '</a>'
                : $m[0]
        )(
            $mysqli->execute_query('SELECT name FROM users WHERE id = ?', [$m[2]])->fetch_assoc() ?: []
        ),
        'img' => (
            fn($row) => $row
                ? '<img src="/images/' . $m[2] . '" alt="' . htmlentities($row['name']) . '">'
                : $m[0]
        )(
            $mysqli->execute_query('SELECT name FROM images WHERE id = ?', [$m[2]])->fetch_assoc() ?: []
        ),
        'file' => file_exists('your/system/path/' . $m[2])
            ? '<a href="/your/system/path/' . $m[2] . '">' . $m[2] . '</a>'
            : $m[0],
    },
    $text
);

Output:

Here we have some test text to show <img src="/images/1234567890" alt="some_image.png">
belonging to <a href="/user/1234567890">Chad</a> and a reference
to [@file:file_name-with.ext].

Upvotes: 0

inhan
inhan

Reputation: 7470

Here's what I would do.

<pre>
<?php

    $subj = 'An image:[@img:1234567890], a user:[@user:1234567890] and a file:[@file:file_name-with.ext]';
    preg_match_all('~(?<match>\[@(?<type>[^:]+):(?<value>[^\]]+)\])~',$subj,$matches,PREG_SET_ORDER);
    foreach ($matches as &$arr) unset($arr[0],$arr[1],$arr[2],$arr[3]);
    print_r($matches);

?>
</pre>

This will output

Array
(
    [0] => Array
        (
            [match] => [@img:1234567890]
            [type] => img
            [value] => 1234567890
        )

    [1] => Array
        (
            [match] => [@user:1234567890]
            [type] => user
            [value] => 1234567890
        )

    [2] => Array
        (
            [match] => [@file:file_name-with.ext]
            [type] => file
            [value] => file_name-with.ext
        )

)

And here's a pseudo version of how I would use the preg_replace_callback() function:

function replace_shortcut($matches) {
    global $users;
    switch (strtolower($matches['type'])) {
        case 'img'  : return '<img src="images/img_'.$matches['value'].'jpg" />';
        case 'file' : return '<a href="files/'.$matches['value'].'" target="_blank">'.$matches['value'].'</a>';
        // add id of each user in array
        case 'user' : $users[] = (int) $matches['value']; return '%s';
        default : return $matches['match'];
    }
}

$users = array();
$replaceArr = array();

$subj = 'An image:[@img:1234567890], a user:[@user:1234567890] and a file:[@file:file_name-with.ext]';
// escape percentage signs to avoid complications in the vsprintf function call later
$subj = strtr($subj,array('%'=>'%%'));
$subj = preg_replace_callback('~(?<match>\[@(?<type>[^:]+):(?<value>[^\]]+)\])~',replace_shortcut,$subj);

if (!empty($users)) {

    // connect to DB and check users
    $query = "  SELECT `id`,`nick`,`date_deleted` IS NOT NULL AS 'deleted'
                FROM `users` WHERE `id` IN ('".implode("','",$users)."')";
    // query
    // ...
    // and catch results
    while ($row = $con->fetch_array()) {
        // position of this id in users array:
        $idx = array_search($row['id'],$users);
        $nick = htmlspecialchars($row['nick']);
        $replaceArr[$idx] = $row['deleted'] ?
            "<span class=\"user_deleted\">{$nick}</span>" :
            "<a href=\"users/{$row['id']}\">{$nick}</a>";
        // delete this key so that we can check id's not found later...
        unset($users[$idx]);
    }
    // in here:
    foreach ($users as $key => $value) {
        $replaceArr[$key] = '<span class="user_unknown">User'.$value.'</span>';
    }
    // replace each user reference marked with %s in $subj
    $subj = vsprintf($subj,$replaceArr);

} else {

    // remove extra percentage signs we added for vsprintf function
    $subj = preg_replace('~%{2}~','%',$subj);

}
unset($query,$row,$nick,$idx,$key,$value,$users,$replaceArr);

echo $subj;

Upvotes: 1

Brombomb
Brombomb

Reputation: 7086

Not sure what you mean by generators. I always use online matchers to see that my test cases work. @Virendra almost had it except forgot to escape the [] charaters.

/\[@(\w+):(.*)\]/

You need to start and end with a regex delimeter, in this case the '/' character.

Then we escape the '[]' which is use by regex to match ranges of characters hence the '['.

Next we match a literal '@' symbol.

Now we want to save this next match so we can use it later so we surround it with ().

\w matches a word. Basically any characters that aren't spaces, punctuation, or line characters.

Again match a literal :.

Maybe useful to have the second part in a match group as well so (.*) will match any character any number of times, and save it for you.

Then we escape the closing ] as we did earlier.

Since it sounds like you want to use the matches later in a query we can use preg_match to save the matches to an array.

$pattern = '/\[@(\w+):(.*)\]/';
$subject = '[@user:1234567890]';
preg_match($pattern, $subject, $matches);
print_r($matches);

Would output

array(
    [0] => '[@user:1234567890]', // Full match
    [1] => 'user', // First match
    [2] => '1234567890' // Second match
)

An especially helpful tool I've found is txt2re

Upvotes: 1

nneonneo
nneonneo

Reputation: 179717

You can try something like this:

/\[@(\w+):([^]]*)\]/

\[ escapes the [ character (otherwise interpreted as a character set); \w means any "word" character, and [^]]* means any non-] character (to avoid matching past the end of the tag, as .* might). The parens group the various matched parts so that you can use $1 and $2 in preg_replace to generate the replacement text:

echo preg_replace('/\[@(\w+):([^]]*)\]/', '$1 $2', '[@link:abcdef]');

prints link abcdef

Upvotes: 0

Related Questions