venerdì, agosto 30, 2013

PHP - SQLite


Questo post fa parte di una serie preparata qualche anno fa per delle lezioni su PHP.

SQLite

SQLite è il database più diffuso al mondo, è nativamente a disposizione degli sviluppatori che usano PHP5 e non necessita di connessioni a database esterni. È ottimo per fare degli esperimenti e anche per applicazioni professionali che necessitano delle suemolte caratteristiche positive e non hanno bisogno di caratteristiche che invece gli mancano.

Un database SQLite è costituito da un unico file binario contenente tutto ciò che serve. I permessi per l'accesso al database corrispondono a quelli per l'accesso al file.

Il database è accessibile tramite delle API pubbliche che hanno consentito l'implementazione di librerie per diversi linguaggi di programmazione e anche per semplici programmi da usare sulla riga di comando, oppure integrati nel browser come SQLite Manager.

SQLite supporta transazioni e trigger, ma non i i vincoli di integrità referenziale, che però possono essere ottenuti predisponendo degli appositi trigger (esiste anche un apposito generatore).

Creazione del DB

Supponiamo di voler creare un database con due tabelle come quelle qui rappresentate:

Schema del database di esempio (ottenuto con wwwsqldesigner)

Nota: nella progettazione di basi di dati, spesso i nomi delle tabelle vengono impostati al plurale e i campi al singolare (avremmo quindi Pictures e Categories, ma Category_id). Visto che, come vedremo, ci sarà una corrispondenza abbastanza diretta tra tabelle e classi, qui useremo, per semplicità (l'approccio è pragmatico, no?), sempre i nomi al singolare.

Il codice sarà simile al seguente:

<?php

try
{
  $db = new SQLiteDatabase('pictures.sql.db', 0666);
  // il file dovrebbe essere in un punto inaccessibile agli utenti

  $query="
CREATE TABLE Picture (
id INTEGER AUTOINCREMENT NOT NULL,
Path TEXT NOT NULL ,
Description TEXT DEFAULT NULL,
Type TEXT DEFAULT NULL,
Width INTEGER DEFAULT NULL,
Height INTEGER DEFAULT NULL,
Category_id INTEGER NOT NULL ,
PRIMARY KEY (id)
);

CREATE TABLE Category (
id INTEGER AUTOINCREMENT NOT NULL,
Description TEXT NOT NULL DEFAULT 'NULL',
Rank INTEGER NOT NULL ,
PRIMARY KEY (id)
);
";

  $db->query($query);

  unset($db); 
}
catch (SQLiteException $e)
{
  echo 'Errore: ' . $e->getMessage() . "\n";
  die();
}
Nota: il codice per la query è stato ottenuto direttamente da wwwsqldesigner.

Inserimento di record

Per inserire record in una tabella sarà sufficiente effettuare delle query di tipo Insert:

<?php
try
{
  $db = new SQLiteDatabase('pictures.sql.db');

  $query="
INSERT INTO Category(Description, Rank) VALUES('Luoghi', 1);
INSERT INTO Category(Description, Rank) VALUES('Persone', 2);
INSERT INTO Category(Description, Rank) VALUES('Oggetti', 3);
";

  $db->query($query);

  unset($db);

}
catch (SQLiteException $e)
{
  echo 'Errore: ' . $e->getMessage() . "\n";
  die();
}

Dalla riga di comando possiamo vedere il risultato (shell bash sotto Linux):

$ echo "SELECT * FROM Category;" | sqlite pictures.sql.db 
1|Luoghi|1
2|Persone|2
3|Oggetti|3

Ottenimento di record

Per ottenere i record dobbiamo fare una query di selezione:

<?php

try
{
  $db = new SQLiteDatabase('pictures.sql.db');
  $query="SELECT * FROM Category;";

  $result = $db->arrayQuery($query, SQLITE_ASSOC);
  print_r($result);
  unset($db);
}
catch (SQLiteException $e)
{
  echo 'Errore: ' . $e->getMessage() . "\n";
  die();
}

Quello che otteniamo è:

Array
(
    [0] => Array
        (
            [id] => 1
            [Description] => Luoghi
            [Rank] => 1
        )

    [1] => Array
        (
            [id] => 2
            [Description] => Persone
            [Rank] => 2
        )

    [2] => Array
        (
            [id] => 3
            [Description] => Oggetti
            [Rank] => 3
        )

)

Come si vede, abbiamo usato per la funzione arrayQuery la costante SQLITE_ASSOC, che fa sì che ci venga restitutito un array associativo per ogni record. Avremmo potuto optare anche per SQLITE_NUM o SQLITE_BOTH.

Tutti i risultati (tutte le tuple) sono stati posti automaticamente in un array. In alternativa, potremmo desiderare di ottenere solo un handle all'insieme dei risultati, da passare in rassegna uno per uno:

<?php
try
{
  $db = new SQLiteDatabase('pictures.sql.db');
  $query="SELECT * FROM Category;";

  $result = $db->query($query);

  while($record = $result->fetch(SQLITE_ASSOC))
  {
    print_r($record);
  }

  unset($db);
}
catch (SQLiteException $e)
{
  echo 'Errore: ' . $e->getMessage() . "\n";
  die();
}

Risultati come oggetti

Si potrebbe desiderare anche che i risultati della query servano ad istanziare degli oggetti di una determinata classe:

<?php

class Category
{
  private
    $id,
    $Description,
    $Rank;
}

try
{
  $db = new SQLiteDatabase('pictures.sql.db');
  $query="SELECT * FROM Category;";
  $result = $db->query($query);

  while($category = $result->fetchObject('Category', null))
  {
    print_r($category);
  }

  unset($db);
}
catch (SQLiteException $e)
{
  echo 'Errore: ' . $e->getMessage() . "\n";
  die();
}

Risultato:

Category Object
(
    [id:private] => 1
    [Description:private] => Luoghi
    [Rank:private] => 1
)
Category Object
(
    [id:private] => 2
    [Description:private] => Persone
    [Rank:private] => 2
)
Category Object
(
    [id:private] => 3
    [Description:private] => Oggetti
    [Rank:private] => 3
)

La funzione fetchObject() permette di impostare anche una serie di parametri da passare al costruttore della classe. Potremmo sfruttare questa caratteristica per passare un riferimento al database di provenienza dell'oggetto (che ci tornerà utile):

<?php

class Category
{
  private
    $id,
    $Description,
    $Rank,
    $db;
    
  public function __construct($db)
  {
    $this->db=$db;
  }
}


try
{
  $db = new SQLiteDatabase('pictures.sql.db');
  
  $query="SELECT * FROM Category;";

  $result = $db->query($query);

  while($record = $result->fetchObject('Category', array($db)))
  {
    print_r($record);
  }

  unset($db);

}
catch (SQLiteException $e)
{
  echo 'Errore: ' . $e->getMessage() . "\n";
  die();
}

Setters e Getters impliciti

Già che ci siamo, introduciamo il concetto di setter e getter implicito, ottenuto con l'overloading delle funzioni standard __set() e__get()

<?php

class Category
{
  private
    $id,
    $Description,
    $Rank,
    $db;
    
  public function __construct($db)
  {
    $this->db=$db;
  }

  function __get($property)
  {
    return $this->$property;
  }
  
  function __set($property, $value)
  {
    if ($property=='Rank')
    {
      if ($value<0)
      {
        throw new InvalidArgumentException('Not a valid value: ' . $value);
      }
    }

    $this->$property = $value;
  }
}

try
{
  $db = new SQLiteDatabase('pictures.sql.db');
  $query="SELECT * FROM Category WHERE Description = 'Luoghi';";

  $catPlaces = $db->query($query)->fetchObject('Category', array($db));

  $catPlaces->Rank = 7; // viene richiamata la funzione __set()

  print_r($catPlaces);

  unset($db);
}
catch (SQLiteException $e)
{
  echo 'Errore: ' . $e->getMessage() . "\n";
  die();
}

Funzione save()

Un oggetto della classe Category ha a disposizione tutto ciò che serve per salvare se stesso in caso di modifiche ai suoi dati. Aggiungendo la funzione membro save() alla classe:

class Category
{
  // ..
  public function save()
  {
    $query=sprintf(
      'UPDATE Category SET Description="%s", Rank=%d WHERE id = %d',
      $this->Description, $this->Rank, $this->id);
      
    $this->db->query($query);
  }
}

possiamo scrivere delle istruzioni come le seguenti:

  $query="SELECT * FROM Category WHERE Description = 'Luoghi';";
  $catPlaces = $db->query($query)->fetchObject('Category', array($db));
  $catPlaces->Rank = 9;
  $catPlaces->save();

e ottenere l'aggiornamento del record nella tabella:

$ echo "SELECT * FROM Category;" | sqlite pictures.sql.db 
1|Luoghi|9
2|Persone|2
3|Oggetti|3

Setters e Getters espliciti

L'impostazione di setters e getters espliciti consente, fra le altre cose, l'uso di un'interfaccia fluente:

class Category
{
  // ...

  public function setRank($value)
  {
    $this->Rank = $value;
    return $this;
  }

  public function setDescription($value)
  {
    $this->Description = $value;
    return $this;
  }
}

Nel codice della funzione chiamante potremo quindi scrivere:

// ...
$catPlaces = $db->query($query)->fetchObject('Category', array($db));

echo "Prima della cura\n";
print_r($cat_places);

$catPlaces
->setRank(9)
->save();

Esercizi

  1. Fare in modo che la funzione save() effettui un operazione di Insert quando l'oggetto è stato creato direttamente e di Updatequando invece proviene dal DB.
  2. Scrivere un'applicazione che legge una directory contenente delle immagini di vario tipo (jpeg, png, gif), recupera le informazioni su di esse, attribuisce una categoria casuale (o determinata in base a qualche criterio) e scrive le informazioni nel DB.
  3. Scrivere un'applicazione, secondo il pattern MVC, che mostra in una pagina web tutte le immagini di una determinata categoria.
  4. Aggiungere all'applicazione la possibilità di cambiare le informazioni associate ad un'immagine (descrizione e categoria di appartenenza), usando il metodo POST per la conferma (con redirezione alla pagina di dettaglio dopo che l'aggiornamento è stato effettuato).

PHP - Basi di dati


Questo post fa parte di una serie preparata qualche anno fa per delle lezioni su PHP.

Basi di dati

Un'applicazione con un minimo di complessità avrà bisogno sicuramente di memorizzare dei dati e di sfruttare quindi delle basi di dati.
Un classico delle applicazioni web sviluppate con PHP si appoggiano a MySQL (e deriva da questo il fatto che spesso si parli di LAMP, Linux-Apache-MySQL-PHP), ma in realtà sono disponibili moltissimi altri tipi di database.

Accesso ai dati

Per accedere ai dati in una base di dati si può far uso:
  • degli strumenti nativi offerti da PHP per il particolare tipo di basi di dati (funzioni native per MySQL, per SQLite, ecc.);
  • di strumenti che offrono un livello di astrazione per accedere ad un qualsiasi tipo di base di dati tra quelli supportati, permettendo le operazioni basilari (ad esempio, PDO);
  • di strumenti che consentano la generazione di codice PHP per la gestione degli oggetti associati ai record nel database (ORM, Object-Relational Mapper, come Propel o Doctrine);
  • di framework che, appoggiandosi a un ORM, permettono di gestire secondo il pattern MVC i dati.

Gli esempi di queste lezioni

In questo corso vedremo degli esempi di base con il codice nativo per SQLite, per MySQL e tramite PDO. L'uso degli ORM e dei framework di sviluppo è fortemente consigliato, ma non ne parleremo perché la documentazione in proposito è ottima e abbondante e non vale la pena di riprenderla qui.
Per le operazioni che possono essere effettuate sia con un'interfaccia procedurale sia con una interfaccia orientata agli oggetti, privilegeremo quest'ultima.

PHP - Analisi di un file XML


Questo post fa parte di una serie preparata qualche anno fa per delle lezioni su PHP.

Feed Atom come esempio di file XML

Prefiggiamoci come scopo quello di elaborare, per uso strettamente personale, un feed Atom come quello dell'ANSA relativo al Friuli-Venezia Giulia.

La struttura del file è la seguente:

<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<atom:link rel="self" type="application/rss+xml" href="http://www.ansa.it/web/notizie/regioni/friuliveneziagiulia/friuliveneziagiulia_rss.xml"></atom:link>
<title>News di 01. Friuli Venezia Giulia - ANSA.it</title>
<link>http://www.ansa.it/web/notizie/regioni/friuliveneziagiulia/friuliveneziagiulia.shtml</link><description>Updated every day - FOR PERSONAL USE ONLY</description>
<language>it</language>
<copyright>Copyright: (C) ANSA, http://www.ansa.it/web/static/disclaimer.html</copyright>
<item>
  <title><![CDATA[Mostre:Enzensberger...]]></title>
<description]]></title>

  <description><![CDATA[Per avvicinare...]]></description>
  <link>http://www.ansa.it/web/notizie/regioni/friuliveneziagiulia/2010/03/14/visualizza_new.html_1733110800.html</link>
  <pubDate>14 Mar 2010 18:44:00 +0100</pubDate>
  <guid>http://www.ansa.it/web/notizie/regioni/friuliveneziagiulia/2010/03/14/visualizza_new.html_1733110800.html</guid>
