Archivi categoria: php-mysql

Fare il backup compresso del database MySQL con PHP e inviarlo come allegato email

Ero un felice utente di MySQLDumper fino a quando Tophost non ha aggiornato i server all’architettura cloud e all’ultima versione di PHP, cosa che ha comportato la scomparsa di un paio di moduli PERL necessari e mi ha lasciato a terra (la routine PHP di MySQLDumper è molto lenta ed usa javascript per ricaricare le pagine, il che la rende incompatibile sia con wget che cronjob remoti come SetCronJob.com).

Così ho cercato e trovato due guide su internet (questa, e questa), le ho mischiate assieme potenziando soprattutto il codice di David Walsh, ho aggiunto un pizzico di compressione bzip2, e ho messo il tutto sul mio spazio web.

Ho rimosso la parte in cui è possibile selezionare quali tabelle salvare: ho pensato che un backup degno di questo nome è “all-inclusive”, e in fondo chiunque avesse il bisogno particolare di salvare solo alcune tabelle dovrebbe essere anche piuttosto navigato da poter modificare il codice in proprio.

AGGIORNAMENTO 9/9/11: siccome ne avevo bisogno (lo script andava incontro ad un errore di memoria insufficiente) ho aggiunto qualche riga per riattivare la lista delle tabelle da ignorare, basta riempire l’array con i nomi che vi servono.

Questo è il risultato:

<?php
$creationstart=strtok(microtime()," ")+strtok(" ");

$dbhost="indirizzo.server.mysql";
$dbname="nomedatabase";
$dbuser="usernamemysql";
$dbpass="passwordmysql";

$mailto="scrivimi@lamiacasella.it";
$subject="Backup DB";
$from_name="Il tuo caro sito web";
$from_mail="noreply@ilmiosito.it";

mysql_connect($dbhost, $dbuser, $dbpass);
mysql_select_db($dbname);

$tablesblocklist=array(
    "tablename1"=>1,
    "tablename2"=>1,
    "tablename3"=>1,
);
$tables = array();
$result = mysql_query("SHOW TABLES");
while($row = mysql_fetch_row($result))
$tables[] = $row[0];
foreach($tables as $table) {
if (!isset($tablesblocklist[$table])) {
$result = mysql_query("SELECT * FROM $table");
$return.= "DROP TABLE IF EXISTS $table;";
$row2 = mysql_fetch_row(mysql_query("SHOW CREATE TABLE $table"));
$return.= "\n\n".$row2[1].";\n\n";
while($row = mysql_fetch_row($result)) {
$return.= "INSERT INTO $table VALUES(";
$fields=array();
foreach ($row as $field)
$fields[]="'".mysql_real_escape_string($field)."'";
$return.= implode(",",$fields).");\n";
}
$return.="\n\n\n";
}
}
$filename='db-backup-'.date("Y-m-d H.m.i").'.sql.bz2';

$content=chunk_split(base64_encode(bzcompress($return,9)));
$uid=md5(uniqid(time()));
$header=
"From: ".$from_name." <".$from_mail.">\r\n".
"Reply-To: ".$replyto."\r\n".
"MIME-Version: 1.0\r\n".
"Content-Type: multipart/mixed; boundary=\"".$uid."\"\r\n\r\n".
"This is a multi-part message in MIME format.\r\n".
"--".$uid."\r\n".
"Content-type:text/plain; charset=iso-8859-1\r\n".
"Content-Transfer-Encoding: 7bit\r\n\r\n".
$message."\r\n\r\n".
"--".$uid."\r\n".
"Content-Type: application/octet-stream; name=\"".$filename."\"\r\n".
"Content-Transfer-Encoding: base64\r\n".
"Content-Disposition: attachment; filename=\"".$filename."\"\r\n\r\n".
$content."\r\n\r\n".
"--".$uid."--";
mail($mailto,$subject,"",$header);

$creationend=strtok(microtime()," ")+strtok(" ");
$creationtime=number_format($creationend-$creationstart,4);
echo "Backup del database completato, compresso a bz2 e spedito per posta elettronica in $creationtime secondi";
?>

(WordPress ha rimosso l’indent e non ho voglia di aggiustarlo a mano)

L’intera routine viene completata in circa 7secondi sul mio non tanto sveglio server Tophost, inclusi la compressione bzip2 e l’invio per posta elettronica, mentre il salvataggio del file in formato solo testo nel codice originale di David impiegava sullo stesso server 17secondi; probabilmente la differenza è dovuta alla rimozione di diversi cicli ridondanti, e all’uso delle funzioni native mysql_real_escape_string() e implode().

Sinceri ringraziamenti vanno ai rispettivi autori delle guide, senza le quali non stare scrivendo questa paginetta oggi 😉

Query MySQL con ORDER BY,GROUP BY per ordinare e rimuovere righe doppie

Apparentemente questo è un dilemma piuttosto discusso; molti “niubbi” di MySQL (me compreso) vogliono usare un SELECT su righe di una tabella, dove sanno che il campo ‘a’ ha valori ripetuti più volte, ma vogliono estrarre una sola riga per ogni valore, al tempo stesso facendo in modo tale che la riga estratta, tra tutte quelle disponibili, sia quella che ha il campo ‘b’ col valore più basso, o alto, o lungo, o corto tra tutte le possibili… bene, il primo tentativo che fanno questi niubbi è una query come questa:

SELECT a, b, c FROM table WHERE <conditions> ORDER BY b DESC GROUP BY a

e ottenendo un errore si rendono conto che non è possibile, perché la sintassi MySQL vuole che la clausola GROUP BY venga prima di quella ORDER BY, come nel seguente esempio:

SELECT a, b, c FROM table WHERE <conditions> GROUP BY a ORDER BY b DESC

In questo caso però, le righe prima vengono sfoltite rimuovendo i doppioni del campo ‘a’ (con un criterio che non possiamo scegliere noi), e solo dopo vengono ordinate per ‘b’, e questo non ci serve.

Quindi come si fa per ordinare prima le righe così come ci servono, e poi rimuovere quelle di troppo?

Per essere brevi, ecco come:

SELECT t.a, t.b, t.c
  FROM table t
  JOIN (SELECT MIN(b) as b
    FROM table
    GROUP BY a) t2 ON t2.b = t.b
  WHERE <condizioni>
  GROUP BY a
  ORDER BY NULL

Quello che fa questo “scheletro di query”, è ordinare le righe per il campo ‘b’ (in senso crescente, cioè ASC, siccome è stata usata la  funzione MIN(), altrimenti potreste usare MAX() per un ordinamento decrescente, e/o altre funzioni in base al tipo di ordinamento che vi serve) all’interno dell’operatore JOIN, e quindi raggruppa per valori unici del campo ‘a’ le righe nella query esterna. L’operatore ORDER BY NULL serve a evitare che l’interprete MySQL perda tempo a ordinare le righe risultanti, siccome un ordinamento viene già fatto con l’operatore GROUP BY. La query è efficace, ma poco efficiente, siccome risulta molto lenta.

All’inizio pensavo che mi sarei dovuto accontentare, tuttavia non soddisfatto, siccome imparando PHP e MySQL sono col tempo diventato ossessionato con l’ottimizzazione per le prestazioni, ho pensato di provare un approccio alternativo, cioè richiedere via SQL le righe “grezze”, e poi rielaborarle successivamente in PHP.

Esaminiamo questo approccio:

$results=doquery("
  SELECT a, b, c
    FROM table
    WHERE <condizioni>
  ");
$datagrid=array();
while ($slot=mysql_fetch_array($results)) $datagrid[]=$slot;
sort($datagrid);
$compare="";
foreach ($datagrid as $k=>$row)
  if ($row[0]==$compare)
    unset($datagrid[$k]);
  else
    $compare=$row[0];

Alla fine di questas routine, $datagrid contiene tutti i dati di cui avete bisogno, e solo quelli; spieghiamo:

  • la query SQL restituisce le righe non processate che corrispondono alle condizioni di WHERE, quindi con i doppioni del campo ‘a’, e senza nessun ordinamento particolare
  • dopo aver inserito i campi delle righe in un array multidimensionale, la funzione PHP sort() si occupa dell’ordinamento dell’array in base agli elementi dei sotto-array, prima per subarray[0], poi per subarray[1] e così via (apriamo una parentesi su questo discorso: se cercate su internet “ordinamento di un array multimensionale in PHP” -meglio se in inglese-, troverete ogni tipo di script artigianale, come se la sola funzoine sort() funzionasse solo sugli array monodimensionali; beh, io ho PHP5 sul mio hosting, ed ho provato a passare il suddetto array multimendionale a sort(), sorpresa delle sorprese l’ha messo in ordine esattamente come serviva a me; se però per voi non dovesse funzionare, recatevi sulla pagina di php.net dedicata alla funzione sort, e nella sezione dei commenti troverete molti esempi già compilati di script “casalinghi”, anche se non saranno mai veloci come sort() che è una funzione propria di PHP)
  • la stringa $compare è inizializzata, e usata nel successivo ciclo foreach per fare un unset() di tutti i subarray dove l’elemento [0] (il nostro campo ‘a’) è uguale a quello del precedente subarray, quindi rimuovendo i duplicati oltre la prima riga

Dopo di ciò potete passare l’array risultate ad un’altra routine per processare i dati così come vi servono.

Per portare la performance ancora oltre, nel caso abbiate bisogno di eseguire una procedura semplice sui dati ottenuti, potete modificare il ciclo foreach dell’esempio come segue:

foreach ($datagrid as $row)
  if ($row[0]!=$compare) {
    $compare=$row[0];
    <routine di elaborazione in questo spazio>
  }

Chiarendo, questo ciclo foreach non si occupa di eliminare i subarray duplicati, ma si limita ad eseguire la routine di elaborazione solo sulla prima istanza di un subarray, ignorando le copie successive; in questo modo, tagliate diverse volte sui tempi di esecuzione:

  1. risparmiate il tempo di un secondo foreach per scorrere l’array ed eseguire le operazioni di elaborazione, siccome queste vengono lanciate direttamente dal primo foreach
  2. evitate di lanciare la funzione unset() tante volte quanti sono i doppioni
  3. risparmiate i cicli di CPU e la memoria necessari a definire un secondo array per contenere i dati sfoltiti, siccome state lavorando direttamente sul primo array costruito sui risultati della query
  4. semplificate il foreach perché non richiedete più un elemento associativo ($k=>$row) ma solo il valore dell’elemento

Con i miei benchmark (su un set di dati MOLTO ristretto, lo ammetto), ho misurato un incredibile aumento di prestazioni di cento volte (già, 100 volte più veloce) per l’ordinamento/sfoltimento in PHP, paragonato al “doppio GROUP BY” in MySQL (0.08 secondi per MySQL/PHP, contro 8 secondi per la query complessa, su 100 iterazioni). I vostri risultati potrebbero essere diversi, ma partendo da un vantaggio di cento volte, c’è molto spazio di manovra.

Ricerca invertita in MySQL: controlla se le parole chiave nel campo corrispondono ad un testo

Di solito, “cercare in un database” significa cercare quelle righe che contengono un “documento” in un campo, che corrisponde a delle parole chiave (di solito usando la funzione MATCH AGAINST); mettiamo invece che vogliate fare il contrario, cioè avete una tabella con delle righe che contengono un campo in cui avete memorizzato delle parole chiave, e volete selezionare solo quelle righe in cui le parole chiave corrispondono ad un testo o un particolare documento. Questa è quella che io chiamo una ricerca invertita.

Uno scenario reale: avete un sito di offerte di lavoro, ed i candidati cercano annunci di loro gradimento, ma non ne trovano nessuno, oppure ce ne sono troppo pochi e non interessanti. O continuano a collegarsi al sito ripetendo la ricerca periodicamente (ad esempio con le parole chiave “veterinar* gatt*“, perché vogliono prendersi cura degli animali, e hanno un debole per i gatti in praticolare), o si iscrivono al feed RSS di quella ricerca (se ne avete uno)… oppure “salvano” la ricerca e aspettano che sia il sito ad inviare loro una mail non appena compare un nuovo annuncio pertinente, ed è qui che torna utile questo articolo.

Salvare le parole chiave per ogni utente in una tabella MySQL è semplice, ma quanto è semplice verificare se un annuncio appena inserito corrisponde alle parole chiave? In PHP, costruireste un array delle parole chiave, e per ogni elemento richiamereste strpos() per verificare che siano tutte contenute nel testo.

In MySQL la funzione INSTR() è l’equivalente di strpos() in PHP, ma non potete salvare degli array in un campo (intendo array veri e propri, non serializzati), e d’altra parte salvare tante righe quante sono le parole chiave scelte da ogni utente è sconveniente, siccome la normalizzazione non è necessaria: un “insieme di parole chiave” è quasi sempre unico per ogni utente che si iscrive ad una ricerca, per cui ha senso salvarlo in un campo testuale nella forma di “parola1,parola2,parola3,…“; anche qui, in PHP useremmo explode(“,”,$parole) per ottenere un array, ma in MySQL non esiste nessuna funzione simile, per cui è necessario crearne una.

Idealmente, avreste bisogno di una funzione a cui passare il testo/documento in vostro possesso (la somma di titolo e corpo dell’offerta di lavoro, in questo caso), la stringa nella forma “parola1,…,parolaN“, e un parametro per indicare che il delimitatore (“colla” nel caso di implode/explode) è una virgola, quindi:

funzione(document, keywords, delimiter)

Questa funzione dovrebbe restituire un valore intero positivo se tutte le parole chiave sono presenti nel testo, oppure 0 in caso contrario.

La funzione che ho messo assieme incollando informazioni prese un po’ dovunque è:

DELIMITER ;;

DROP FUNCTION IF EXISTS `matchkwdarray`;;
CREATE FUNCTION `matchkwdarray`(`str` text, `arr` text, `delimit` tinytext) RETURNS char(1) CHARSET utf8
 DETERMINISTIC
BEGIN
 DECLARE i INT DEFAULT 1;
 DECLARE matches INT DEFAULT 1;
 DECLARE token TINYTEXT;
 label:WHILE(matches>0) DO
 SET token=replace(substring(substring_index(arr, delimit, i), length(substring_index(arr, delimit, i - 1)) + 1), delimit, '');
 IF token='' THEN LEAVE label; END IF;

 SET matches=INSTR(str, token);
 SET i=i+1;
 END WHILE;
 RETURN matches;
END;;

DELIMITER ;

Per aggiungere questa funzione al vostro database MySQL, potete collegarvi sia tramite CLI, sia usando una interfaccia web al database, come dovranno fare tutti gli utenti su un hosting condiviso, non avendo accesso alla linea di comando; phpMyAdmin, rigonfio com’è, non supporta comunque routine e funzioni, per cui avete bisogno di un altro DB manager, ed io ho scelto Adminer che svolge il suo lavoro egregiamente, più rapido di phpMyAdmin, e tutto in un piccolissimo singolo file PHP di circa 150kb.

Un esempio d’uso della funzione:

SELECT matchkwdarray('questo è solo un piccolo test molto semplice', 'solo,test', ',')
Restituisce: 2

SELECT matchkwdarray('questo è solo un piccolo test molto semplice', 'casa,albero', ',')
Restituisce: 0

SELECT matchkwdarray('questo è solo un piccolo test molto semplice', 'solo,test,semplice,albero', ',')
Restituisce: 0

Questa funzione è efficiente: appena verifica che una parola chiave non è contenuta nel testo interrompe la procedura e restituisce subito 0; ovviamente, se non cercate una precisione del 100% ma vi accontentate di una corrispondenza inferiore, potete modificarla per verificare comunque tutte le parole chiave, e restituire una percentuale di corrispondenza; potete anche rimuovere l’ultimo carattere dalle singole parole in modo tale che siano ammesse più varianti delle stesse.

Una query MySQL SELECT con numerose condizioni WHERE è più veloce

Per un certo motivo volevo aggiungere più condizioni WHERE ridondanti all’interno di una query MySQL SELECT, ma temevo di rallentare l’esecuzione della stessa.

In breve, sto lavorando a questo sito di annunci, dove per ogni oggetto c’è una tabella contenente tra le altre le righe comune_id, provincia_id e regione_id. Conoscendo il valore di comune_id, è possibile usare la query:

SELECT * FROM oggetti WHERE comune_id='12'

invece di:

SELECT * FROM oggetti WHERE comune_id='12' AND provincia_id='34' AND regione_id='56'

siccome i risultati sono gli stessi… ma conviene?

Ho preparato un veloce test:

include "db.inc.php";
$creationstart=strtok(microtime()," ")+strtok(" ");
for($i=0;$i<=999;$i++)
 $test=doquery("SELECT * FROM oggetti WHERE comune_id='58003'");
$creationend=strtok(microtime()," ")+strtok(" ");
$creationtime=number_format($creationend-$creationstart,4);
echo "solo comune: $creationtime<br />";

$creationstart=strtok(microtime()," ")+strtok(" ");
for($i=0;$i<=999;$i++)
 $test=doquery("SELECT * FROM oggetti WHERE comune_id='58003' AND provincia_id='58' AND regione_id='12'");
$creationend=strtok(microtime()," ")+strtok(" ");
$creationtime=number_format($creationend-$creationstart,4);
echo "tutti i parametri: $creationtime<br />";

A sorpresa, ci è voluto un tempo variabile dai 2.4s ai 4.5s per eseguire le 1000 iterazioni con “tutti i parametri”, mentre la versione “solo comune” richiedeva circa 0.4s in più. All’inizio ho pensato che si trattasse della cache MySQL in azione, ma invertendo la posizione delle due routine all’interno del codice i risultati sono rimasti gli stessi.

Quindi, anche se vi sembra ridondante, aggiungere più clausole WHERE ... AND ... nelle vostre query accelera l’esecuzione della ricerca.

This article has been Digiproved

La velocità delle funzioni PHP di ricerca su stringhe dipende dall'”ago”

Stavo giocando con alcuni test di prestazioni su dei templates nel mio post precedente, e sono incappato in una scoperta interessante riguardante il modo in cui funziona strpos, e conseguentemente tutte le altre funzioni di tipo “cerca (e sostituisci)” come str_replace, strrpos, stripos, str_ireplace, substr_replace, persino preg_match and preg_replace (ho testato solo str_replace e preg_match, ma suppongo ci siano gli stessi risultati per tutte le altre).

  1. Userò in questo articolo la convenzione di php.net, e chiamerò “ago” la stringa che state cercando, e “pagliaio” la stringa all’interno della quale effettuate la ricerca
  2. Conoscere questi risultati è utile in pratica solo se potete scegliere quali aghi inserire nel pagliaio, per poterli poi cercare successivamente, cioè se state costruendo un template
  3. Ho già cercato per controllare se questa “trovata” era già stata documentata da qualcun altro, ma così non sembra, quindi se mi fossi perso qualcosa evitate di aggredirmi

Mettiamo che stiate creando lo schema per un template, dove inserirete dei tag che dovranno essere sostituiti dai contenuti generati dinamicamente dal vostro sito. Potete chiamarli come volete purché siano stringhe uniche all’interno del modello, quindi per esempio %tag% o {tag} o <!–tag–> o ~tag~ (o come vi pare). Magari pensate che scegliere quale tipo di carattere di delimitazione usare per il nome del tag dipenda solo dai vostri gusti personali, e lo pensavo pure io prima di scoprire questa cosa per puro caso, comunque vi dico subito che se il template consiste in codice HTML ben formato, allora usare per esempio ~tag~ sarà molto più veloce che usare <!–tag–>.

Il principio generale è che cercare un ago che inizia con un carattere “raro” è molto più veloce che cercare un ago che inizia con un carattere molto comune all’interno del pagliaio.

Per esempio, se il vostro pagliaio è un estratto di un ebook, cercare “%ciao” (se presente) sarà molto più rapido che cercare “ciao”.  La giustificazione di questo? Evidentemente la funzione in C che cerca la stringa, inizia col cercare il primo carattere, e una volta trovato vede se quello successivo corrisponde, e così via; quindi cercando “ciao” la funzione incapperà in tutte le “c” per controllare se sono seguite da una “i”, se sì poi verificherà se c’è una “a”, ma se ad esempio la parola è “ciarpame”, la funzione verificando l’assenza della “o” finale dovrà scartare il lavoro ed il tempo impiegato, e proseguire. Invece il carattere “%” è raro, se non unico, all’interno di un testo “normale”, quindi la funzione dovrà “fermarsi” molte meno volte a controllare prima di trovare una corrispondenza piena.

Mettiamo questa teoria alla prova, ecco il codice:

$creationstart=strtok(microtime()," ")+strtok(" ");
for ($i=0;$i<100000;$i++) $testpos=strpos($test,"malesuarda");
$creationend=strtok(microtime()," ")+strtok(" ");
$creationtime=number_format($creationend-$creationstart,4);
echo "malesuarda $testpos: ".$creationtime."<br />";

$creationstart=strtok(microtime()," ")+strtok(" ");
for ($i=0;$i<100000;$i++) $testpos=strpos($test,"%malesuada");
$creationend=strtok(microtime()," ")+strtok(" ");
$creationtime=number_format($creationend-$creationstart,4);
echo "%malesuada $testpos: ".$creationtime."<br />";

Spieghiamo un po’ di cose: $test è una stringa relativamente lunga, definita precedentemente nel codice (è composta da un testo lorem ipsum con diversi paragrafi, per 13kb totali), all’interno della quale ho scelto una parola a caso, “malesuada“, che viene ripetuta diverse volte, e ho cambiato due occorrenze di questa parola, entrambe verso la fine della stringa, per renderle uniche, una è diventata malesuarda, cioè ho aggiunto una “r”, e l’altra (più avanti nella stringa) invece è stata modificata in %malesuada, quindi alla fine ho caricato lo script PHP; ho aggiunto un echo per $testpos in modo da confermare che le stringhe fossero state realmente trovate da strpos.

Come atteso, ecco i risultati:

malesuarda 10970: 3.5609
%malesuada 11514: 0.7632

Sostituendo strpos con qualunque altra funzione elencata all’inizio dell’articolo otterrete valori simili.

  This article has been Digiproved

Template in PHP, il più veloce tra preg_match, file_get_contents e strpos

Lavorando al sito di reecycle.it, il frecycling italiano per il riciclo degli oggetti, sto dedicando molta attenzione alla gestione del carico sul server Apache di Tophost, e quindi ho deciso di eseguire dei test per verificare quale fosse il metodo più veloce per caricare i template delle varie sezioni del sito (un template è un “modello” di pagina -o parte di pagina- in codice HTML, nel quale vengono inseriti i dati dinamici prodotti dall’engine del sito).

Tutti i template di reecycle.it sono stati inizialmente creati sottoforma di tanti file .html (uno per ogni template) con dei %tag% rimpiazzati tramite la funzione str_replace() prima dell’output, ma ho pensato che forse c’erano modi diversi e più rapidi per ottenere lo stesso risultato; infatti ogni pagina può contenere anche fino a 5 o più template diversi (il layout generale, quello del pannello di login e del modulo di ricerca semplice, il layout della tabella dei risultati di ricerca, e quello delle singole righe della tabella), e cioè richiedere ben 5 operazioni di accesso e lettura su altrettanti file diversi sull’hard disk (trascurando per ora la cache del disco); probabilmente è più rapido leggere una sola volta un unico file grande contenente tutti i template e caricarlo in memoria, e quindi estrarre di volta in volta le sezioni che ci interessano; questo ultimo passaggio può esser fatto in due modi diversi, ovvero con un paio di righe di codice “pulite” tramite la funzione preg_match() ed una espressione regolare, oppure con un “accrocco” meno elegante ma più grezzamente rapido come strpos() e substr() che richiede invece diverse righe di codice.

In pratica, dovevo scoprire quale fosse, tra i tre metodi (template in file separati, template su file unico con estrazione tramite regular expression, e template su file unico con estrazione tramite strpos/substr) quello più veloce. Ero già sicuro che strpos/substr avessero una performance migliore di preg_match, ma ho incluso tutti i metodi per completezza.

Questa è la routine che ho usato:

<?php
$creationstart=strtok(microtime()," ")+strtok(" ");

for ($i=0;$i<1000;$i++) {
 $text=file_get_contents("full.html");
 for ($n=1;$n<=8;$n++) {
 preg_match("/<!--{$n}\(-->(.*)<!--\){$n}-->/s",$text,$matches);
 $html[$n]=$matches[1];
 }
}

$creationend=strtok(microtime()," ")+strtok(" ");
$creationtime=number_format($creationend-$creationstart,4);
echo "preg_match: ".$creationtime."<br />";

/////////////////
$creationstart=strtok(microtime()," ")+strtok(" ");

for ($i=0;$i<1000;$i++) {
 $text=file_get_contents("full.html");
 for ($n=1;$n<=8;$n++) {
 $start=strpos($text,"<!--$n(-->")+strlen("<!--$n(-->");
 $ending=strpos($text,"<!--)$n-->");
 $html[$n]=substr($text,$start,($ending-$start));
 }
}

$creationend=strtok(microtime()," ")+strtok(" ");
$creationtime=number_format($creationend-$creationstart,4);
echo "strpos/substr: ".$creationtime."<br />";

////////////////////
$creationstart=strtok(microtime()," ")+strtok(" ");

for ($i=0;$i<1000;$i++) {
 for ($n=1;$n<=8;$n++) {
 $html[$n]=file_get_contents($n.".html");
 }
}

$creationend=strtok(microtime()," ")+strtok(" ");
$creationtime=number_format($creationend-$creationstart,4);
echo "file_get_contents: ".$creationtime."<br />";

dove full.html è il singolo file HTML contenente tutti i template (8 in tutto, consistenti in paragrafi del tipo lorem ipsum di varia lunghezza), identificati tramite <!--numerotemplate(--> e <!--)numerotemplate--> tra i quali era presente il codice da estrarre di ogni template, mentre i singoli template erano salvati come file nominati da 1.html a 8.html.

Quello che fa il codice è, per ogni metodo, ripetere 1000 iterazioni in cui vengono caricati tutti i singoli template, dall’1 all’8, e misurare il tempo impiegato. L’utilizzo non è del tutto realistico, siccome il codice del template è in realtà costituito da HTML su più linee, e non poche ma lunghe righe di testo, e i template non vengono mai caricati tutti assieme, ma ne servono solo alcuni per ogni pagina, nondimeno prestazioni migliori in questo test indicano anche prestazioni migliori con un utilizzo reale (SBAGLIATO! controllate in fondo per maggiori dettagli).

Ebbene, il risultato del test è stato il seguente:

preg_match: 1.8984
strpos/substr: 0.0681
file_get_contents: 0.1352

I tempi finali variavano ovviamente ad ogni refresh, da un minimo (per preg_match) di 1.4s fino ad un massimo di 3s, ma i rapporti tra i valori rimanevano sostanzialmente costanti, cioè la combinazione di strpos/strsub su singolo grande file è più veloce del doppio rispetto al metodo file_get_contents chiamato per ogni singolo file, ma quello che mi sorprende è come il metodo preg_match sia nientemeno che quasi 30 volte  più lento di strpos/strsub, e di rimando 15 volte più lento che costringere il server a leggere più volte singoli file (ma penso che qui ci sia lo zampino della cache del disco).

Menomale che il supporto tecnico di tophost mi aveva suggerito invece proprio di usare preg_match su un unico file piuttosto che file_get_contents su più file.

AGGIORNAMENTO:

Ho appena testato questo benchmark con i template reali di reecycle.it… quanto mi sbagliavo.

Ho creato una funzione su reecycle.it per recuperare i template, in modo che se il template richiesto non si trova nell’archivio a singolo file, viene caricato dal singolo file html, e in modo trasparente aggiunto in fondo all’archivio unico in modo da poter essere caricato da lì per le occorrenze future; alla fine ho ottenuto un file supertemplate.tpl di 37kb contenente (quasi) tutti i template di reecycle.it. Ho modificato la routine presentata sopra in modo che usasse i template reali (sia l’archivio a singolo file, sia i file separati per ogni template) escludento però il metodo con preg_match per ovvi motivi, ed i risultati si sono invertiti! Usare file_get_contents sui singoli template era veloce il doppio rispetto ad usare strpos/substr sull’archivio unico.

Do la colpa a due cose: il file è effettivamente grande, quindi le funzioni devono gestire un pacchetto dati molto più ampio, e specialmente i tag sono nel formato dei commenti HTML, quindi strpos viene confuso dai numerosi tag simili presenti nella stringa.

In effetti, dopo aver modificato l’archivio dei template in modo che i tag di delimitazione fossero tipo {tag(} and {)tag} invece che <!–…–>, i risultati sono tornati come quelli attesi, e la combinazione di strpos/substr era più performante di file_get_contents chiamato più volte, e maggiore era il numero di template richiesto, più aumentava la differenza di velocità tra i due metodi; riguardo a questo potete controllare i risultati del mio post successivo.

This article has been Digiproved

Tophost e variabile di sessione “user” riservata in PHP

Stavo allegramente imprecando stamattina contro il server FTP del nodo8 di tophost su cui è ospitato un progetto a cui lavoro, siccome non era utilizzabile a causa di impressionante lentezza nelle risposte.

Ho colto l’occasione per rivedere alcuni aspetti del codice PHP e riordinare la gestione delle sessioni utente per renderle più funzionali, al che ho deciso di caricare in un’unica variabile di sessione, $_SESSION["user"], un array contenente tutti i dati rilevanti dell’utente per potervi accedere rapidamente senza richiedere costantemente un accesso al database.

Ebbene, non sembrava che riuscissi a far funzionare nulla, eppure è tecnicamente possibile caricare un array all’interno di una variabile di sessione. L’array veniva caricato subito dopo il login (confermato da var_dump) ma alla pagina successiva era sostituito da una stringa, corrispondente allo username MySQL del sito. Ho messo in discussione il mio codice, cercando dove fosse il problema, e modificando alcune assegnazioni (il tutto perdendo minuti ad aspettare che il server FTP accettasse gli upload), fino a quando non ho provato a cambiare il nome della variabile a $_SESSION["visitor"], e magia, ha funzionato tutto correttamente. A quanto pare, siccome non ho trovato traccia di questo in manuali “ufficiali”, i server di tophost sono impostati per accedere al nome utente MySQL tramite la variable di sessione “user”… non so cosa pensare. Qual è la variabile di sessione contenente la password MySQL?

  This article has been Digiproved

Estensione PunBB notifica via e-mail ogni nuovo post

Su un altro sito che gestisco, ho appena eseguito il passaggio dal vecchio forum a PunBB, ed sin da subito mi è mancata la possibilità di attivare una notifica via mail per ogni nuovo post inviato dagli utenti, così ho dovuto scrivere da solo un’estensione che facesse proprio questo.

Di seguito è riportato il codice sorgente dell’estensione, basta salvarlo come manifest.xml, e caricarlo nella cartella /extensions/newpost_mail_notify/ di PunBB (o un altro nome di vostro gradimento), quindi installarlo dal pannello di amministrazione di PunBB col metodo solito. Non c’è nulla da impostare, finché è attivo fa il suo lavoro.

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE extension SYSTEM "ext-1.0.dtd">

<extension engine="1.0">
<id>pun_newpost_mail_notify</id>
<title>Invia una mail all'admin per ogni nuovo post</title>
<version>1.1</version>
<description>Questa estensione invia una mail di notifica all'indirizzo preoconfigurato dell'admin ogni volta che qualcuno che non è admin invia un post nel forum</description>
<author>Ephestione</author>

<minversion>1.3.2</minversion>
<maxtestedon>1.3.4</maxtestedon>

<hooks>
<hook id="po_pre_redirect"><![CDATA[
if ($forum_user['group_id']!=1) { //default admin user id
$notification_to=$forum_config['o_admin_email'];
$notification_subject='['.$forum_config['o_board_title'].'] Nuovo post/discussione';
$notification_message='Nuovo post/discussione all'indirizzo: '.$base_url.'/viewtopic.php?pid='.$new_pid.'#p'.$new_pid;
mail($notification_to,$notification_subject,$notification_message);
}
]]></hook>

</hooks>

</extension>

L’estensione è molto spartana, invia una mail all’indirizzo preconfigurato dell’amministratore del forum, contenente solo un link al post all’interno del forum.

Ho appena aggiornato il codice come da suggerimento di Grez sui forum di PunBB in modo che il check sull’utente sia più solido, e controlli non l’id utente, che può essere mutevole, ma l’id del gruppo a cui appartiene, che in PunBB per gli amministratori è 1.

Note informative dovute: non sono responsabile di nulla se esplode il vostro server, l’estensione è stata fatta e testata per PunBB 1.3.4 (e se ancora non avete quella versione, è forse ora di aggiornare); se avete un forum molto attivo è caldamente sconsigliato il suo utilizzo, siccome finireste con la casella di posta inondata di messaggi; l’utilità per me è quella di essere avvisato non appena qualcuno scrive nel mio forum, che invece è tutt’altro che frequentato, e quindi non viene controllato quasi mai.

This article has been Digiproved

Come risolvere Fatal error: Allowed memory size exhausted in WordPress 3

Questo errore non è niente di nuovo per chi usa WordPress, da quanto so compariva anche nelle versioni precedenti di WordPress (sono un utente fresco della piattaforma, ed uso la versione 3 da più tempo di quanto abbia usato le versioni inferiori).

La novità però è che, diversamente da quanto trovate nelle guide che ne parlano, ora non è più possibile risolvere il problema modificando il file [CARTELLA WORDPRESS] > wp-settings.php, siccome la riga che definisce il limite di memoria ram allocabile non si trova più lì.

Ma guardando i contenuti del file wp-settings.php, all’inizio è presente la riga

require( ABSPATH . WPINC . '/default-constants.php' );

che la dice tutta, a meno che non siate a secco di PHP, e quindi questa guida fa per voi.

Quello che dovete fare è aprire il file [CARTELLA WORDPRESS] > wp-includes > default-constants.php e modificare la riga

define('WP_MEMORY_LIMIT', '32M');

in

define('WP_MEMORY_LIMIT', '40M');

o il valore che preferite, che sia 36, 48, o 64MB di ram, a seconda del numero di plugin e funzioni che usate; ovviamente il valore più basso possibile con cui funziona WordPress è preferibile, per motivi di prestazioni, soprattutto se vi trovate su un hosting condiviso.

Un appunto a parte proprio su quest’ultimo discorso: questo sito è ospitato su Tophost (almeno al momento in cui sto scrivendo), e una delle ultime polemiche sulla qualità del servizio è nata proprio da un certo utente che ha contattato piccato l’assistenza per questo problema, e non soddisfatto dal contenuto e dal tono della risposta ha dato il via ad una sciocca (almeno secondo me) campagna denigratoria; se l’utente avesse speso un paio di minuti per cercare di capire il codice sorgente (siccome si autodefinisce “programmatore”), avrebbe scoperto, proprio come ho scoperto io, dove doveva andare a cercare la famosa riga da modificare; avrebbe così risparmiato un bel po’ di bile, e anche di tempo, siccome da quanto so si lamenta di aver dovuto reinstallare la versione 2.9.7 e ripristinare un vecchio backup; quando, come appena descritto, è sufficiente una procedura di 30 secondi.

This article has been Digiproved

Ridimensionare, espandere o ridurre una image map HTML con uno script PHP

Sto creando un portale per un sito che avrà una simpatica cartina dell’Italia con le regioni cliccabili, messe in evidenza da un overlay in javascript. Ho trovato a questo scopo una imagemap già pronta e gratuita con relativa immagine di accompagnamento, ma era troppo piccola per essere utilizzabile, e quindi avevo bisogno di ingrandirla; allargare l’immagine è un compito elementare, ma modificare le coordinate della imagemap non era qualcosa che avevo intenzione di fare a mano!

Dopo aver cercato soluzioni già pronte, ma senza frutto alcuno, ho elaborato uno scriptino PHP che assolve esattamente questo compito. Questo script inserisce il codice HTML della image map in una stringa, e quindi elabora questa variabile di testo con una regular expression (se non conoscete le espressioni regolari non spaventatevi, all’inizio non ci capivo nulla neanch’io) che cerca e sostituisce i valori numerici al bisogno. A me serviva semplicemente di raddoppiare le dimensioni della cartina, lasciando ovviamente le proporzioni intatte, ma siccome sto pubblicando questa guida, ho deciso di modificare lo script (testato come funzionante) in modo che possa ridimensionare le coordinate in modo diverso per gli assi orizzontale e verticale, in modo che ad esempio possiate raddoppiare la larghezza di un’immagine, e dimezzare l’altezza.

Questo è uno script di esempio (la stringa $html è associata alle sole coordinate della regione lazio, per motivi di spazio, comunque potete copiare all’interno della variabile tutto il codice HTML della imagemap, compresi i tag <map>, i ritorni a capo e l’indentazione. Prestate attenzione a fare un ESCAPE delle doppie virgolette all’interno del codice HTML prima di inserirlo nella stringa, in altre parole dovete mettere un backslash (cioè il carattere \) davanti a tutte doppie virgolette (il carattere ) per non corrompere la definizione della stringa; per questo io ho usato la funzione cerca e sostituisci di Notepad++.

<?php
$html="<area  href=\"#\" alt=\"state\" title=\"lazio\" shape=\"poly\"  coords=\"74.513,86.938,75.667,87.365,75.667,88.007,74.744,89.077,75.436,90.467,76.359,90.039,77.857,90.039,78.319,90.039,79.127,90.788,79.588,91.857,79.588,92.606,80.049,93.034,80.51,93.034,81.317,94.103,81.779,94.852,82.24,94.959,83.74,94.852,84.201,94.959,85.123,94.959,86.392,94.103,87.43,93.141,88.122,93.141,89.39,93.141,89.967,92.713,91.351,90.895,91.813,90.895,92.274,91.216,93.196,90.895,94.349,90.788,94.926,90.467,96.31,89.825,96.886,90.467,96.656,90.895,95.849,91.323,95.387,92.072,94.234,92.072,92.965,92.713,92.505,93.676,92.505,94.317,92.734,94.959,91.928,95.28,91.813,95.922,91.467,96.778,92.505,98.382,92.505,99.023,92.505,99.986,91.928,101.804,91.928,103.194,92.734,103.837,94.234,103.623,96.31,104.264,97.579,105.013,99.309,106.51,102.191,108.543,103.229,108.543,104.728,109.077,106.113,110.361,106.574,111.965,106.804,113.035,106.574,113.783,106.574,114.425,105.882,114.853,105.305,115.067,104.844,115.067,104.728,116.029,104.728,117.099,104.152,118.061,103.46,118.703,102.999,119.345,102.999,120.093,101.961,120.308,100.23,120.735,99.539,120.308,98.271,119.345,96.656,118.489,95.156,118.275,92.965,118.489,91.005,118.703,89.39,116.885,89.506,116.029,88.122,114.639,85.931,113.997,83.97,112.607,81.548,110.574,78.55,107.687,77.627,105.869,76.128,104.692,74.975,102.874,73.706,101.056,71.745,99.023,70.131,97.098,67.594,94.959,69.093,93.676,70.131,92.606,70.592,91.216,70.592,90.039,71.745,89.611,72.553,88.649,73.014,88.221,72.553,86.938,73.245,86.189,74.513,86.938\"  />";
echo preg_replace("/([0-9.]{2,}),([0-9.]{2,})/e","round(\\1*2,3).','.round(\\2*1.5,3)",htmlentities($html));
?>

Tutto quello che dovete fare è cambiare i parametri di ridimensionamento evidenziati in rosso, nell’esempio la mappa immagine viene raddoppiata orizzontalmente, e moltiplicata di una volta e mezzo verticalmente. Se volete aumentare le dimensioni di tre volte, usate questa riga:

echo preg_replace("/([0-9.]{2,}),([0-9.]{2,})/e","round(\\1*3,3).','.round(\\2*3,3)",htmlentities($html));

Una volta fatto, copiate lo script PHP sul vostro server, apritelo con un browser, e fate copia/incolla. Fate attenzione ad un particolare: nel mio esempio la image map conteneva solo le coordinate separate da virgole e nient’altro, quindi ho dovuto usare questa espressione regolare… se nel vostro caso tra le coordinate sono presenti spazi, tabulazioni o altre cose, avrete quasi sicuramente bisogno di modificare l’espressione per farla funzionare. Se siete persi, scrivete nei commenti con un estratto dal codice HTML della imagemap e vedrò di darvi una mano.

This article has been Digiproved