WikiDump: Tool to get Wikipedia content

June 30th, 2010

WikiDump tool has been made to get Wikipedia content from a list of article ids. The tool connect on http://en.wikipedia.org to get text data.

For more informations about getting Wikipedia content, you can see this post.

Here is the source code:


package com.devbypractice;

import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

public class WikiDump
{
 /**
 * Get xhmlt content of Wikipedia source to String
 *
 * @param sUrlSource Url Source
 * @param sEncoding Encoding
 * @return xhtml content
 * @throws Exception
 */
 public static String dumpWikiXhtmlToString(String sUrlSource, String sEncoding)
 throws Exception
 {
 String inputLine;

 URL url;
 url = new URL(sUrlSource);
 URLConnection urlConn = url.openConnection();

 // TimeOut 30sec
 //
 urlConn.setConnectTimeout(30000);
 BufferedReader in = new BufferedReader(new InputStreamReader(
 urlConn.getInputStream(), sEncoding));

 StringBuilder sb = new StringBuilder();

 // XML header
 //
 sb.append("<?xml version=\"1.0\"?>");
 sb.append("<WikiContent>");

 // Copy response to buffer
 //
 while ((inputLine = in.readLine()) != null) {
 sb.append(inputLine);
 sb.append("\n");
 }
 in.close();

 // XML footer
 //
 sb.append("</WikiContent>");

 return sb.toString();
 }

 /**
 * Load article ids from files File must have an id on each line.
 *
 * @param sFileName
 * @return Articles ids
 * @throws Exception
 */
 public static List<Integer> loadArticleIdsFromFile(String sFileName)
 throws Exception
 {
 List<Integer> articleIds = new ArrayList<Integer>();

 FileReader reader = new FileReader(sFileName);
 BufferedReader buffReader = new BufferedReader(reader);

 String sLine = "";

 while ((sLine = buffReader.readLine()) != null) {
 Integer id = Integer.parseInt(sLine);
 articleIds.add(id);
 }

 buffReader.close();

 return articleIds;

 }

 /**
 * Display usage
 */
 public static void displayUsage()
 {
 System.out.println("Arguments:");
 System.out.println("<Article id source file path> <Output directory>");
 System.out.println("");
 System.out.println("Notes:");
 System.out.println("Article id file must have an id on each line. ");
 System.out.println("Output directory must exist.");

 }

 /**
 * Text cleanup. References [12], [3] etc... are deleted.
 *
 * @param text Text to clean
 * @return Cleaned text
 */
 public static String cleanup(String text)
 {
 return text.replaceAll("\\[[0-9]+\\]", "");
 }

 /**
 * Main entry
 *
 * @param args Arguments
 */
 public static void main(String[] args)
 {
 try {

 // UTF-16 chars count for output files
 // 1000000 ~= 2 Mo
 //
 final int fileSizeLimit = 1000000;

 // Current output file size
 // set to MAX + 1 for the file to be created at first time
 //
 int currentFileSize = fileSizeLimit + 1;

 // Url to get online Wikipedia article by id
 //
 String sWikiUrlFormat = "http://en.wikipedia.org/w/index.php?action=render&curid=%d";

 FileOutputStream outStream = null;
 OutputStreamWriter writer = null;

 if (args.length != 2) {
 displayUsage();

 }
 else {

 // Get article ids from file
 //
 List<Integer> articleIds = loadArticleIdsFromFile(args[0]);

 // Output dir
 //
 String sDestinationDir = args[1];

 // For each article id
 //
 for (Integer articleId : articleIds) {

 try {

 if (currentFileSize > fileSizeLimit) {
 if (writer != null) {
 writer.close();
 }
 outStream = new FileOutputStream(sDestinationDir + "\\"
 + articleId + ".txt");
 writer = new OutputStreamWriter(outStream, "UTF-16");

 currentFileSize = 0;

 }

 // Build article url
 //
 String sWikiUrl = String.format(sWikiUrlFormat, articleId);

 // Get article content
 //
 String sWiki = dumpWikiXhtmlToString(sWikiUrl, "UTF-8");

 // Load Xhtml content
 //
 InputSource is = new InputSource(new StringReader(sWiki));
 Document doc = DocumentBuilderFactory.newInstance()
 .newDocumentBuilder()
 .parse(is);

 // writer.write("========= " + articleId + " ==========\n");

 // Target desired wikipedia content
 //
 NodeList nl = XPathProcessor.getInstance().EvaluateMutli(
 "/WikiContent/p", doc);

 // For each node
 //
 for (int i = 0; i < nl.getLength(); i++) {
 String sContent = "";
 try {
 Node node = nl.item(i);

 // get text content
 //
 sContent = node.getTextContent();

 // clean text
 //
 sContent = cleanup(sContent);

 // Write in output file
 //
 writer.write(sContent);
 writer.write("\n\n");
 writer.flush();

 // Increase output file size count
 //
 currentFileSize += sContent.length() + 2;
 }
 catch (Exception e2) {
 String sLog = "Paragraph not get " + i
 + " for id=" + articleId;
 System.out.println(sLog);
 }
 }
 }
 catch (Exception e3) {
 String sLog = "Article not get for id = " + articleId
 + "\n";
 System.out.println(sLog);

 e3.printStackTrace();
 }

 }
 writer.close();
 }
 }

 catch (Exception e) {
 e.printStackTrace();

 }

 }

}

To compile the source you will need a XPath query manager available on this link.

A compiled version of WikiDump can be downloaded here.

Get Wikipedia content

June 29th, 2010

Wikipedia is a huge source of content for people who need big text data.

Wikipedia allow to get these data and provide database and documentation on this link.

My own experience allow to make such statements:

  • It’s painfull to parse Wiki language (Wikecode or WikiText), even if many tools exist (Mylyn etc..), proprietary templates make substitutions not possible.
  • The best Wikitext parser is Wikipedia itself…
  • Creating a local Wikipedia mirror is painfull and poorly documented.
  • Get parsed arcticles from Wikipedia website is not a problem when it is as slow as 1 article per second, but getting a large amout of articles is very long (1 article = 1 sec <=> 100000 articles = 1 whole day)
  • Arcticles must be filtered because everything is in Wikipedia, best and worst.

I was able to get data from Wikipedia in these few steps:

  1. Prerequisites
    MySQL server and tools
    Java virtual machine (the path of java.exe must be in PATH environnement variable)
    Custom Java tool WikiDump [ Sources - Compiled version ] (in the compiled version a batch is provided to launch a demo,  an exception is thrown as expected but it doesn’t interrupt  the process )
  2. Go to website http://download.wikimedia.org/enwiki/latest/ to get MySQL dumps:
    enwiki-latest-category.sql.gz
    : Contains a list of categories and arcticles count.
    enwiki-latest-categorylinks.sql.gz
    : Makes the link between articles and categories.
  3. Import theses scripts in a MySQL database (with Windows I used MySQL Administrator).
    Note: The process is long (many hours).
  4. Filter wanted articles by executing the following query to get desired portals:SELECT * FROM category WHERE cat_pages > 0 AND cat_title LIKE “Portal:%” ORDER BY cat_pages DESCOnce portals choosen  (take and refuse), select included articles. Execute following query (put your own portals here):

    SELECT cl_data_in.cl_from FROM ((SELECT DISTINCT cl3.cl_from FROM categorylinks AS cl3 WHERE cl3.cl_to IN (“Portal:take1,Portal:take2″)) AS cl_data_in)
    WHERE cl_data_in.cl_from NOT IN (SELECT DISTINCT cl2.cl_from FROM categorylinks AS cl2 WHERE LOWER(cl2.cl_to LIKE “liste d%”) OR cl2.cl_to IN (“Disambiguation_pages”,”Portal:refuse1″))

    Once article list is filtered, export result in csv file, not matter file format, the goal is to have an article id by line.

    Cleanup the file, delete column name and empty lines.

  5. To launch process to get content, type in command line:
    java -jar WikiDump.jar article_file_path output_directory > log.txt