</item>
<item>
  <title><![CDATA[Regioni: sanita', in...]]></title>
  <description><![CDATA[Nel 2008 Molise...]]></description>
  <link>http://www.ansa.it/web/notizie/regioni/friuliveneziagiulia/2010/03/12/visualizza_new.html_1732935087.html</link>
  <pubDate>12 Mar 2010 18:47:00 +0100</pubDate>
  <guid>http://www.ansa.it/web/notizie/regioni/friuliveneziagiulia/2010/03/12/visualizza_new.html_1732935087.html</guid>
</item>
</channel>
</rss>

Come si vede, esiste una parte introduttiva, con il nome della testata, le informazioni sul copyright, ecc., seguita da una serie di articoli (item), per i quali viene riportato il titolo, una descrizione, un link, la data di pubblicazione e un identificativo univoco della risorsa (guid).

Prima versione: lettura semplice con simpleXml

In una prima versione, per capire se tutto funziona, possiamo scrivere codice di questo genere:

<pre>
<?php
$feed="http://www.ansa.it/web/notizie/regioni/friuliveneziagiulia/friuliveneziagiulia_rss.xml";
$xml=simplexml_load_file($feed);
echo "Title: " . chop($xml->channel->title) . "\n"; 
foreach ($xml->channel->item as $item)
{
  echo "title: " . $item->title . "\n";
  echo "  description: " . $item->description . "\n";
  echo "  link: " . $item->link . "\n";
}

che ci consente di ottenere:
Title: News di 01. Friuli Venezia Giulia - ANSA.it
title: Mostre:Enzensberger...
  description: Per avvicinare...
  link: http://www.ansa.it/web/notizie/regioni/friuliveneziagiulia/2010/03/14/visualizza_new.html_1733110800.html
title: Regioni: sanita', in...
  description: Nel 2008 Molise...
  link: http://www.ansa.it/web/notizie/regioni/friuliveneziagiulia/2010/03/12/visualizza_new.html_1732935087.html
title: Roma: spara...
  description: Arrestato...
  link: http://www.ansa.it/web/notizie/regioni/friuliveneziagiulia/2010/03/12/visualizza_new.html_1732933202.html

Seconda versione: MVC

Secondo la buona pratica della separazione tra modello (cicciotto), controller snello) e view (come serve), si potrebbe ripensare il codice in questo modo:

<?php
/* model */
class Article
{
  private
    $_title,
    $_description, 
    $_link,
    $_pubDate;

  public function __construct($title, $description='', $link='', $pubDate='')
  {
     $this->_title=$title;
     $this->_description=$description;
     $this->_link=$link;
     $this->_pubDate=$pubDate;
  }
  public function getTitle()
  {
    return $this->_title;
  }
  public function getLink()
  {
    return $this->_link;
  }
  public function getDescription()
  {
    return $this->_description;
  }
  public function getPubDate()
  {
    return $this->_pubDate;
  }
}

class NewsReader
{
  private
    $_feed,
    $_xml,
    $_articles;

  public function __construct($feed)
  {
    $this->_feed=$feed;
    $this->_xml=simplexml_load_file($this->_feed);
    $this->findArticles();
  }
  private function getXML()
  {
    return $this->_xml;
  }
  private function findArticles()
  {
    $this->_articles=Array();

    foreach ($this->getXML()->channel->item as $item)
    {
      $this->_articles[]=new Article(
        $item->title,
        $item->description,
        $item->link,
        $item->pubDate
      );
    }
  }

  public function getTitle()
  {
    return $this->getXML()->channel->title;
  }

  public function getArticles()
  {
    return $this->_articles;
  }
}


/* controller */

$feed="http://www.ansa.it/web/notizie/regioni/friuliveneziagiulia/friuliveneziagiulia_rss.xml";
$newsreader = new NewsReader($feed);

/* view */
?>
<html><head>...</head>
<body>
<h1><?php echo $newsreader->getTitle() ?></h1>

<ul>
<?php foreach($newsreader->getArticles() as $article): ?>
   <li><a href="<?php $article->getLink() ?>"><?php echo $article->getTitle() ?></a><br />
   <em><?php echo $article->getDescription() ?></em>
   <span class='pubdate'><?php echo $article->getPubDate() ?></span>
   </li>
<?php endforeach ?>
</ul>
</body>
</html>

Altre cose su XML

Esistono altri due modi per analizzare codice XML con PHP: SAX (che funziona ad eventi) e DOM (che mappa gli elementi in un albero in memoria, senza trasformarli in proprietà come fa SimpleXML).

Con SimpleXML ci possono essere alcuni problemi che riguardano la presenza di namespaces. Si veda l'articolo Using SimpleXML To Parse RSS Feeds di Stuart Hebert (o altri analoghi) per informazioni al riguardo.

Se un elemento contiene valori rappresentati con CDATA, bisogna caricare il file specificando di non interpretarne il codice (FIXME).

Esempio:

<pre>
<?php
$feed='http://www.ansa.it/web/notizie/regioni/friuliveneziagiulia/friuliveneziagiulia_rss.xml';
$xml=simplexml_load_file($feed, 'SimpleXMLElement', LIBXML_NOCDATA);
$items=$xml->xpath('channel/item');
foreach($items as $item)
{
    echo $item->title . "\n";
}

Se in un file XML ci sono più elementi fratelli (sibling), la proprietà con il nome dell'elemento rappresenta il primo dei fratelli, ma si possono specificare gli altri aggiungendo l'indice (FIXME):

echo $xml->channel->item  // primo elemento
echo $xml->channel->item[0]  // primo elemento
echo $xml->channel->item[1]  // secondo elemento

Esercizi

  1. Fare delle prove con altri tipi di feed
  2. Fare delle prove con altri tipi di file XML
  3. Fare delle prove con xpath

PHP - File di configurazione


Questo post fa parte di una serie preparata qualche anno fa per delle lezioni su PHP.

Dove mettere i file di configurazione

I parametri necessari per la configurazione/personalizzazione dell'applicazione andrebbero posti in uno o più file accessibili al server web, ma - se possibile - non al pubblico.
Per esempio, i file dell'applicazione potrebbero essere in /var/www/myapp, mentre i file di configurazione in /etc/myapp.
Ovvii motivi di sicurezza dicono che i file - soprattutto se non in formato php - non vanno messi in una directory accessibile.
Vale anche la pena di notare che ci potrebbero essere problemi in ambienti di shared hosting (sulla stessa macchina, il server web può leggere i file di configurazione di un altro dominio).

Tipi di file di configurazione

I tipi più comuni di file di configurazione sono:
  • puro PHP;
  • XML;
  • INI;
  • YAML.

File in puro PHP

Un file come questo config.inc.php

<?php
$CONFIG['directory_name'] = 'stuff';
$CONFIG['default_type'] = 'png';

potrebbe essere incluso in questo modo:

require_once('config.inc.php');
print_r($CONFIG);
echo "directory name: " . $CONFIG['directory_name'] . "\n";

File in formato XML

Il file è come questo:

<?xml version='1.0'?>
<config>
<directory_name>stuff</directory_name>
<default_type>png</default_type>
</config>

Per leggerlo scriveremo:

$conf=simplexml_load_file('config.xml');
print_r($conf);
echo "directory name: " . $conf->directory_name . "\n";

File in formato INI

Il file è come questo:

;CONFIGURATION FOR MY APPLICATION
directory_name = stuff
default_type = png

e per leggerlo scriveremo: 

$conf=parse_ini_file('config.ini');
print_r($conf);
echo "directory name: " . $conf['directory_name'] . "\n";

File in formato YAML


I file in formato YAML non sono (ancora) supportati nativamente da PHP (bisogna installare un estensione PECL o librerie apposite).

PHP - Intestazioni HTTP


Questo post fa parte di una serie preparata qualche anno fa per delle lezioni su PHP.

Introduzione

Spesso capita di dover indicare esplicitamente quali intestazioni HTTP devono essere inviate prima di servire un determinato contenuto.
La funzione header permette di farlo, ma è bene incapsularla in qualche classe che gestisca la questione in maniera strutturata.
Come esempio di partenza, consideriamo il caso in cui si voglia servire un file contenente il logo della nostra organizzazione:

<?php
header('Content-Type: image/png');
header('Content-Length: 5727');
readfile('logo.png');

Un errore comune è di avere qualche carattere prima dell'inizio del codice PHP, che porta al seguente esito:

Warning: Cannot modify header information - headers already sent by (output started at ...)

File not found

Se si vuole gestire un errore di tipo 404 (file not found), si può inviare un'intestazione specifica:

<?php
header('HTTP/1.0 404 Not Found');
header('Content-Type: text/html; charset=utf-8');
?>
<h1>Error 404: File not found</h1>

Esercizio

Predisporre uno script image.php che mostri una determinata immagine presente nel filesystem del server web.
In una prima versione, lo script verrà invocato con un URL simile al seguente:

http://../image.php?file=logo

Lo script dovrà verificare se il file logo.png esiste o meno in una determinata directory e servirlo se lo trova ed è leggibile (con la corretta impostazione della dimensione del file); altrimenti, dovrà restituire una pagina 404.

Predisporre una pagina web picture.php, richiamabile con un URL simile al seguente,

http://../picture.php?file=logo

che produca codice HTML valido (usare validator.w3.org per controllare) per mostrare l'immagine, sfruttando lo script precedente:

...
<html>
<head><title>...</title></head>
<body>
<img src="image.php?file=logo" alt="..." width="..." height="..." />
</body>
</html>

Il testo da riportare nell'attributo alt corrisponderà al nome del file (senza l'estensione), mentre larghezza e altezza dipenderanno dalle dimensioni effettive dell'immagine.

