Metodi Magici

35. I metodi magici __sleep() __wakeup e __set_state()

I primi due metodi magici che illustrerò in questo capitolo sono __sleep() e __wakeup(), che hanno in comune la Serializzazione degli Oggetti descritta in questa guida prima della serie sui metodi magici.

PHP richiamerà il metodo __sleep() subito prima di una serializzazione di una istanza della nostra classe.
__wakeup() invece sarà chiamato subito dopo una deserializzazione dell'oggetto, rendendolo utile ad esempio per ristabilire una connessione al database chiusa prima della serializzazione.

Altra differenza importante fra i due metodi è che __sleep() deve ritornare un'array contenente la lista degli attributi da serializzare, e questo ci consente di avere un controllo maggiore sulle serializzazioni della nostra classe.

Se omettete il valore di ritorno di __sleep() la serializzazione fallirà restituendo un valore NULL.

Vediamo un esempio teorico :

<?php

	class Oggetto
	{
		public $attributo = "stringa";

		public function __sleep()
		{
			echo "L'oggetto sta per essere serializzato!<br>";
			return array("attributo");
		}

		public function __wakeup()
		{
			echo "L'oggetto è appena stato deserializzato!<br>";
		}
	}

	$obj = new Oggetto();

	$s = serialize($obj);
	echo "Oggetto serializzato = $s<br>";

	$u = unserialize($s);
	echo $u->attributo;

?>

Il codice produce il seguente risultato :

L'oggetto sta per essere serializzato!
Oggetto serializzato = O:7:"Oggetto":1:{s:9:"attributo";s:7:"stringa";}
L'oggetto è appena stato deserializzato!
stringa

Overload di __set_state()

__set_state() è un metodo statico che viene richiamato per l'esportazione delle classi tramite var_export().

L'unico parametro di questo metodo è un array contenente le proprietà esportate nella forma array ("proprietà" => valore, ...)

Il metodo è disponibile da PHP 5.1.0


E con questo capitolo abbiamo definitivamente concluso la serie sui metodi magici.

34. La funzione Magica __autoload()

E' buona pratica nella programmazione orientata agli oggetti, avere un file per la dichiarazione di ogni classe, ognuno nominato con lo stesso identico case usato per il nome della classe.

Questo ci consente di trovare immediatamente il codice di una classe, e di apportarvi modifiche immediatamente disponibili in tutti i sorgenti che utilizzano tale classe, facendoci risparmiare molto tempo durante la manutenzione del codice.

Questo metodo ci consentirà anche di farci risparmiare molte righe di codice perchè ci da la possibilità di includere esclusivamente le classi che realmente sono necessarie per la corretta esecuzione dello script.

In applicazioni reali però, diventa problematico inserire in ogni file dello script decine e decine di righe di inclusioni di file esterni, e a questo proposito PHP ci mette a disposizione la funzione Magica __autoload().

Esempio con __autoload()

ClasseUno.php

<?php

	class ClasseUno
	{
		public $attributo = "Attributo di Classe Uno";
	}

?>

ClasseDue.php

<?php

	class ClasseDue
	{
		public $attributo = "Attributo di Classe Due";
	}

?>

test.php

<?php

	function __autoload($classe)
	{
		require_once("$classe.php");
	}

	$obj = new ClasseUno();
	echo $obj->attributo . "<br />\n";

	$obj = new ClasseDue();
	echo $obj->attributo;

?>

L'output prodotto è il seguente :

Attributo di Classe Uno
Attributo di Classe Due

Come potete notare dalla pagina test.php, non ho esplicitamente incluso i file esterni ClasseUno.php e ClasseDue.php, ma ci ha pensato la funzione __autoload() richiamata automaticamente da PHP che non ha trovato subito le dichiarazioni delle classi.

Nota : fate attenzione a non specificare più di una funzione __autoload() per script o riceverete un errore fatale.