WikiDump : Outil de récupération du contenu de Wikipedia

June 27th, 2010

L’utilitaire WikiDump permet de récupérer le contenu de Wikipedia à partir d’une liste d’identifiant d’articles. L’outil se connecte sur le site fr.wikipedia.org afin de récupérer des données à jour et correctement formattées.

Pour plus de détail concernant la récupération des données de Wikipedia rendez vous sur cet article.

Pour la curiosité ou bien pour modifier son comportement voici le code source:


package com.devbypractice;

import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

/**
 * Classe contenant un main permettant de faire un dump
 * de wikipédia.
 *
 */
public class WikiDump
{
 /**
 * Récupère le contenu xhtml d'une url source Wikipédia en String
 *
 * @param sUrlSource Url Source
 * @param sEncoding Encodage
 * @return Le contenu du html
 * @throws Exception
 */
 public static String dumpWikiXhtmlToString(String sUrlSource, String sEncoding) throws Exception {
 String inputLine;

 URL url;
 url = new URL(sUrlSource);
 URLConnection urlConn = url.openConnection();

 // TimeOut de 30sec
 //
 urlConn.setConnectTimeout(30000);
 BufferedReader in = new BufferedReader(
 new InputStreamReader(
 urlConn.getInputStream(), sEncoding));

 StringBuilder sb = new StringBuilder();

 // En tête XML
 //
 sb.append("<?xml version=\"1.0\"?>");
 sb.append("<WikiContent>");

 // Copie du contenu html vers le buffer
 //
 while ((inputLine = in.readLine()) != null){
 sb.append(inputLine);
 sb.append("\n");
 }
 in.close();

 // Fin XML
 //
 sb.append("</WikiContent>");

 return sb.toString();
 }

 /**
 * Charge la liste des identifiants des articles d'un fichier
 * vers un liste d'entiers.
 * Le fichier doit contenir un id sur chaque ligne.
 *
 * @param sFileName
 * @return Liste des identifiants des articles
 * @throws Exception
 */
 public static List<Integer> loadArticleIdsFromFile(String sFileName) throws Exception
 {
 List<Integer> articleIds = new ArrayList<Integer>();

 FileReader reader = new FileReader(sFileName);
 BufferedReader buffReader = new BufferedReader(reader);

 String sLine ="";

 while ((sLine = buffReader.readLine())!=null) {
 Integer id = Integer.parseInt(sLine);
 articleIds.add(id);
 }

 buffReader.close();

 return articleIds;

 }

 /**
 * Affiche le mode d'emploi de la ligne de commande
 */
 public static void displayUsage() {
 System.out.println("Arguments:");
 System.out.println("<Chemin fichier source id articles> <Chemin de destination>");
 System.out.println("");
 System.out.println("Notes:");
 System.out.println("Le fichier source des id articles doit être un fichier " +
 "comportant la liste des identifiants articles Wikipédia à récupérer, " +
 "ces identifiants doivent être séparés par une fin de ligne");
 System.out.println("Le chemin de destination doit exister.");

 }

 /**
 * Nettoyage du texte.
 * Les références du type [12], [3] etc... sont supprimées.
 *
 * @param text Texte à nettoyer
 * @return Texte nettoyé
 */
 public static String cleanup(String text) {
 return text.replaceAll("\\[[0-9]+\\]", "");
 }

 /**
 * Entrée principale
 *
 * @param args Arguments
 */
 public static void main(String[] args) {
 try {

 // Taille en caractères UTF-16 des fichiers de sortie
 // 1 Millions ~= 2 Mo
 //
 final int fileSizeLimit = 1000000;

 // Taille courante du fichier de sortie
 // mise à MAX + 1 afin qu'il soit créé lors du premier passage
 //
 int currentFileSize = fileSizeLimit + 1;

 // Url de récupération des articles Wikipédia par leur identifant article
 //
 String sWikiUrlFormat = "http://fr.wikipedia.org/w/index.php?action=render&curid=%d";

 FileOutputStream outStream = null;
 OutputStreamWriter writer = null;

 if (args.length != 2) {
 displayUsage();

 }
 else {

 // Récupération de la liste des identifiants articles à partir du fichier
 //
 List<Integer> articleIds = loadArticleIdsFromFile(args[0]);

 // Format de destination des fichiers du contenu de wikipédia
 //
 String sDestinationDir = args[1];

 // Pour chaque identifiant d'article
 //
 for (Integer articleId : articleIds) {

 try {

 // Si la taille courant du fichier de sortie est supérieure
 // à la taille max on en crée un nouveau
 //
 if (currentFileSize > fileSizeLimit) {
 if (writer != null) {
 writer.close();
 }
 outStream = new FileOutputStream(sDestinationDir + "\\" + articleId + ".txt");
 writer = new OutputStreamWriter(outStream, "UTF-16");

 currentFileSize = 0;

 }

 // Construction de l'url de l'article
 //
 String sWikiUrl = String.format(sWikiUrlFormat, articleId);

 // Récupération du contenu de l'article
 //
 String sWiki = dumpWikiXhtmlToString(sWikiUrl, "UTF-8");

 // Chargement XML du contenu
 //
 InputSource is = new InputSource(new StringReader(sWiki));
 Document doc = DocumentBuilderFactory.newInstance()
 .newDocumentBuilder()
 .parse(is);

 //writer.write("========= " + articleId + " ==========\n");

 // Ciblage du contenu à récupérer via XPath
 //
 NodeList nl = XPathProcessor.getInstance().EvaluateMutli(
 "/WikiContent/p", doc);

 // Pour chaque noeud
 //
 for (int i = 0; i < nl.getLength(); i++) {
 String sContent = "";
 try {
 Node node = nl.item(i);

 // Récupération du texte
 //
 sContent = node.getTextContent();

 // Nettoyage
 //
 sContent = cleanup(sContent);

 // Ecriture dans le fichier
 //
 writer.write(sContent);
 writer.write("\n\n");
 writer.flush();

 // Augmentation de la taille du fichier courant
 //
 currentFileSize += sContent.length() + 2;
 }
 catch (Exception e2) {
 String sLog = "Impossible de récupérer le paragraphe " + i + " pour id=" + articleId;
 System.out.println(sLog);

 }
 }
 }
 catch (Exception e3) {
 String sLog = "Impossible de récupérer l'article id=" + articleId + "\n";
 System.out.println(sLog);

 e3.printStackTrace();
 }

 }
 writer.close();
 }
 }

 catch (Exception e) {
 e.printStackTrace();

 }

 }

}

Cet utilitaire nécessite une classe de gestion des requêtes XPath, dont le code est à cette adresse.

Une version déjà compilée de WikiDump est disponible à cette adresse.

Récupérer le contenu de Wikipedia

June 26th, 2010

Wikipedia est une gigantesque source de contenu pour celui qui a besoin d’ une grand quantité de données en format texte.

Récupérer ces données est autorisé, Wikipedia fourni même des moyens pour récupérer tous styles de données ainsi que de la documentation (sommaire et fouillie) à cette adresse.