Nel caso di file non presente, deve essere restituita una pagina con l'indicazione che il file non esiste.

Definire una classe per l'immagine (quali saranno le funzioni membro?), una per la WebResponse, con funzioni tipo setHttpHeader(), una per la WebRequest, con funzioni tipo getParameter().

Varianti ed integrazioni

A partire dal codice precedente:
  1. fare in modo che image.php restituisca una pagina 404 quando il referer non è picture.php;
  2. gestire un URL più SEO, come ad esempio http://.../picture.php/file/logo.png;
  3. costruire i casi di test per la classe che gestisce l'immagine;
  4. far sì che, in qualche caso particolare (ad esempio, se la richiesta avviene nei secondi dispari) l'immagine venga restituita leggermente alterata (ad esempio, con un bordo rosso di due pixel)

PHP - Gestione degli errori


Questo post fa parte di una serie preparata qualche anno fa per delle lezioni su PHP.

Introduzione

La gestione degli errori ha a che fare con cosa si vuole che succeda nel caso si verifichino errori durante l'esecuzione. Esistono diversi livelli di errore (notice, warning, parse, error) che possono essere gestiti.

Funzioni utili

Tra le molte funzioni utili, segnalo error_log, che fa scrivere ciò che si desidera nel file di log del server web.

$name='Mario';
error_log(
  sprintf('name is currently "%s" (line %d)',
    $name,
    __LINE__
    )
  );

