Article
Parsing content in PHP

Introduction
When working with web applications or handling user-generated content, it's often necessary to extract specific pieces of information such as URLs, timecodes, or other structured data from raw text. PHP, being a powerful and flexible scripting language, offers several methods and tools for parsing content efficiently.
In this article, we'll explore practical technique for parsing content in PHP, including the use of regular expressions, to help you reliably extract relevant data from any input.
In a first step, we'll start by looking at a simple solution to apply a single parser for extracting specific data. Then, we'll move on to a more maintainable and scalable approach that supports applying multiple parsers in a clean and extensible way.
Simple Data Extraction
To do this, we can use PHP's preg_replace function. This function allows you to search and replace string based on regular expressions. One powerful feature of this function is its ability to capture specific parts of the match known as capture groups which you can then manipulate or replace as needed.
Let’s try to extract the link from the following text :
Some description before www.test.com an other description afterTo achieve this, we need to write a regular expression that matches typical URL patterns in this text. There are many possible regex patterns depending on your specific needs. For example, a simple pattern to detect basic URLs might look like this:
'/(https?:\/\/)?([a-z0-9]+([\-\.][a-z0-9]+)*\.[a-z]{2,5})(:[0-9]{1,5})?(\/\S*)?/im';Let’s take a closer look at this regular expression:
This regex contains several capture groups, which are portions of the pattern enclosed in brackets () and can be extracted individually. Here's a breakdown of each group:
1. (https?:\/\/)? – This optional group matches the protocol part of the URL, such as http:// or https://
2. ([a-z0-9]+([\-\.][a-z0-9]+)*\.[a-z]{2,5}) – This group captures the full domain name, including subdomains.
3. ([\-\.][a-z0-9]+)* – This is a subgroup within the second group, and it specifically matches only the domain name.
4. (:[0-9]{1,5})? – This optional group captures the port number, if present (e.g., :8080).
5. (\/\S*)? – This final optional group captures everything after the domain, such as the path, query parameters, and fragments.
In a replacement pattern using preg_replace, you can reference these groups with ${1}, ${2}, etc. The ${0} variable refers to the entire matched string.
The final code would look like this:
$string = "Some description before www.test.com an other description after";
$regex = '/(https?:\/\/)?([a-z0-9]+([\-\.][a-z0-9]+)*\.[a-z]{2,5})(:[0-9]{1,5})?(\/\S*)?/im';
$parsedString = preg_replace($regex, '<a href="//${2}" target="_blank" title="$0" rel="external nofollow">$0</a>', $string);For better SEO, we can enhance the link by adding the rel="external nofollow" attribute. This indicates to search engines that the link is external to the main site, and they should not follow or reference it in their indexing.
With this code, our original string would now look like this :
Some description before <a class="text-decoration-none" href="//test.com" target="_blank" title="www.test.com" rel="external nofollow">www.test.com</a> an other description afterModular approach
In the previous example, we applied a single parser to the string. However, in real project, we often need to apply multiple parsers to the same string.
To achieve this in a more organized and reusable way, we should create a class for each parser. These parser classes should implement a Parseable interface, which defines a unique parse() method for parsing the content.
Here’s what it might look like:
Create the Parseable interface
<?php
namespace App\Parsers;
interface Parseable
{
public function parse(string $string) : string;
}Create the LinkParser
<?php
namespace App\Parsers;
class LinkParser implements Parseable
{
public function parse(string $string): string
{
$regex = '/(https?:\/\/)?([a-z0-9]+([\-\.][a-z0-9]+)*\.[a-z]{2,5})(:[0-9]{1,5})?(\/\S*)?/im';
return preg_replace($regex, '<a class="text-decoration-none" href="//${2}" target="_blank" title="$0" rel="external nofollow">$0</a>', $string);
}
}Create the Parser Class
Next, we need to create the Parser class, which will handle the logic for applying multiple parsers:
<?php
namespace App\Parsers;
class Parser
{
/**
* @param string|null $string
* @param array $parsers
* @return string|null
*/
public static function apply(string|null $string, array $parsers): string|null {
$parsedString = htmlspecialchars($string);
foreach ($parsers as $parser) {
if (!is_string($parser) || !class_exists($parser)) {
throw new \InvalidArgumentException('Invalid parser :'. $parser);
}
$parserClass = new $parser();
if (!$parserClass instanceof Parseable) {
throw new \InvalidArgumentException('Parser '. $parser. ' must implement ' .Parseable::class. ' interface');
}
$parsedString = $parserClass->parse($parsedString);
}
return $parsedString;
}
}Security First: The first line of the apply() method converts special characters to HTML entities using htmlspecialchars(). This is crucial for preventing XSS attacks, which can occur when user inputs contain harmful scripts.
Iterating Over Parsers: The method then iterates over all the parsers passed in the $parsers array and applies each parser's parse() method to transform the string.
Error Handling: We ensure that each parser is a valid class that implements the Parseable interface. If any issues are detected, an exception is thrown.
Applying Multiple Parsers:
Now, you can easily apply multiple parsers to a string like this:
$parsedString = Parser::apply($string, [LinkParser::class, TimeCodeParser::class])The result is always the same:
Some description before <a class="text-decoration-none" href="//test.com" target="_blank" title="www.test.com" rel="external nofollow">www.test.com</a> an other description afterDisplaying the Result on the Frontend
To display the parsed string on your frontend, simply output the variable that contains the parsed content.
However, be cautious most modern frameworks automatically escape HTML tags to prevent XSS attacks. To bypass this behavior, you'll need to explicitly tell the framework that you don't want it to escape the string.
This is safe because we applied the htmlspecialchars() method initially to prevent XSS attacks before any parsing occurs.
In Laravel :
You can use the Blade directive to render the string without escaping:
{!! $parsed_string !!}In Symfony :
For Symfony, you can disable autoescaping with the following syntax :
{% autoescape false %}
{{ parsed_string }}
{% endautoescape %}Only use methods that bypass HTML escaping for content that you know is safe, content that has already been sanitized or content from trusted sources. For any raw, user-generated content, always ensure that the framework’s default escaping mechanism is used, which will automatically escape potentially harmful content to prevent security risks like XSS attacks.
Bonus: Extracting Timecodes
Let’s create a parser that extracts timecodes from strings (e.g., 1:01, 02:03:59, etc.) and replaces them with a button element that can trigger an action when clicked.
The code below demonstrates how we can parse timecodes and generate buttons for each match:
<?php
namespace App\Parsers;
class TimeCodeParser implements Parseable
{
public function parse(string $string): string
{
$regex = '/(\d{1,2}:){1,2}\d{2}/m';
return preg_replace_callback($regex, function ($matches) {
$times = array_reverse(explode(':', $matches[0]));
$timeCode = array_reduce(array_keys($times), function($carry, $index) use ($times) {
return $carry + $times[$index] * pow(60, $index);
} , 0);
return "<button onclick='time($timeCode)' data-timecode='$timeCode'>{$matches[0]}</button>";
}, $string);
}
}Example
For an input like :
This is a timecode: 01:02:03The output will be :
This is a timecode: <button onclick='time(3723)' data-timecode='3723'>01:02:03</button>