Mon expérience personnelle permet de faire les constats suivants:

  • Il est difficile de parser le balisage propre à Wiki (Wikicode ou Wikitext), même si de nombreux outils existent en Java ( Mylyn etc..) la présence de templates spécifiques Wikipedia rend certaines substitutions impossibles simplement.
  • Le meilleur parser de Wikitext est Wikipedia lui même…
  • Installer un mirroir exact de Wikipedia en local est long fastidieux et assez mal documenté
  • La récupération des articles parsés directement par Wikipedia à 1 article par seconde ne pose pas de problème à Wikipedia, mais le processus est très long (1 article = 1 sec <=> 100000 articles = 1 jour)
  • Les articles doivent être filtrés car on a de tout, le meilleurs comme le pire

J’ai pu récupérer les données de Wikipedia de la manière suivante:

  1. Pré requis
    Serveur et outils MySQL
    Machine virtuelle java (le chemin de java.exe doit être dans la variable d’environnement PATH)
    L’utilitaire maison java WikiDump [ Sources - Version compilée ] (dans la version compilée un batch est fourni pour lancer une démo, l’exception qui apparait est voulue, elle n’interrompt pas le traitement )
  2. Se rendre sur le site http://dumps.wikimedia.org/frwiki/latest/ afin de télécharger les dumps MySQL:
    frwiki-latest-category.sql.gz
    : Contenant la liste des catégories et le nombre d’articles qui s’y ratache.
    frwiki-latest-categorylinks.sql.gz
    : Permettant de faire le lien entre les articles et les catégories.
  3. Importer les scripts dans une base de données MySQL (sous Windows j’ai utilisé MySQL Administrator).
    Note: Le processus est relativement long (3 heures environ).
  4. Filtrer les articles voulus en lançant la requête suivante afin de récupérer les catégories Portail : SELECT * FROM category WHERE cat_pages > 0 AND cat_title LIKE “Portail:%” ORDER BY cat_pages DESC

    Une fois les portails choisis (A prendre et A refuser), il est nécessaire d’en sélectionner les articles. Lancer la requête suivante en mettant votre propre liste de portails:

    SELECT cl_data_in.cl_from FROM ((SELECT DISTINCT cl3.cl_from FROM categorylinks AS cl3 WHERE cl3.cl_to IN ("Portail:APrendre1,Portail:APrendre2")) AS cl_data_in)
    WHERE cl_data_in.cl_from NOT IN (SELECT DISTINCT cl2.cl_from FROM categorylinks AS cl2 WHERE LOWER(cl2.cl_to LIKE "liste d%") OR cl2.cl_to IN ("Homonymie","Portail:ARefuser1"))

    Une fois la liste des articles récupérés les exporter dans un fichier csv ou autre, le format importe peu, l’essentiel est d’avoir un identifiant par ligne.

    Nettoyer le fichier en supprimant le nom de colonne et les ligne vides.

  5. Pour lancer la récupération du contenu, dans la ligne de commande, tapper:java -jar WikiDump.jar Chemin_fichier_articles Repertoire_de_destination > log.txt

Une fois le dernier (long) traitement effectué, vous avez pu récupérer une quantité de données au delà de vos espérances.

Par courtoisie, si vous effectuez un gros ramassage, n’oubliez pas de faire à un don à Wikipedia.

Java XPath cache query

June 26th, 2010

See post in french.

When we want to process multiple identical XPath query on XML document, in a optimized way, we have to compile only once XPath query and to store it to be able to use it later.

Le following singleton Java class manage in a simple way cached XPath query:


package com.devbypractice;

import java.util.HashMap;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class XPathProcessor
{
 // Singleton instance
 //
 protected static XPathProcessor _instance = new XPathProcessor();

 // XPath expressions hashmap cache
 //
 protected HashMap<String, XPathExpression> _hmXPathExpressions = new HashMap<String, XPathExpression>();

 // Xpath processing object
 //
 XPath xPath = XPathFactory.newInstance().newXPath();

 /**
 * Private constructor
 */
 protected XPathProcessor() {}

 /**
 * @return Singleton instance
 */
 public static XPathProcessor getInstance()
 {
 return _instance;
 }

 /**
 * Get XPath compiled expression from cache
 *
 * @param sXpathExpress XPath string
 * @return XPath compiled expression
 * @throws Exception
 */
 protected  XPathExpression getExpression(String sXpathExpress) throws Exception
 {
 XPathExpression expression;
 if (_hmXPathExpressions.containsKey(sXpathExpress)) {
 expression = _hmXPathExpressions.get(sXpathExpress);
 }
 else {
 expression = xPath.compile(sXpathExpress);
 _hmXPathExpressions.put(sXpathExpress, expression);
 }

 return expression;
 }

 /**
 * Evaluate XPath expression on Xml with single result
 * @param sXpathExpress XPath string
 * @param xml XML node
 * @return Single result of XPath evaluation
 * @throws Exception
 */
 public String Evaluate(String sXpathExpress, Node xml) throws Exception
 {
 XPathExpression expr = getExpression(sXpathExpress);

 return expr.evaluate(xml);
 }

 /**
 * Evaluate XPath expression on Xml with multiple results
 * @param sXpathExpress XPath string
 * @param xml XML node
 * @return Multiple results of XPath evaluation
 * @throws Exception
 */
public NodeList EvaluateMutli(String sXpathExpress, Node xml) throws Exception
 {
 XPathExpression expr = getExpression(sXpathExpress);

 return (NodeList)expr.evaluate(xml, XPathConstants.NODESET);
 }

}

Usage:

  • XPathProcessor.getInstance().Evaluate(“/xpath1″, xmlNode1)
  • XPathProcessor.getInstance().EvaluateMulti(“/xpath2″, xmlNode2)

Cache de requêtes XPath Java

June 26th, 2010

Voir l’article en anglais.

Lorsque l’on veut lancer de multiples requêtes XPath identiques sur des documents XML, et ce de manière optimisée, il est nécessaire de ne compiler qu’une seule fois la requête XPath et de la stoker  afin de pouvoir la réutiliser plus tard.

La classe singleton Java suivante permet de gérer de manière simple un cache de requêtes XPath compilées:


package com.devbypractice;

import java.util.HashMap;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * Classe d'évaluation d'expressions XPath
 *
 */
public class XPathProcessor
{
 // Instance du singleton
 //
 protected static XPathProcessor _instance = new XPathProcessor();

 // Hashmap du cache d'expressions Xpath
 //
 protected HashMap<String, XPathExpression> _hmXPathExpressions = new HashMap<String, XPathExpression>();

 // Objet de traitement Xpath
 //
 XPath xPath = XPathFactory.newInstance().newXPath();

 /**
 * Constructeur privé
 */
 protected XPathProcessor() {}

 /**
 * @return L'instance du singleton
 */
 public static XPathProcessor getInstance()
 {
 return _instance;
 }

 /**
 * Récupération de l'expression XPath compilée à partir du cache
 *
 * @param sXpathExpress L'expression XPath sour forme de chaine
 * @return L'expression XPath compilée
 * @throws Exception
 */
 protected  XPathExpression getExpression(String sXpathExpress) throws Exception
 {
 XPathExpression expression;
 if (_hmXPathExpressions.containsKey(sXpathExpress)) {
 expression = _hmXPathExpressions.get(sXpathExpress);
 }
 else {
 expression = xPath.compile(sXpathExpress);
 _hmXPathExpressions.put(sXpathExpress, expression);
 }

 return expression;
 }

 /**
 * Evaluation d'une expression XPath sur un document XML
 * @param sXpathExpress Expression XPath sous forme de chaine
 * @param xml XML parsé
 * @return Le résultat unique de l'évaluation Xpath
 * @throws Exception
 */
 public String Evaluate(String sXpathExpress, Node xml) throws Exception
 {
 XPathExpression expr = getExpression(sXpathExpress);

 return expr.evaluate(xml);
 }