fa sì che venga scritto nel file di log:

[...date...] [error] [client 127.0.0.1] name is currently "Mario" (line 8)




PHP - Gestione delle eccezioni


Questo post fa parte di una serie preparata qualche anno fa per delle lezioni su PHP.

Introduzione

Non sempre possiamo dare per scontato che le cose funzionino come dovrebbero.
Supponiamo di dover leggere un file e elaborarlo in qualche modo.
L'istruzione di base è

$lines=file('foo.txt');

Siamo sicuri che il file esista? E che sia leggibile?

Se il file non esistesse, otterremmo

Warning
: file(foo.txt) [function.file]: failed to open stream: No such file or directory in... 

Se il file non fosse leggibile, otterremmo

Warning: file(foo.txt) [function.file]: failed to open stream: Permission denied in...

L'approccio del pessimista

L'approccio del pessimista si basa sullo scrivere del codice di test prima di effettuare l'operazione:

$filename='foo.txt';
if (file_exists($filename) && is_readable($filename))
{
  $lines=file('foo.txt');
}
else
{
  // some code here...
}

I problemi di questo approccio sono:
  1. le cose potrebbero andare male anche per altri motivi oltre a quelli presi in considerazione nel test (il file esiste ed è leggibile, ma ci sono blocchi danneggiati nel disco...);
  2. vengono fatte tre cose anziché una;
  3. nella frazione di secondo tra l'esecuzione dei test e l'esecuzione delle operazioni lo stato potrebbe cambiare (un altro processo cambia i permessi sui file);
  4. in alcuni casi (ad esempio la connessione ad un database con determinate credenziali) non c'è modo di verificare prima se le credenziali sono corrette.