Fatal error : Cannot redeclare __autoload() (previously declared in C:\AppServ\www\test\test.php:3)


Inoltre i nomi delle classi in PHP sono case-insensitive, mentre __autoload() li gestisce come sensitive, perciò prestate molta attenzione a quando nominate i file, che come dicevo all'inizio è buona norma che siano identici al nome della classe.

33. Il metodo Magico __call()


Overload di __call()

Prototipo della funzione :

mixed __call (string method, array arguments)

Mediante l'overloading del metodo magico __call(), abbiamo la possibilità di catturare e gestire tutte le chiamate a metodi non esistenti ossia non dichiarati nella classe.

L'overload di questo metodo torna particolarmente utile qualora si utilizzi una strategia di programmazione come la Delegation Pattern.

<?php

	class Oggetto
	{
		private $arr;

		public function Oggetto()
		{
			$this->arr = array();
		}

		private function aggiungiAllaFine($valore)
		{ array_push($this->arr, $valore); }
		
		private function aggiungiAllInizio($valore)
		{ array_unshift($this->arr, $valore); }

		public function __call($metodo, $parametri)
		{
			if ($metodo == "aggiungi")
			{
				if ($parametri[1] == "inizio")
				{ $this->aggiungiAllInizio($parametri[0]); }
				else if ($parametri[1] == "fine")
				{ $this->aggiungiAllaFine($parametri[0]); }
				else
				{ throw new Exception("Parametri non validi"); }
			}
			else
			{ throw new Exception("Metodo inesistente"); }
		}

		public function mostra()
		{
			foreach ($this->arr as $valore)
			{
				echo "$valore<br />\n";
			}
		}
	}

	try
	{
		$obj = new Oggetto();

		$obj->aggiungi(3, "inizio");
		$obj->aggiungi("pippo", "inizio");
		$obj->aggiungi(44.5, "fine");
		$obj->aggiungi("ciao", "inizio");

		$obj->mostra();

		$obj->aggiungi(5);
	}
	catch (Exception $e)
	{ echo $e->getMessage(); }

?>

Il codice produce questo output sul browser :

ciao
pippo
3
44.5
Parametri non validi

Nota : __call() può restituire dei valori proprio come un metodo tradizionale, e come tale può chiamare anche funzioni esterne e metodi di altre classi.

Nell'articolo Wikipedia da me segnalato sulla Delegation Pattern non viene usato __call(), ma viene spiegato semplicemente l'utilizzo di questa strategia di programmazione, a cui potrete poi facilmente applicare l'overload di __call().

32. Overload di __isset() e __unset()


Prototipi di __isset() e __unset()

bool __isset (string name)
void __unset (string name)

Effettuando un overloading su questi due metodi magici, abbiamo la possibilità di controllare quando vengono effettuate delle chiamate alle funzioni isset() e unset() (descritte qui) su un attributo della classe.

Anche in questo caso, le chiamate vengono effettuate solo per attributi non accessibili o non dichiarati.

Il metodo __isset() viene richiamato anche per l'overload su empty() :

<?php

	class Oggetto
	{
		private $privato;
		public $pubblico;

		public function __isset($attributo)
		{
			echo "<br />Chiamata a __isset() da \"$attributo\" = ";

			switch ($attributo)
			{
				case "privato" :
					return isset($this->privato);
					break;

				case "pubblico" :
					return isset($this->pubblico);
					break;

				default :
					throw new Exception("Attributo non esistente");
					break;
			}
		}

		public function __unset($attributo)
		{
			echo "<br />Chiamata a __unset() da \"$attributo\"";
			unset($attributo);
		}
	}

	try
	{
		$obj = new Oggetto();

		echo (int) isset($obj->pubblico);
		echo (int) isset($obj->privato);
		echo (int) empty($obj->privato);

		unset($obj->bho);
		echo (int) isset($obj->nonesisto);
	}
	catch (Exception $e)
	{ echo $e->getMessage(); }