 /**
 * @param sXpathExpress Expression XPath sous forme de chaine
 * @param xml XML parsé
 * @return Le résultat multiple de l'évaluation Xpath
 * @throws Exception
 */
public NodeList EvaluateMutli(String sXpathExpress, Node xml) throws Exception
 {
 XPathExpression expr = getExpression(sXpathExpress);

 return (NodeList)expr.evaluate(xml, XPathConstants.NODESET);
 }

}

A l’utilisation, il suffit d’exécuter:

  • XPathProcessor.getInstance().Evaluate(“/xpath1″, xmlNode1) pour obtenir un résultat unique d’une requête XPath
  • XPathProcessor.getInstance().EvaluateMulti(“/xpath2″, xmlNode2) afin de récupérer de multiples résultats de l’exécution de la requête XPath.

Reusable Singleton class in Objective C for iPhone and iPad

June 26th, 2010

See post in french.

It is painfull to type the same code again and again when we want to create a new singleton class. To solve this, a reusable singleton class has been created in Objective C, once inherited, hardly all code needed by singleton is already implemented in the new class.

The following reusable singleton class allow to free singleton and provide thread safe methods.


//
//  NSSingleton.h
//  TCCALC
//  ©Jean-Luc Pedroni
//
//  Created by Jean-Luc Pedroni on 12/05/08.
//  Mail: jeanluc.pedroni AT free.fr
//
//  Code provided as is, without warranty.
//  You can use it as you wish,
//  but please keep this header.
//

#import <Foundation/NSObject.h>

@interface NSSingleton : NSObject {
}

+(void)cleanup;
// Deux cas:
// . [NSSingleton cleanup]  : Free all singletons.
// . [MySingleton cleanup] : Free MySingleton.

+(id)sharedInstance;
// Notes :
// . +(id)sharedInstance can be overridden to return the right type..
//   ex : (MySingleton *)sharedInstance { return [super sharedInstance]; }
// . Singleton initialization must be done as usual in -(id)init.
//
@end

Implementation:


//
//  NSSingleton.m
//  TimecodeCalculatorIPhone
//  ©Jean-Luc Pedroni
//
//  Created by Jean-Luc Pedroni on 12/05/08.
//  Mail: jeanluc.pedroni AT free.fr
//
//  Code provided as is, without warranty.
//  You can use it as you wish,
//  but please keep this header.
//

#import "NSSingleton.h"
#import <objc/runtime.h>

@implementation NSSingleton

typedef struct _Singleton {
 id   _class;
 id   _instance;
} Singleton;

#define  MAX_BLOC 5

static BOOL _fromInternalCall = NO; // In order to call [alloc] [dealloc] via NSSingelton.
static struct {
 int        n;
 int        capacity;
 Singleton *array;

} _singletonArray = {0};

static Singleton *FindSingleton(id class)
{
 Singleton *ptr = _singletonArray.array;
 Singleton *end = _singletonArray.array + _singletonArray.n;

 while (ptr < end) {
 if (class == ptr->_class) {
 return (ptr);
 }
 ptr++;
 }
 return (NULL);
}

static BOOL AddSingleton(id class, id instance)
{
 BOOL success = YES;

 if (_singletonArray.n >= _singletonArray.capacity) {
 Singleton *array = realloc(_singletonArray.array, (_singletonArray.capacity + MAX_BLOC) * sizeof(Singleton));

 if (array == NULL) success = NO;
 else {
 _singletonArray.array    = array;
 _singletonArray.capacity += MAX_BLOC;
 }
 }

 if (success == YES) {
 _singletonArray.array[_singletonArray.n]._class     = class;
 _singletonArray.array[_singletonArray.n]._instance = instance;
 _singletonArray.n++;
 }
 return (success);
}

+(void)cleanup
{
 @synchronized([NSSingleton class]) {
 _fromInternalCall = YES;

 // Call via 'NSSingelton' to free all singletons.
 //
 if ([self class] == [NSSingleton class]) {
 int i;

 for (i = 0; i < _singletonArray.n; i++) {
 [_singletonArray.array[i]._instance dealloc]; // Don't call [release] because [retainCount] is set to NSUIntegerMax so [dealloc] would never be called.
 }
 free(_singletonArray.array);
 memset(&_singletonArray, 0, sizeof(_singletonArray));
 }
 //
 // Call via inherited class, free class instance.
 //
 else {
 Singleton *singleton = FindSingleton(self);

 if (singleton != NULL) {
 [singleton->_instance dealloc];
 memmove(singleton, singleton + 1, ((_singletonArray.array + _singletonArray.n) - (singleton + 1)) * sizeof(_singletonArray.array[0]));
 _singletonArray.n--;
 if ((_singletonArray.capacity - _singletonArray.n) > MAX_BLOC) {
 _singletonArray.capacity -= MAX_BLOC;

 // Memory reduction, no failure corruption.
 //
 _singletonArray.array = realloc(_singletonArray.array, _singletonArray.capacity * sizeof(Singleton));

 #if DEBUG
 assert(_singletonArray.array != NULL);
 #endif
 }
 }
 }
 _fromInternalCall = NO;
 }
}

+(id)sharedInstance
{
 id sharedInstance = nil;

 @synchronized([NSSingleton class]) {
 if ([self class]  == [NSSingleton class]) [NSException raise:NSInternalInconsistencyException format:@"+[NSSingleton sharedInstance] - Abstract class instantiation -"];
 else {
 Singleton *singleton = FindSingleton(self);

 if (singleton != NULL) sharedInstance = singleton->_instance;
 else {
 _fromInternalCall = YES;
 id instance  = [super alloc];

 sharedInstance = [instance init];
 if (sharedInstance == nil) [instance dealloc];  // Don't call [release] because [retainCount] is set to NSUIntegerMax so [dealloc] would never be called.
 else if (AddSingleton(self, sharedInstance) == NO) {
 [sharedInstance dealloc];
 sharedInstance = nil;
 }
 _fromInternalCall = NO;
 }
 }
 }
 return (sharedInstance);
}

+(id)allocWithZone:(NSZone *)zone
{
 id instance = nil;

 @synchronized([NSSingleton class]) {
 if (_fromInternalCall == YES) instance = [super allocWithZone:zone];
 else                          [NSException raise:NSInternalInconsistencyException format:@"+[NSSingleton allocWithZone] - invalid call -"];
 }
 return (instance);
}

-(id)copyWithZone:(NSZone *)zone
{
 return (self);
}

-(id)retain
{
 return (self);
}

- (NSUInteger)retainCount
{
 return (NSUIntegerMax);  // l'objet ne peut être libéré.
}

-(void)release
{
 // Do nothing.
}

-(id)autorelease
{
 return (self);
}

-(void)dealloc
{
 // Thread safe dealloc in case of multi threading context
 //
 @synchronized([NSSingleton class]) {
 if (_fromInternalCall == YES) [super dealloc];
 else                          [NSException raise:NSInternalInconsistencyException format:@"-[NSSingleton dealloc] - invalid call -"];
 }
}
@end

Usage to create your own singleton class:

Interface:


//
//  MySingleton..h
//  test singleton
//
//  Created by Jean-Luc Pedroni on 12/05/08.
//  Mail: jeanluc.pedroni AT free.fr
//
//  You can use it as you wish,
//  but please keep this header.
//
#import <Cocoa/Cocoa.h>
#import "NSSingleton.h"

@interface MySingleton : NSSingleton
{
 @private
 int _value;
}