Un vero pessimista quindi non potrebbe usarlo... :-)

L'approccio dell'ottimista

Un ottimista potrebbe dare per scontato che le cose vadano bene, ma tenere in considerazione l'ipotesi (da lui considerata remota) che invece vadano storte. Scriverà quindi un codice di questo genere, mettendo il silenziatore (simbolo @) all'istruzione che potrebbe fallire e controllandone l'esito a posteriori:

$lines=@file('foo.txt');
if ($lines)
{
  print_r($lines);
}
else
{
  // some code here...
}

Anche questo approccio ha dei problemi (vedi al riguardo Five reasons why the shut-op operator (@) should be avoided):
  1. vengono nascosti messaggi di errore insospettabili, rendendo più difficile il debug;
  2. rende l'esecuzione più lenta, perché tutto il meccanismo delle impostazioni (file php.ini) è invocato per cambiare il valore della variabile error_reporting) e il codice non viene ottimizzato (TODO: benchmark?)

L'approccio try... catch


L'approccio serio è di includere il codice che potrebbe fallire in un blocco try e gestire separatamente il possibile errore:

try
{
  $filename='foo.txt';
  if (!$lines=file($filename))
  {
    throw new Exception(sprintf('Could not read file "%s"', $filename));
  };
  print_r($lines);
}
catch (Exception $e)
{
  // do something here...
}