?>

Il codice produce il seguente risultato :

0
Chiamata a __isset() da "privato" = 0
Chiamata a __isset() da "privato" = 1
Chiamata a __unset() da "bho"
Chiamata a __isset() da "nonesisto" = Attributo non esistente

Analizziamo il codice nel blocco try :

  • Prima echo : non provoca chiamate al metodo magico __isset() ma direttamente alla funzione standard isset() perchè l'attributo Oggetto->pubblico è accessibile (public)
  • Seconda echo : provoca una chiamata a __isset() che ritorna false da isset($this->privato); poichè $privato non è stato inizializzato
  • Terza echo : provoca una chiamata a __isset() che ritorna true perchè $privato non è stato inizializzato (quindi è vuoto)
  • unset() : provoca una chiamata a __unset() che stampa ("Chiamata a __unset() da "bho"")
  • Quarta echo : provoca una chiamata a __isset() che solleva un'eccezione ("Attributo non esistente")

Nota : nelle echo viene effettuato il casting esplicito al tipo int, perchè se echo riceve il valore booleano false non stampa nulla.

Inoltre fate attenzione alla versione di PHP che utilizzate perchè l'overloading di queste funzioni è disponibile solo da PHP 5.1.0

31. Il metodo Magico __get()


Overload di __get()

Prototipo della funzione :

mixed __get(string name)

Al contrario di __set(), l'overload di questo metodo ci consente di effettuare operazioni sulla lettura di un attributo.

Il metodo __get viene richiamato quando si tenta di accedere ad attributi con restrizioni (private o protected) o ad attributi non dichiarati.

Vediamo un semplice esempio :

<?php

	class Utente
	{
		private $nickname;
		private $password;
		public $dataiscrizione;

		public function Utente($n, $p)
		{
			$this->nickname = $n;
			$this->password = $p;
			$this->dataiscrizione = "01-01-1970";
		}

		public function __get($attributo)
		{
			switch ($attributo)
			{
				case "nickname" :
					return "Nickname : " . $this->nickname;
					break;

				case "password" :
					return "Password : " . $this->password;
					break;

				default :
					return "Attributo inesistente";
					break;
			}
		}
	}

	$utente = new Utente("johndoe", "mypassword");

	echo $utente->nickname . "<br />";
	echo $utente->password . "<br />";
	echo $utente->dataiscrizione . "<br />";
	echo $utente->nonesisto;

?>

L'esempio produce questo risultato :

Nickname : johndoe
Password : mypassword
01-01-1970
Attributo inesistente

Le prime due echo richiamano __get() e stampano correttamente i due attributi private ossia $nickname e $password, a cui __get() applica anche un prefisso col nome dell'attributo, come si evince dai primi due case della switch.

La terza echo invece stampa direttamente l'attributo $dataiscrizione senza passare da __get(), poichè tale attributo è esistente ed accessibile essendo dichiarato come public.

Infine l'ultima echo passa nuovamente per __get() stampando la stringa "Attributo inesistente" come programmato nell'opzione di default della switch.


30. Il metodo Magico __set()


Overload di __set()

Prototipo della funzione :

void __set (string name, mixed value)

Questo metodo ci consente di effettuare delle operazioni di assegnazione tradizionali sugli attributi della classe.

Il primo parametro è il nome dell'attributo mentre il secondo parametro rappresenta il valore da assegnare a tale attributo.

Se ad esempio abbiamo un'istanza di una ipotetica classe "Oggetto" che ha definito il metodo __set(), questo sarà richiamato quando verrà effettuata un'operazione di assegnazione ad un attributo della classe non esistente o non accessibile (private o protected), passando alla funzione i soliti due parametri, il nome dell'attributo sotto forma di stringa e il valore da assegnargli.


Esempi di overloading