@property (nonatomic) int value;

+(MySingleton *)sharedMySingleton;
@end

Implementation:


//
//  MySingleton.m
//  test singleton
//
//  Created by Jean-Luc Pedroni on 12/05/08.
//  Mail: jeanluc.pedroni AT free.fr
//
//  You can use it as you wish,
//  but please keep this header.
//
#import "MySingleton.h"

@implementation MySingleton

@synthesize value = _value;

-(void)dealloc
{
 [super dealloc];

 //
 // MySingleton cleanup.
 //
}

-(id)init
{
 id _self_ = [super init];

 if (_self_ != nil) {
 self = _self_;
 _value = 1;
 }

 return (_self_);
}

+(MySingleton *)sharedMySingleton
{
 return (MySingleton *)[super sharedInstance];
}

@end

This code is a short part of iPhone application TCCalc created  by Jean-Luc Pedroni.

Singleton réutilisable en Objective C pour IPhone et IPad

June 26th, 2010

Voir l’article en anglais

Le design pattern singleton est celui qui est le plus couramment utilisé, il nécessite peu de ligne de code mais si l’application contient plusieurs singleton, il est fastidieux de retapper du code identique à peu de chose près. Ne supportant pas les templates C++ ou les types génériques Java ou C#, l’Objective C possède le typage dynamique qui nous permet de créer un singleton réutilisable.

La classe Objective C suivante permet, une fois héritée, de ne pas avoir à recoder le code spécifique du singleton et fournit  un mécanisme de libération du singleton.

L’interface de classe du singleton réutilisable:


//
//  NSSingleton.h
//  TCCALC
//
//  Created by Jean-Luc Pedroni on 12/05/08.
//  Mail: jeanluc.pedroni AT free.fr
//
//  Ce code peut être utilisé à votre convenance,
//  merci de garder cet en-tête.
//

#import <Foundation/NSObject.h>

@interface NSSingleton : NSObject {
}

+(void)cleanup;
// Deux cas:
// . [NSSingleton cleanup]  : Supprime tout les singletons.
// . [MonSingleton cleanup] : Supprime MonSingleton.

+(id)sharedInstance;
// Notes :
// . +(id)sharedInstance peut être redéfini pour que, par exemple, le type de retour corresponde au type du singleton.
//   ex : (MonSingleton *)sharedInstance { return [super sharedInstance]; }
// . L'initialisation du singleton doit être faite de la manière usuelle en objective C, dans -(id)init, lors de la seule et unique instanciation.
//
@end

L’implémentation


//
//  NSSingleton.m
//  TimecodeCalculatorIPhone
//
//  Created by Jean-Luc Pedroni on 12/05/08.
//  Mail: jeanluc.pedroni AT free.fr
//
//  Ce code peut être utilisé à votre convenance,
//  merci de garder cet en-tête.
//

#import "NSSingleton.h"
#import <objc/runtime.h>

@implementation NSSingleton

typedef struct _Singleton {
 id   _class;
 id   _instance;
} Singleton;

#define  MAX_BLOC 5

static BOOL _fromInternalCall = NO; // Permet d'accéder aux fonctions [alloc] [dealloc] via NSSingleton.
static struct {
 int        n;
 int        capacity;
 Singleton *array;

} _singletonArray = {0};

static Singleton *FindSingleton(id class)
{
 Singleton *ptr = _singletonArray.array;
 Singleton *end = _singletonArray.array + _singletonArray.n;

 while (ptr < end) {
 if (class == ptr->_class) {
 return (ptr);
 }
 ptr++;
 }
 return (NULL);
}

static BOOL AddSingleton(id class, id instance)
{
 BOOL success = YES;

 if (_singletonArray.n >= _singletonArray.capacity) {
 Singleton *array = realloc(_singletonArray.array, (_singletonArray.capacity + MAX_BLOC) * sizeof(Singleton));

 if (array == NULL) success = NO;
 else {
 _singletonArray.array    = array;
 _singletonArray.capacity += MAX_BLOC;
 }
 }

 if (success == YES) {
 _singletonArray.array[_singletonArray.n]._class     = class;
 _singletonArray.array[_singletonArray.n]._instance = instance;
 _singletonArray.n++;
 }
 return (success);
}

+(void)cleanup
{
 @synchronized([NSSingleton class]) {
 _fromInternalCall = YES;

 // Appel par la classe 'NSSingleton' supprimer tout les singletons.
 //
 if ([self class] == [NSSingleton class]) {
 int i;

 for (i = 0; i > _singletonArray.n; i++) {
 [_singletonArray.array[i]._instance dealloc]; // Ne pas faire [release] car le [retainCount] est à NSUIntegerMax et donc le [dealloc] ne serait jamais appelé.
 }
 free(_singletonArray.array);
 memset(&amp;_singletonArray, 0, sizeof(_singletonArray));
 }
 //
 // Appel via une classe dérivée de NSSingleton, supprimer l'instance de cette classe.
 //
 else {
 Singleton *singleton = FindSingleton(self);

 if (singleton != NULL) {
 [singleton->_instance dealloc];
 memmove(singleton, singleton + 1, ((_singletonArray.array + _singletonArray.n) - (singleton + 1)) * sizeof(_singletonArray.array[0]));
 _singletonArray.n--;
 if ((_singletonArray.capacity - _singletonArray.n) > MAX_BLOC) {
 _singletonArray.capacity -= MAX_BLOC;

 // Réduction de la mémoire, normalement aucun échec, sauf si corruption.
 //
 _singletonArray.array = realloc(_singletonArray.array, _singletonArray.capacity * sizeof(Singleton));

 #if DEBUG
 assert(_singletonArray.array != NULL);
 #endif
 }
 }
 }
 _fromInternalCall = NO;
 }
}

+(id)sharedInstance
{
 id sharedInstance = nil;

 @synchronized([NSSingleton class]) {
 if ([self class]  == [NSSingleton class]) [NSException raise:NSInternalInconsistencyException format:@"+[NSSingleton sharedInstance] - Abstract class instantiation -"];
 else {
 Singleton *singleton = FindSingleton(self);

 if (singleton != NULL) sharedInstance = singleton->_instance;
 else {
 _fromInternalCall = YES;
 id instance  = [super alloc];

 sharedInstance = [instance init];
 if (sharedInstance == nil) [instance dealloc];  // Ne pas faire [release] car le [retainCount] est à NSUIntegerMax et donc le [dealloc] ne serait jamais appelé.
 else if (AddSingleton(self, sharedInstance) == NO) {
 [sharedInstance dealloc];
 sharedInstance = nil;
 }
 _fromInternalCall = NO;
 }
 }
 }
 return (sharedInstance);
}

+(id)allocWithZone:(NSZone *)zone
{
 id instance = nil;

 @synchronized([NSSingleton class]) {
 if (_fromInternalCall == YES) instance = [super allocWithZone:zone];
 else                          [NSException raise:NSInternalInconsistencyException format:@"+[NSSingleton allocWithZone] - invalid call -"];
 }
 return (instance);
}

-(id)copyWithZone:(NSZone *)zone
{
 return (self);
}

-(id)retain
{
 return (self);
}

- (NSUInteger)retainCount
{
 return (NSUIntegerMax);  // l'objet ne peut être libéré.
}

-(void)release
{
 // ne fait rien.
}

-(id)autorelease
{
 return (self);
}