Se si vogliono evitare gli spiacevoli messaggi di warning, si dovrà lavorare sul file di configurazione php.ini (in produzione non li si vuole, nell'ambiente di sviluppo sì). In ambiente di produzione potrà essere utile impostare error_log a true, in modo da avere i messaggi di errore scritti nel log del server web.

Cosa fare nella gestione dell'eccezione dipende dai singoli casi, ma è bene sapere che un oggetto di tipo Exception mette a disposizione delle funzioni membro per accedere a informazioni utili per il debug:

catch (Exception $e)
{
  echo "Something went wrong\n";
  echo sprintf("message: %s\n", $e->getMessage());
  echo sprintf("file: %s\n", $e->getFile());
  echo sprintf("line: %s\n", $e->getLine());
}

con un risultato simile al seguente:

Something went wrong
message: Could not read file "foo.txt"
file:    /var/www/...mycode.php
line:    61

Subclassing delle eccezioni e catene di eccezioni

Può essere utile definire delle eccezioni personalizzate in modo da gestire in maniera specifica i vari problemi che si possono presentare. Inoltre, esiste un meccanismo a catena di blocchi try... catch che consente di recuperare informazioni su cosa è andato storto. Si veda questo esempio completo:

class FileNotReadableException extends Exception
{
}

function readFooFile($filename)
{
  try
  {
    if (!$lines=file($filename))
    {
      throw new FileNotReadableException(sprintf('Could not read file "%s"', $filename)); 
    };
    return $lines;
  }
  catch (Exception $e)
  {
    // there was another kind of error...
    throw $e;
  }
}


try
{
  print_r(readFooFile('foo.txt'));
}
catch (Exception $e)
{
  if ($e instanceof FileNotReadableException)
  {
    echo "I couldn't read the file\n";
    echo sprintf("message: %s\n", $e->getMessage());
    echo sprintf("file:    %s\n", $e->getFile());
    echo sprintf("line:    %s\n", $e->getLine());
    echo "trace:\n";
    foreach($e->getTrace() as $number=>$error)
    {
      echo sprintf("  error %d:\n", $number);
      foreach($error as $key=>$value)
      {
        echo sprintf("    %s: %s\n", $key, $value);
      }
    }
  }
  else
  {
    echo "Something went wrong for an unknown reason...\n";
  }
}

in cui:
  1. viene definita una classe personalizzata FileNotReadableException
  2. viene definita una funzione che lancia un'eccezione presa in carico nel codice principale
  3. viene controllato il tipo di eccezione lanciata (con l'operatore instance_of)

Il risultato potrebbe essere simile al seguente:

I couldn't read the file
message: Could not read file "foo.txt"
file: /var/www/corsophp/loris/lezioni/lezione_eccezioni.php
line: 62
trace:
  error 0:
    file: /var/www/corsophp/loris/lezioni/lezione_eccezioni.php
    line: 76
    function: readFooFile
    args: Array

In alternativa, è possibile impostare una serie di catch in cui si specificano i tipi di eccezione:

...
catch (FileNotReadableException $e)
{
  echo "I couldn't read the file\n";
  echo sprintf("message: %s\n", $e->getMessage());
  echo sprintf("file:    %s\n", $e->getFile());
  echo sprintf("line:    %s\n", $e->getLine());
  echo "trace:\n";
  foreach($e->getTrace() as $number=>$error)
  {
    echo sprintf("  error %d:\n", $number);
    foreach($error as $key=>$value)
    {
      echo sprintf("    %s: %s\n", $key, $value);
    }
  }
}
catch (Exception $e)
{
  echo "Something went wrong for an unknown reason...\n";
}

Il meccanismo è delle catene di eccezioni è alla base degli strumenti di debug di Symfony:


Classi predefinite di eccezioni

La Standard PHP Library mette a disposizione alcuni tipi di classi derivare di eccezioni, che potrebbero essere utilmente utilizzate.
Nell'esempio qui sopra, avremmo potuto scrivere:

class FileNotReadableException extends RunTimeException
{
}

PHP - Passaggio di parametri


Questo post fa parte di una serie preparata qualche anno fa per delle lezioni su PHP.

Passaggio di parametri

Quando si richiama una funzione è possibile passarle dei parametri. Gli esempi che seguono possono essere applicati anche, ovviamente, alle funzioni membro delle classi.
Tutti gli esempi vengono presentati con il relativo test Lime, in modo da abituarsi all'idea dello Unit Testing, di cui abbiamo parlato.

Passaggio per valore

function foobar($a)
{
  $a++;
  return $a;
}

$t=new lime_test(1, new lime_output_color());
$t->is(foobar(5), 6, 'foobar() returns the value incremented by one');

Impostazione di un valore di default


function foobar($a=4)
{
  $a++;
  return $a;
}

$t=new lime_test(2, new lime_output_color());

$t->is(foobar(5), 6, 'foobar() returns the value incremented by one');
$t->is(foobar(), 5, 'foobar() takes 4 as default, and returns 5');

Controllo dei parametri


function foobar($a)
{
  if (!is_integer($a))
  {
    throw new Exception('Function foobar accepts only integers as parameter');
  }
  $a++;
  return $a;
}

$t=new lime_test(3, new lime_output_color());

$t->is(foobar(5), 6, 'foobar() returns the value incremented by one');
try
{
  $n=foobar('abc');
  $t->fail('foobar() does not throw an exception with a string parameter');
}
catch(Exception $e)
{
  $t->pass('foobar() throws an exception with a string parameter');
}

try
{
  $n=foobar(1.2);
  $t->fail('foobar() does not throw an exception with a float parameter');
}
catch(Exception $e)
{
  $t->pass('foobar() throws an exception with a float parameter');
}

Oggetti come parametri


Il controllo del tipo è automatico per gli oggetti, se si indica esplicitamente a che classe devono appartenere:

class BazBar
{
  private $v;
  public function __construct($v)
  {
    $this->v=$v;
  }
  public function getV()
  {
    return $this->v;
  }
  
  public function incV()
  {
    $this->v++;
    return $this;
  }
}

class ExtraBazBar extends BazBar
{
}

function foobar(BazBar $a)
{
  $a->incV();
  return $a->getV();
}

$t=new lime_test(2, new lime_output_color());

$t->is(foobar(new BazBar(5)), 6, 'foobar() accepts a BazBar object');

$t->is(foobar(new ExtraBazBar(5)), 6, 'foobar() accepts an ExtraBazBar object');

Array di parametri


function foobar($parameters=array())
{
  $v=$parameters['value']+$parameters['inc'];
  return $v;
}

$t=new lime_test(1, new lime_output_color());

$t->is(foobar(array('value'=>5, 'inc'=>1)), 6, 'foobar() accepts an array of parameters');

Passaggio per riferimento


function foobar(&$v)
{
  $v++;
  return $v;
}

$t=new lime_test(1, new lime_output_color());

$k=5;
foobar($k);
$t->is($k, 6, 'foobar() changes the value of the variable passed as parameter');

Altre cose utili


In alcuni casi potrebbe essere utile fare ricorso alle funzioni func_num_args(), func_get_args(), ecc. Consultare le relative pagine del manuale.

PHP - Unit testing


Questo post fa parte di una serie preparata qualche anno fa per delle lezioni su PHP.
Adesso consiglierei di usare PHPUnit anziché Lime.

Unit testing

La pratica della predisposizione dei test unitari (unit testing) permette di semplificare le modifiche, semplificare l'integrazione e supportare la documentazione. Buoni motivi per adottarla...

Lime

Lime fa parte del framework Symfony, ma può essere utilizzato anche in maniera isolata. Documentazione al riguardo è rintracciabile nelle pagine del progetto (la guida rimane valida per quanto riguarda Lime, anche se la versione Symfony 1.2 è deprecata). Il codice di Lime è disponibile nel deposito SVN di Symfony.

Uso di Lime

Un semplice esempio di partenza di unit test con Lime è il seguente:

<pre>
<?php
require('../../lib/lime.php');
// impostare il percorso a seconda di dov'è il file

ini_set('error_reporting', E_ALL);

function __autoload($className)
{
    $filename=$className.'.class.php';
    include($filename);
}

$t=new lime_test(4, new lime_output_color());

$calculator = new Calculator();

$t->cmp_ok($calculator->getOperand(0), '===', false, '->getOperand() returns false for an unitialized value');

$t->isa_ok($calculator->setOperand(0,10), 'Calculator', '->setOperand() returns a Calculator object');

$t->is($calculator->getOperand(0), 10, '->getOperand() returns the correct value for an initialized value');

try
{
    $calculator->setOperator('_');
    $t->fail('->setOperator() allows a non valid operator');
}
catch (Exception $e)
{
    $t->pass('->setOperator() throws an exception with a non valid operator');
}


L'output dovrebbe assomigliare al seguente:
1..4
ok 1 - ->getOperand() returns false for an unitialized value
ok 2 - ->setOperand() returns a Calculator object
ok 3 - ->getOperand() returns the correct value for an initialized value
ok 4 - ->setOperator() throws an exception with a non valid operator
# Looks like everything went fine. 

Esercizio

Modificare la classe Calculator aggiungendo nuove funzioni membro e predisporre i relativi test.