title: php preg_replace assign different replacement pattern for each capturing group
body: <p>I'm trying to perform a mysql fulltext search in boolean mode and I need to prepare the search text before I build the mysql query.</p>
  
  <p>In order to achieve it, I though I could use the PHP function <code>preg_replace</code> and replace each capturing group by one specific pattern.</p>
  
  <ol>
  <li>First pattern must find words or sentences between quotes (<code>"hello world"</code>) and add a <code>+</code> before (<code>+"hello world"</code>).</li>
  <li>Second pattern must find the rest of words (without quotes) and add a <code>+</code> before and a <code>*</code> after (<code>+how*</code>).</li>
  </ol>
  
  <p><strong>regex pattern</strong></p>
  
  <pre><code>["']+([^"']+)["']+|([^\s"']+)
  </code></pre>
  
  <p><strong>substitution pattern</strong></p>
  
  <pre><code>+"\1" +\2*
  </code></pre>
  
  <hr>
  
  <p><strong>EXAMPLE</strong></p>
  
  <p>For the following input:</p>
  
  <pre><code>"hello world" how are you?
  </code></pre>
  
  <p>It should return:</p>
  
  <pre><code>+"hello world" +how* +are* +you?*
  </code></pre>
  
  <p>But instead, it returns <strong>something 'wrong'</strong>:</p>
  
  <pre><code>+"hello world" +* +"" +how* +"" +are* +"" +you?*
  </code></pre>
  
  <p>I'm aware that the replacement pattern <code>+"\1" +\2*</code> will never work since I'm not telling anywhere that <code>+"..."</code> should only apply to the first capturing group and <code>+...*</code> to the second one.</p>
  
  <p><a href="https://regex101.com/r/iBzKHu/1" rel="nofollow noreferrer">Test online regex</a>.</p>
  
  <p><strong>PHP code</strong></p>
  
  <pre><code>$query = preg_replace('~["\']+([^"\']+)["\']+|([^\s"\']+)~', '+"\1" +\2*', $query);
  </code></pre>
  
  <p>Is there a way to achieve this in PHP? Thank you in advance.</p>
  
  <hr>
  
  <p><strong>EDIT / SOLUTION</strong></p>
  
  <p>Thanks to <a href="https://stackoverflow.com/users/1020526/revo">@revo</a> suggestion to use the PHP function <a href="http://php.net/preg_replace_callback" rel="nofollow noreferrer"><code>preg_replace_callback</code></a>, I managed to assign a replace pattern to each search pattern with the extended function <a href="http://php.net/preg_replace_callback_array" rel="nofollow noreferrer"><code>preg_replace_callback_array</code></a>. Please note that this function needs PHP >= 7.</p>
  
  <p>Here I post the final version of the function I use to perform a <code>FULLTEXT</code> search via <code>MATCH (...) AGAINST (...) IN BOOLEAN MODE</code>. The function is declared within the <code>class dbReader</code> in a Wordpress Plugin. Maybe it can be useful for someone.</p>
  
  <pre><code>// Return maximum 100 ids of products matching $query in
  // name or description searching for each word using MATCH AGAINST in BOOLEAN MODE
  public function search_products($query) {
  
      function replace_callback($m, $f) {
          return sprintf($f, isset($m[1]) ? $m[1] : $m[0]);
      }
  
      // Replace simple quotes by double quotes in strings between quotes:
      // iPhone '8 GB' =&gt; iPhone "8 GB"
      // Apple's iPhone 8 '32 GB' =&gt; Apple's iPhone 8 "32 GB"
      // This is necessary later when the matches are devided in two groups:
      //      1. Strings not between double quotes
      //      2. Strings between double quotes
      $query = preg_replace("~(\s*)'+([^']+)'+(\s*)~", '$1"$2"$3', $query);
  
      // Do some magic to take numbers with their units as one word
      // iPhone 8 64 GB =&gt; iPhone 8 "64 GB"
      $pattern = array(
          '(\b[.,0-9]+)\s*(gb\b)',
          '(\b[.,0-9]+)\s*(mb\b)',
          '(\b[.,0-9]+)\s*(mm\b)',
          '(\b[.,0-9]+)\s*(mhz\b)',
          '(\b[.,0-9]+)\s*(ghz\b)'
      );
      array_walk($pattern, function(&amp;$value) {
          // Surround with double quotes only if the user isn't doing manual grouping
          $value = '~'.$value.'(?=(?:[^"]*"[^"]*")*[^"]*\Z)~i';
      });
      $query = preg_replace($pattern, '"$1 $2"', $query);
  
      // Prepare query string for a "match against" in "boolean mode"
      $patterns = array(
          // 1. All strings not sorrounded by double quotes
          '~([^\s"]+)(?=(?:[^"]*"[^"]*")*[^"]*\Z)~'   =&gt; function($m){
              return replace_callback($m, '+%s*');
          },
  
          // 2. All strings between double quotes
          '~"+([^"]+)"+~'                             =&gt; function($m){
              return replace_callback($m, '+"%s"');
          }
      );
  
      // Replace every single word by a boolean expression: +some* +word*
      // Respect quoted strings: +"iPhone 8"
      // preg_replace_callback_array needs PHP Version &gt;= 7
      $query = preg_replace_callback_array($patterns, $query);
  
      $fulltext_fields = array(
          'title'         =&gt; array(
              'importance'    =&gt; 1.5,
              'table'         =&gt; 'p',
              'fields'        =&gt; array(
                  'field1',
                  'field2',
                  'field3',
                  'field4'
              )
          ),
          'description'   =&gt; array(
              'importance'    =&gt; 1,
              'table'         =&gt; 'p',
              'fields'        =&gt; array(
                  'field5',
                  'field6',
                  'field7',
                  'field8'
              )
          )
      );
      $select_match = $match_full = $priority_order = "";
  
      $args = array();
      foreach ($fulltext_fields as $index =&gt; $obj) {
          $match          = $obj['table'].".".implode(", ".$obj['table'].".", $obj['fields']);
          $select_match  .= ", MATCH ($match) AGAINST (%s IN BOOLEAN MODE) AS {$index}_score";
          $match_full    .= ($match_full!=""?", ":"").$match;
          $priority_order.= ($priority_order==""?"ORDER BY ":" + ")."({$index}_score * {$obj['importance']})";
          array_push($args, $query);
      }
      $priority_order .= $priority_order!=""?" DESC":"";
  
      // User input $query is passed as %s parameter to db-&gt;prepare() in order to avoid SQL injection
      array_push($args, $query, $this-&gt;model_name, $this-&gt;view_name);
  
      return $this-&gt;db-&gt;get_col(
          $this-&gt;db-&gt;prepare(
              "SELECT p.__pk $select_match
              FROM ankauf_... AND
                      MATCH ($match_full) AGAINST (%s IN BOOLEAN MODE)
                  INNER JOIN ...
              WHERE
                  m.bezeichnung=%s AND
                  a.bezeichnung=%s
                  $priority_order
              LIMIT 100
              ;",
              $args
          )
      );
  }
  </code></pre>