-(void)dealloc
{
 // Synchroniser l'appel au cas ou au moment du l'appel
 // du déalloc avec '_fromInternalCall' à VRAI, un
 // un autre thread, appel ce dealloc.
 // Peut probable, mais mieux vaut se prémunir.
 //
 @synchronized([NSSingleton class]) {
 if (_fromInternalCall == YES) [super dealloc];
 else                          [NSException raise:NSInternalInconsistencyException format:@"-[NSSingleton dealloc] - invalid call -"];
 }
}
@end

Afin donner les propriétés de singleton à votre classe, il suffit d’hériter de NSSingleton et de redéfinir sharedInstance afin de ne pas à avoir à caster le type de retour à chaque appel:

Interface:


//
//  MySingleton..h
//  test singleton
//
//  Created by Jean-Luc Pedroni on 12/05/08.
//  Mail: jeanluc.pedroni AT free.fr
//
//  Ce code peut être utilisé à votre convenance,
//  merci de garder cet en-tête.
//

#import <Cocoa/Cocoa.h>
#import "NSSingleton.h"

@interface MySingleton1 : NSSingleton
{
 @private
 int _value;
}

@property (nonatomic) int value;

+(MySingleton1 *)sharedMySingleton1;
@end

Implémentation:


//
//  MySingleton.m
//  test singleton
//
//  Created by Jean-Luc Pedroni on 12/05/08.
//  Mail: jeanluc.pedroni AT free.fr
//
//  Ce code peut être utilisé à votre convenance,
//  merci de garder cet en-tête.
//
#import <MySingleton.h>

@implementation MySingleton1

@synthesize value = _value;

-(void)dealloc
{
 [super dealloc];

 //
 // faire ici le ménage nécessaire pour MySingleton1.
 //
}

-(id)init
{
 id _self_ = [super init];

 if (_self_ != nil) {
 self = _self_;
 _value = 1;
 }

 return (_self_);
}

+(MySingleton1 *)sharedMySingleton1
{
 return (MySingleton1 *)[super sharedInstance];
}

@end

Ce code est extrait de l’application TCCalc réalisée par Jean-Luc Pedroni.

PHP multi database management

May 30th, 2010

In some projects, we sometimes need to make them work on different database engine (MySQL, MS SQL, Oracle). These databases are mostly common but have some specific behaviour. Something great would have been to be able to switch from a database engine to antother without modifying every request method.

In the following code, once database primitives’ methods are set up, a simple change in a config file allow to switch from a database to another.

Object polymorphism is used here so you must understand object programming in PHP 5.

The following class provide abstract methode which will have to be overriden for ever databse engine, it also provide common method of database engines. The main goal is not to code for common database query but to be able to specialize if not.


<?php
/**
 * Generic connection class
 */

require_once("config.php");

abstract class Connection
{
 protected $_hConnection = null;

 /*****************************************************
 * Specific method to override
 *****************************************************/

 protected abstract function FullConnect($sHost, $sUsername, $sPassword, $sDbName);
 public abstract function ExecQuery($sQuery);
 public abstract function FetchArrayResultInArray($result);
 public abstract function FetchSingleResultInArray($result, $nRowIndex=0);
 public abstract function GetLastError();

 /*****************************************************
 * Date management methode to override
 *****************************************************/

 public abstract function DateComparison($sDateFieldName, $sDate);
 public abstract function SystemDateFormat($sDateFieldName);
 public abstract function FrenchDateFormat($sDateFieldName);

 /*****************************************************
 * Common database method
 *****************************************************/

 public function Connect()
 {
 $this->FullConnect(DB_HOST, DB_USERNAME, DB_PASWORD, DB_NAME);
 }

 public function DieOnLastError()
 {
 $sLastError = $this->GetLastError();
 die ($sLastError);
 }

 /*****************************************************
 * Common business method
 *****************************************************/

 public function getUsers()
 {
 $sQuery = "SELECT * FROM `user`";

 $result = $this->ExecQuery($sQuery);

 $aUsers = $this->FetchArrayResultInArray($result);

 return $aUsers;
 }

 /*****************************************************
 *Specific business method to override
 *****************************************************/

 public abstract function getFirstUser();

}

?>

MySQL specific:


<?php

/**
 * MYSQL Specific
 */

require_once("Connection.class.php");

class MYSQLConnection extends Connection{
 protected function FullConnect($sHost, $sUsername, $sPassword, $sDbName)
 {
 if ($this->_hConnection != null)
 return true;

 //Connection
 $this->_hConnection = mysql_connect($sHost, $sUsername, $sPassword);
 if ($this->_hConnection == null)
 $this->DieOnLastError();

 //db selection
 $bSuccess = mysql_select_db($sDbName, $this->_hConnection);

 if(!$bSuccess)
 $this->DieOnLastError();

 return $bSuccess;

 }

 public function ExecQuery($sQuery)
 {
 if ($this->_hConnection == null)
 return null;

 //Exec query
 $result = mysql_query($sQuery, $this->_hConnection);

 if ($result == null)
 $this->DieOnLastError();

 return $result;
 }

 public function FetchArrayResultInArray($result)
 {
 $aRows = array();

 while($aRow = mysql_fetch_array($result))
 {
 array_push($aRows, $aRow);
 }

 return $aRows;

 }

 public function FetchSingleResultInArray($result, $nRowIndex=0)
 {
 $aRows = array();

 while($aRow = mysql_fetch_array($result))
 {
 if (count($aRow)>$nRowIndex)
 array_push($aRows, $aRow[$nRowIndex]);
 }

 return $aRows;
 }

 public function DateComparison($sDateFieldName, $sDate)
 {
 $sFormat = "%s = '%s'";
 $sComparison = sprintf($sFormat, $sDateFieldName, $sDate);

 return $sComparison;
 }

 public function SystemDateFormat($sDateFieldName)
 {
 return $sDateFieldName;
 }

 public function FrenchDateFormat($sDateFieldName)
 {
 $sFormat = "date_format(%s, '%%e/%%m/%%Y')";

 $sDateFormat = sprintf($sFormat, $sDateFieldName);

 return $sDateFormat;
 }

 public function GetLastError()
 {
 return mysql_error();
 }

 /**
 * MySQL specific business method
 * Return first user
 * @return Array User
 */
 public function GetFirstUser()
 {
 $sQuery = "SELECT * FROM `user` LIMIT 1";

 $result = $this->ExecQuery($sQuery);

 $user = $this->FetchArrayResultInArray($result);

 return $user;
 }

}
?>

MS SQL specific:


<?php

/**
 * MS SQL Specific
 */

require_once("Connection.class.php");

class MSSQLConnection extends Connection
{
 protected function FullConnect($sHost, $sUsername, $sPassword, $sDbName)
 {
 if ($this->_hConnection != null)
 return true;

 //Connection
 $this->_hConnection = mssql_connect($sHost, $sUsername, $sPassword);
 if ($this->_hConnection == null)
 $this->DieOnLastError();

 //db delection
 $bSuccess = mssql_select_db($sDbName, $this->_hConnection);

 if(!$bSuccess)
 $this->DieOnLastError();

 return $bSuccess;

 }

 public function ExecQuery($sQuery)
 {
 if ($this->_hConnection == null)
 return null;

 //Exec query
 $result = mssql_query($sQuery, $this->_hConnection);

 if ($result == null)
 $this->DieOnLastError();

 return $result;
 }

 public function FetchArrayResultInArray($result)
 {
 $aRows = array();

 while($aRow = mssql_fetch_array($result))
 {
 array_push($aRows, $aRow);
 }

 return $aRows;

 }

 public function FetchSingleResultInArray($result, $nRowIndex=0)
 {
 $aRows = array();

 while($aRow = mssql_fetch_array($result))
 {
 if (count($aRow)>$nRowIndex)
 array_push($aRows, $aRow[$nRowIndex]);
 }

 return $aRows;
 }

