In questo tutorial vedremo come effettuare il parsing e la conversione in JSON di un feed RSS di Medium con PHP.
PHP dispone della classe DOMDocument
per effettuare il parsing di una stringa XML usando l'implementazione delle specifiche DOM.
Quando si utilizza questa classe nel contesto di un feed RSS bisogna tenere presente che alcuni elementi potrebbero contenere sezioni di tipo CDATA
con all'interno codice HTML arbitrario. In questo caso per utilizzare nuovamente la classe per il parsing di una stringa HTML occorre sopprimere gli eventuali Warning generati dalla libreria libXML.
La classe che andremo a definire non avrà il solo URL del feed come parametro del costruttore, ma anche due riferimenti a funzioni esterne con cui l'utenza finale della classe potrà personalizzare il formato delle date e degli URL dei post.
Definiamo la struttura base della nostra classe come segue:
class MediumRSSReader {
private $url;
private $linkFormatter;
private $dateFormatter;
public function __construct($url, $linkFormatter, $dateFormatter) {
$this->url = $url;
$this->linkFormatter = $linkFormatter;
$this->dateFormatter = $dateFormatter;
}
}
Per semplificare il nostro esempio non effettueremo la validazione nel costruttore delle proprietà linkFormatter
e dateFormatter
, ma si tenga presente che andando in seguito ad invocare la funzione core call_user_func()
è necessario che queste proprietà siano riferimenti a entità di tipo callable, quindi questa validazione andrebbe aggiunta se si intende usare la classe in un ambiente di produzione.
Il primo metodo da implementare è quello che effettua la richiesta HTTP e restituisce la stringa XML del feed.
private function getFeed() {
if(!filter_var($this->url, FILTER_VALIDATE_URL) || strpos($this->url, 'https://medium.com/feed/') === false) {
return null;
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($ch);
if(curl_error($ch)) {
return null;
}
curl_close($ch);
return $response;
}
Per prima cosa validiamo l'URL passato alla classe e solo se è un URL di un feed valido proseguiamo invocando cURL. In questo caso non dobbiamo sopprimere gli eventuali errori SSL per motivi di sicurezza. La soppressione degli errori SSL, infatti, ha senso in fase di testing ma non in un ambiente live.
Definiamo quindi il metodo helper che effettuerà il parsing della stringa HTML contenuta nel testo del post e che restituirà il primo paragrafo e l'URL della prima immagine.
private function parseHTML($content) {
$html = new DOMDocument();
libxml_use_internal_errors(true);
$html->loadHTML($content);
$image = $html->getElementsByTagName('img')->item(0);
$src = $image->getAttribute('src');
$firstParaText = $html->getElementsByTagName('p')->item(0)->firstChild->nodeValue;
return [
'image' => $src,
'content' => $firstParaText
];
}
L'implementazione DOM di PHP non prevede metodi che fanno uso dei selettori CSS. L'alternativa è usare XPath se si vuole ridurre la lunghezza del codice che accede al contenuto del testo degli elementi o ai valori dei loro attributi.
libxml_use_internal_errors(true)
sopprime gli eventuali Warning sollevati nel caso di marcatura non standard o comunque non assimilabile ad una DTD nota. Ora possiamo creare il metodo che effettuerà il parsing del feed e che restituirà un array con i dati rilevanti dei post.
private function parseFeed() {
$output = [];
$feed = $this->getFeed();
if(is_null($feed)) {
return $output;
}
$xmlDoc = new DOMDocument();
$xmlDoc->loadXML($feed);
$items = $xmlDoc->getElementsByTagName('item');
if(count($items) === 0) {
return $output;
}
foreach ($items as $item) {
$title = $item->getElementsByTagName('title')->item(0)->firstChild->nodeValue;
$permalink = call_user_func($this->linkFormatter, $item->getElementsByTagName('guid')->item(0)->firstChild->nodeValue);
$published = call_user_func($this->dateFormatter, $item->getElementsByTagName('pubDate')->item(0)->firstChild->nodeValue);
$contentHTML = $item->getElementsByTagNameNS('http://purl.org/rss/1.0/modules/content/', 'encoded')->item(0)->firstChild->nodeValue;
$data = $this->parseHTML($contentHTML);
$content = $data['content'];
$image = $data['image'];
$output[] = compact('title', 'permalink', 'published', 'content', 'image');
}
return $output;
}
getElementsByTagNameNS()
è qui fondamentale per poter accedere a quegli elementi contenuti all'interno di un namespace, che viene passato come primo argomento del metodo. Il secondo argomento è il nome locale, ossia la stringa che si trova a destra dei due punti in un tag come <content:encoded>
.
call_user_func()
accetta invece come primo argomento il nome della funzione da invocare e come secondo argomento il parametro che si intende passargli per l'elaborazione.
Infine definiamo il metodo pubblico principale che servirà i dati come output JSON.
public function render() {
$feed = $this->parseFeed();
header('Content-Type: application/json');
echo json_encode($feed, JSON_PRETTY_PRINT);
exit;
}
Un esempio d'uso potrebbe essere il seguente:
require_once 'MediumRSSReader.php';
function linkFormatter($guid) {
return str_replace('https://medium.com/p/', 'https://gabrieleromanato.it/', $guid);
}
function dateFormatter($date) {
$timestamp = strtotime($date);
if($timestamp === false) {
return $date;
}
return strftime('%d/%m/%Y', $timestamp);
}
$mediumRSSReader = new MediumRSSReader('https://medium.com/feed/@gabriele-romanato', 'linkFormatter', 'dateFormatter');
$mediumRSSReader->render();