<?php

	class Oggetto
	{
		public function __set($attributo, $valore)
		{ /* Corpo funzione */ }
	}

	$obj = new Oggetto();
	$obj->pippo = 32;

?>

L'ultima riga di codice quindi ($obj->pippo = 32), corrisponde ad una chiamata al metodo __set() con questi parametri : __set("pippo", 32);
Questo accade perchè nella definizione della classe "Oggetto", non abbiamo dichiarato nessun attributo di nome "pippo".

Vediamo un esempio più complesso :

<?php

	class Stringa
	{
		private $stringa;
		const MAX_CHARS = 15;

		public function Stringa($valore)
		{
			if (strlen($valore) > self::MAX_CHARS)
			{ throw new Exception("Stringa troppo lunga (max " . self::MAX_CHARS . " car.)"); }
			else
			{ $this->stringa = $valore; }
		}

		public function __set($attributo, $valore)
		{
			if ($attributo == "stringa")
			{ self::Stringa($valore); }
			else
			{ throw new Exception("Attributo non esistente!"); }
		}

		public function __toString()
		{ return $this->stringa; }
	}

	try
	{
		$obj = new Stringa("Ciao ciao!!!");
		echo "$obj<br>\n"; // Stampa "Ciao ciao!!!"

		$obj->stringa = "Ancora ciao!";
		echo "$obj<br>\n"; // Stampa "Ancora ciao!"

		$obj->stringa = "Un ultimo saluto!";
		echo "$obj<br>\n"; // Solleva un'eccezione con il messaggio : "Stringa troppo lunga (max 15 car.)"

	}
	catch (Exception $e)
	{
		echo $e->getMessage();
	}

?>

Nell'esempio soprastante ho impostato l'attributo $stringa della classe Stringa come privato, attraverso il modificatore di accesso private.

Poi ho reso l'attributo accessibile dall'esterno attraverso l'overload del metodo __set(), che in questo caso ho usato per simulare un accesso all'attributo di tipo public.

Questo mi consente di effettuare dei controlli precisi sui valori assegnati all'attributo $stringa, senza comunque perdere la trasparenza e la comodità della classica operazione di assegnamento, tipica degli attributi public.

A seguire un ultimo esempio, dove gestirò gli attributi della classe "Coordinata", come se fossero degli attributi normali memorizzandoli invece in un unico attributo di tipo Array.

Il questo modo avremo la sicurezza che venga sempre effettuato un casting esplicito di tipo float durante l'assegnazione dei valori, e risparmieremo inoltre righe di codice durante la gestione degli pseudo-attributi potendo utilizzare una semplice foreach per scorrerli tutti, creando del codice rapido e più flessibile.

<?php

	// Inizio dichiarazione Classe
	class Coordinata
	{
		private $xyz;

		public function Coordinata($x, $y, $z)
		{
			$this->xyz = array();

			$this->xyz["x"] = (float) $x;
			$this->xyz["y"] = (float) $y;
			$this->xyz["z"] = (float) $z;
		}

		public function __set($attributo, $valore)
		{
			switch ($attributo)
			{
				case "x" :
				case "y" :
				case "z" :
					$this->xyz[$attributo] = (float) $valore;
					break;
				default :
					throw new Exception("Attributo non valido!");
					break;
			}
		}
		
		public function __toString()
		{
			$return = "";

			foreach ($this->xyz as $chiave => $valore)
				$return .= "$chiave = $valore<br />\n";

			$return .= "<br>\n";

			return $return;
		}
	}
	// Fine dichiarazione Classe

	try
	{
		$punto = new Coordinata(15, 17.3, 22.4);

		echo $punto;

		$punto->x = 20.3;
		$punto->z = 11.2;

		echo $punto;

		$punto->a = 22.1; // L'attributo "a" non esiste e sarà sollevata un'eccezione
	}
	catch (Exception $e)
	{ echo $e->getMessage(); }

?>

Il codice soprastante produrrà questo risultato.

Condividi contenuti