 public function DateComparison($sDateFieldName, $sDate)
 {
 $sFormat = "%s = CONVERT(datetime,'%s',121)";
 $sComparison = sprintf($sFormat, $sDateFieldName, $sDate);

 return $sComparison;
 }

 public function SystemDateFormat($sDateFieldName)
 {
 $sFormat = "CONVERT(CHAR(10), %s, 121)";

 $sDateFormat = sprintf($sFormat, $sDateFieldName);

 return $sDateFormat;
 }

 public function FrenchDateFormat($sDateFieldName)
 {
 $sFormat = "CONVERT(CHAR(10), %1\$s, 103) AS %1\$s";

 $sDateFormat = sprintf($sFormat, $sDateFieldName);

 return $sDateFormat;
 }

 public function GetLastError()
 {
 return mssql_get_last_message();
 }

 /**
 * MS SQL Specific business method
 * Return fist user
 * @return Array User
 */
 public function GetFirstUser()
 {
 $sQuery = "SELECT TOP 1 * FROM `user`";

 $result = $this->ExecQuery($sQuery);

 $user = $this->FetchArrayResultInArray($result);

 return $user;
 }

}
?>

A factory class which create connection object depending to configuration:


<?php
/**
 * Conection factory depending confuguration
 */

require_once("config.php");

switch(TARGET_DB)
{
 case "MSSQL":
 require_once("MSSQLConnection.class.php");
 break;
 case "MYSQL":
 require_once("MYSQLConnection.class.php");
 break;
 default:
 die(TARGET_DB." not implemented<br>");
}

 class ConnectionFactory {
 public static function getConnection()
 {
 switch(TARGET_DB)
 {
 case "MSSQL":
 return new MSSQLConnection();
 case "MYSQL":
 return new MYSQLConnection();
 default:
 die(TARGET_DB." not implemented<br>");
 }
 }
 }
 ?>

Configuration file to switch beetween database engines and parameters:


<?php

 //Connection parameters
 define("TARGET_DB","MYSQL");

 define("DB_HOST","localhost");
 define("DB_USERNAME","root");
 define("DB_PASWORD","root");
 define("DB_NAME","sample");

?>

An example file whitch excute getUsers(), a business method common between data engines and defined in generic class Connection, and a specialized method called getFirstUser().


<?php

require_once('ConnectionFactory.class.php');

$connection = ConnectionFactory::getConnection();

$connection->connect();

$users = $connection->getUsers();
$firstUser= $connection->getFirstUser();

echo("Users:<br/>");
print_r($users);

echo("<br/><br/>First User:<br/>");
print_r($firstUser)

?>

To add new business methods, first you have to know if query syntax is common or specific:

  • Common: Add it to Connection class
  • Specific: Add it to specialized class (here MYSQLConnection et MSSQLConnection)

To add a new databse engine, you’ll have to create a new class which inherit from Connection, to override specifics methods and to add it in ConnectionFactory class.

Finally, you’ve just to change TARGET_BD and connection parameters from config file.

Click here do download full source code.

Multi bases de données en PHP

May 30th, 2010

Pour certains projets, il est parfois nécessaire qu’ils puissent fonctionner sur plusieurs types de base de données (MySQL, MS SQL, Oracle…). Ces bases de données ont à la fois de grandes similitudes ainsi que certaines spécificités. L’idéal serait qu’en passant d’une base de données à une autre, on ne soit pas obligé de modifier le code pour chaque requête exécutée.

Dans le code suivant, une fois que les  primitives d’accès aux types de bases de données sont configurées, un simple changement dans le fichier de configuration permet de passer d’un type de base donnée à un autre.

Le polymorphisme objet est utilisé ici, il est donc nécessaire au préalable de maîtriser les classes PHP 5.

La classe suivante fournit les méthodes abstraites qui devront être redéfinie par héritage pour chaque type de basse de données, elle comprend également les méthodes communes aux différentes base de données. L’intérêt est de ne pas coder les requêtes pour chaque type de base de données si elles sont identiques, mais de pouvoir les spécialiser dans le cas contraire.


<?php
/**
 * Objet de connection générique
 */

require_once("config.php");

abstract class Connection
{
 protected $_hConnection = null;

 /*****************************************************
 * Fonctions spécifiques aux bdd à redéfinir
 *****************************************************/

 protected abstract function FullConnect($sHost, $sUsername, $sPassword, $sDbName);
 public abstract function ExecQuery($sQuery);
 public abstract function FetchArrayResultInArray($result);
 public abstract function FetchSingleResultInArray($result, $nRowIndex=0);
 public abstract function GetLastError();

 /*****************************************************
 * Gestion des dates a redefinir
 *****************************************************/

 public abstract function DateComparison($sDateFieldName, $sDate);
 public abstract function SystemDateFormat($sDateFieldName);
 public abstract function FrenchDateFormat($sDateFieldName);

 /*****************************************************
 * Fonctions commmunes aux bdd
 *****************************************************/

 public function Connect()
 {
 $this->FullConnect(DB_HOST, DB_USERNAME, DB_PASWORD, DB_NAME);
 }

 public function DieOnLastError()
 {
 $sLastError = $this->GetLastError();
 die ($sLastError);
 }

 /*****************************************************
 * Fonctions de gestions communes aux bases de données
 *****************************************************/

 public function getUsers()
 {
 $sQuery = "SELECT * FROM `user`";

 $result = $this->ExecQuery($sQuery);

 $aUsers = $this->FetchArrayResultInArray($result);

 return $aUsers;
 }

 /*****************************************************
 * Fonctions de gestions spécifiques aux bases de données
 *****************************************************/

 public abstract function getFirstUser();

}

?>

Le dérivation de la class Connection pour MySQL:


<?php

/**
 * Description of MYSQLConnectionclass
 *
 */

require_once("Connection.class.php");

class MYSQLConnection extends Connection{
 protected function FullConnect($sHost, $sUsername, $sPassword, $sDbName)
 {
 //Si la connexion est déjà établie
 if ($this->_hConnection != null)
 return true;

 //Connexion
 $this->_hConnection = mysql_connect($sHost, $sUsername, $sPassword);
 if ($this->_hConnection == null)
 $this->DieOnLastError();

 //Sélection de la bdd
 $bSuccess = mysql_select_db($sDbName, $this->_hConnection);

 if(!$bSuccess)
 $this->DieOnLastError();

 return $bSuccess;

 }

 public function ExecQuery($sQuery)
 {
 //Si la connexion n'a pas été effectuée ou n'est pas valide
 if ($this->_hConnection == null)
 return null;

 //Exécution de la requête
 $result = mysql_query($sQuery, $this->_hConnection);

 if ($result == null)
 $this->DieOnLastError();

 return $result;
 }

 public function FetchArrayResultInArray($result)
 {
 $aRows = array();

 while($aRow = mysql_fetch_array($result))
 {
 array_push($aRows, $aRow);
 }

 return $aRows;

 }

 /**
 * Création d'une array de donné avec seulement une valeur de la row retournée
 * nRowIndex est la position de la donnée dans la row
 * @param <type> $result
 * @param <type> $nRowIndex
 * @return <type>
 */
 public function FetchSingleResultInArray($result, $nRowIndex=0)
 {
 $aRows = array();

 while($aRow = mysql_fetch_array($result))
 {
 if (count($aRow)>$nRowIndex)
 array_push($aRows, $aRow[$nRowIndex]);
 }

 return $aRows;
 }

 /**
 * Regles de comparaison dans la clause WHERE
 * @param <type> $sDateFieldName
 * @param <type> $sDate
 */
 public function DateComparison($sDateFieldName, $sDate)
 {
 $sFormat = "%s = '%s'";
 $sComparison = sprintf($sFormat, $sDateFieldName, $sDate);

 return $sComparison;
 }

 /**
 * Formattage des dates dans la clause SELECT pour obtenir AAAA-MM-JJ
 * @param <type> $sDateFieldName
 */
 public function SystemDateFormat($sDateFieldName)
 {
 return $sDateFieldName;
 }

 /**
 * Formattage des dates dans la clause SELECT pour obtenir JJ/MM/AAAA
 * @param string $sDateFieldName
 */
 public function FrenchDateFormat($sDateFieldName)
 {
 $sFormat = "date_format(%s, '%%e/%%m/%%Y')";

 $sDateFormat = sprintf($sFormat, $sDateFieldName);

 return $sDateFormat;
 }

 /**
 * Renvoit la dernière erreur du serveur
 * @return string LastErrorMessage
 */
 public function GetLastError()
 {
 return mysql_error();
 }

 /**
 * Spécialisation de la méthode pour MySQL
 * Retourne le premier user
 * @return Array L'utilisateur
 */
 public function GetFirstUser()
 {
 $sQuery = "SELECT * FROM `user` LIMIT 1";

 $result = $this->ExecQuery($sQuery);

 $user = $this->FetchArrayResultInArray($result);

 return $user;
 }

}
?>

La spécialisation MS SQL:


<?php

/**
 * Objet de connection MSSQL
 */

require_once("Connection.class.php");

class MSSQLConnection extends Connection
{
 protected function FullConnect($sHost, $sUsername, $sPassword, $sDbName)
 {
 //Si la connexion est déjà établie
 if ($this->_hConnection != null)
 return true;

 //Connexion
 $this->_hConnection = mssql_connect($sHost, $sUsername, $sPassword);
 if ($this->_hConnection == null)
 $this->DieOnLastError();

 //Sélection de la bdd
 $bSuccess = mssql_select_db($sDbName, $this->_hConnection);

 if(!$bSuccess)
 $this->DieOnLastError();

 return $bSuccess;

 }

 public function ExecQuery($sQuery)
 {
 //Si la connexion n'a pas été effectuée ou n'est pas valide
 if ($this->_hConnection == null)
 return null;

 //Exécution de la requête
 $result = mssql_query($sQuery, $this->_hConnection);

 if ($result == null)
 $this->DieOnLastError();

 return $result;
 }

 public function FetchArrayResultInArray($result)
 {
 $aRows = array();

 while($aRow = mssql_fetch_array($result))
 {
 array_push($aRows, $aRow);
 }

 return $aRows;

 }

 /**
 * Création d'une array de données avec seulement une valeur de la row retournée
 * nRowIndex est la position de la donnée dans la row
 * @param <type> $result
 * @param <type> $nRowIndex
 * @return <type>
 */
 public function FetchSingleResultInArray($result, $nRowIndex=0)
 {
 $aRows = array();

 while($aRow = mssql_fetch_array($result))
 {
 if (count($aRow)>$nRowIndex)
 array_push($aRows, $aRow[$nRowIndex]);
 }

 return $aRows;
 }

 /**
 * Regles de comparaison dans la clause WHERE
 * @param <type> $sDateFieldName
 * @param <type> $sDate
 */
 public function DateComparison($sDateFieldName, $sDate)
 {
 $sFormat = "%s = CONVERT(datetime,'%s',121)";
 $sComparison = sprintf($sFormat, $sDateFieldName, $sDate);

 return $sComparison;
 }

 /**
 * Formattage des dates dans la clause SELECT pour obtenir AAAA-MM-JJ
 * @param <type> $sDateFieldName
 */
 public function SystemDateFormat($sDateFieldName)
 {
 $sFormat = "CONVERT(CHAR(10), %s, 121)";

 $sDateFormat = sprintf($sFormat, $sDateFieldName);

 return $sDateFormat;
 }

 /**
 * Formattage des dates dans la clause SELECT pour obtenir JJ/MM/AAAA
 * @param string $sDateFieldName
 */
 public function FrenchDateFormat($sDateFieldName)
 {
 $sFormat = "CONVERT(CHAR(10), %1\$s, 103) AS %1\$s";

 $sDateFormat = sprintf($sFormat, $sDateFieldName);

 return $sDateFormat;
 }

 /**
 * Renvoit la dernière erreur du serveur
 * @return string LastErrorMessage
 */
 public function GetLastError()
 {
 return mssql_get_last_message();
 }

 /**
 * Spécialisation de la méthode pour MSSQL
 * Retourne le premier user
 * @return Array L'utilisateur
 */
 public function GetFirstUser()
 {
 $sQuery = "SELECT TOP 1 * FROM `user`";

 $result = $this->ExecQuery($sQuery);

 $user = $this->FetchArrayResultInArray($result);

 return $user;
 }

}
?>

Une classe factory permettant de créer l’objet de connexion pour le type de base de données cible selon la configuration:


<?php
/**
 * Gérateur de connection en fonction du type de base de données configuré
 */

require_once("config.php");

switch(TARGET_BD)
{
 case "MSSQL":
 require_once("MSSQLConnection.class.php");
 break;
 case "MYSQL":
 require_once("MYSQLConnection.class.php");
 break;
 default:
 die(TARGET_BD." pas encore implemente<br>");
}

 class ConnectionFactory {
 public static function getConnection()
 {
 switch(TARGET_BD)
 {
 case "MSSQL":
 return new MSSQLConnection();
 case "MYSQL":
 return new MYSQLConnection();
 default:
 die(TARGET_BD." pas encore implemente<br>");
 }
 }
 }
 ?>

Le fichier de configuration permettant de cibler le type de base de donnée et les paramètres de connexion:


<?php

 //Paramètres de connexion à la bdd
 define("TARGET_BD","MYSQL");

 define("DB_HOST","localhost");
 define("DB_USERNAME","root");
 define("DB_PASWORD","pwd");
 define("DB_NAME","sample");

?>

Un exemple d’utilisation qui permet d’exécuter la méthode getUsers() dont la syntaxe de la requête est commune aux différents type de bases de données et est définie dans la classe générique Connection.
La méthode getFirstUser() illustre une méthode qui est spécialisée par base de données.
On remarque qu’à l’utilisation rien de diffère l’une de l’autre grâce au mécanisme de polymorphisme:


<?php

require_once('ConnectionFactory.class.php');

$connection = ConnectionFactory::getConnection();

$connection->connect();

$users = $connection->getUsers();
$firstUser= $connection->getFirstUser();

echo("Users:<br/>");
print_r($users);

echo("<br/><br/>First User:<br/>");
print_r($firstUser)

?>

Pour ajouter de nouvelles méthodes d’accès à la base de données, il suffit de déterminer si la syntaxe de la requête est commune ou spécique à un moteur de bdd.

  • Si elle commune, l’ajouter dans la classe Connection
  • Si elle est spécifique, l’ajouter dans les fichiers spéciques (ici MYSQLConnection et MSSQLConnection)

Pour ajouter un nouveau moteur de base de données, il est nécessaire de créer une classe héritant de Connection, d’implémenter les méthodes spécifique (connexion, fetch, gestion des dates etc…), puis de modifier ConnectionFactory pour le prendre en compte.

Pour finir, il suffit de modifier TARGET_BD du fichier de config ainsi que les parmètres de connexion pour simplement passer d’un type de base de données à l’autre.

Cliquer ici pour télécharger les sources complètes.