My Simple ORM
Présentation
msorm est un générateur de classes php
À partir d'un fichier xml qui décrit un modèle relationnel, msorm génère un ensemble de classe php qui représente ce modèle et ses relations. Cet ensemble d'objets php peut être copié dans un projet et est utilisable tel quel, sans qu'aucun autre outil ne soit necessaire.
En cela, il est différent des autres ORM qui nécessitent d'être intégrés au projet pour pouvoir fonctionner.
L'avantage est que le modèle généré est très léger, la contrepartie est que si le modèle évolue, il faut le re-générer, mais il existe des options qui rendent les choses très simples.
Le modèle généré peut être augmenté avec des méthodes supplémentaires que l'on peut écrire.
L'utlisation du modèle est très facile et rend le code des applications très lisible
msorm est organisé comme suit :
msorm/
├── msorm.php
├── generators/
│ ...
│ ...
└── projects/
├── myproject_1/
│ └── description.xml
└── myproject_2/
└── description.xml
Pour un projet donné, on doit donc créer un fichier description.xml (voir ici) et lancer en ligne de commande (le fichier msorm.php soit avoir les droits d'exécution):
$ ./msorm.php -g model -p myproject
Cette commande est la plus basique. Elle va prendre le fichier description.xml qui se trouve dans le sous-dossier myproject/ du dossier projects/, et créer au même niveau, un dossier model/ qui contient l'ensemble des fichiers php générés.
Il existe plusieurs options. Notamment :
- -g (ou --generate) : doit être suivie de
modelousql
En effet msorm peut générer un fichier sql (au même endroit que le fichier description.xml) qui permet de créer la base de données - -o (ou --output) : permet de spécifier un autre dossier de destination que le sous-dossier
model/ - -u (ou --update) : permet de ne pas écraser le code additionnel qui aurait été écrit
- -h (ou -? ou --help) : affiche une aide qui résume toutes les options
Un exemple
Le fichier description.xml
msorm est livré avec un projet sample qui est un peu un cas d'école des possibilités de l'outil.
Les différents types de relations y sont abordées.
<?xml version="1.0" encoding="UTF-8"?>
<database name="bookstore" phpNamespace="bookstore">
<table name="books" phpObjectName="Book">
<column name="id" type="integer" unsigned="true" autoIncrement="true" primaryKey="true" />
<column name="title" type="varchar" size="256" required="true" />
</table>
<table name="publishers" phpObjectName="Publisher">
<column name="id" type="integer" primaryKey="true" unsigned="true" autoIncrement="true" />
<column name="name" type="varchar" size="128" required="true" />
</table>
<table name="publications" phpObjectName="Publication" > <!-- Ne peut pas être une crossref table parce que le même editeur peut editer plusieurs fois le même livre avec des isbn différents -->
<column name="isbn" type="varchar" size="20" primaryKey="true" />
<column name="book_id" type="integer" unsigned="true" required="true"> <!-- Many to One : Une publication ne concerne qu'un seul livre. Un livre peut avoir plusieurs publications (plusieurs publications peuvent concerner le même livre -->
<reference table="books" column="id" deleteCascade="true" />
</column>
<column name="publisher_id" type="integer" unsigned="true" required="true"> <!-- Many to One : Une publication ne concerne qu'un seul editeur. Un éditeur peut avoir plusieurs publications -->
<reference table="publishers" column="id" deleteCascade="true" />
</column>
<column name="publication_date" type="date" />
<column name="publication_notes" type="varchar" size="200" />
</table>
<table name="contributors" phpObjectName="Contributor" >
<column name="id" type="integer" unsigned="true" autoIncrement="true" primaryKey="true" />
<column name="gender" type="enum" values="man,woman" />
<column name="first_name" type="varchar" size="128" required="true" />
<column name="last_name" type="varchar" size="128" required="true" />
</table>
<table name="contribution_types" phpObjectName="ContributionType" >
<column name="id" type="integer" primaryKey="true" unsigned="true" autoIncrement="true" />
<column name="name" type="varchar" size="128" required="true" />
<column name="description" type="varchar" size="256" />
</table>
<table name="contributions" phpObjectName="Contribution"> <!-- Ne peut pas être une crossref table parce que le même contributeur peut contribuer plusieurs fois au même livre avec des types de contributions différents (voir plus bas) -->
<column name="book_id" type="integer" unsigned="true" primaryKey="true"> <!-- Many to One : On peut avoir plusieurs contributions à un même livre. -->
<reference table="books" column="id" />
</column>
<column name="contributor_id" type="integer" unsigned="true" primaryKey="true"> <!-- Many to One : Un contributeur peut contribuer à plusieurs livres. -->
<reference table="contributors" column="id" />
</column>
<column name="contribution_type_id" type="integer" unsigned="true" primaryKey="true"> <!-- Est clé primaire pour pouvoir enregistrer plusieurs contributions d'un même contributeur au même ouvrage. -->
<reference table="contribution_types" column="id" />
</column>
</table>
<table name="covers" phpObjectName="Cover">
<column name="isbn" type="varchar" size="20" primaryKey="true"> <!-- One to One : Une publication n'a qu'une et une seule couverture. Ici filename aurait pu être ajouté à publication, mais c'est pour l'exemple. -->
<reference table="publications" column="isbn" deleteCascade="true" />
</column>
<column name="filename" type="varchar" size="100" required="true" />
</table>
<table name="distributors" phpObjectName="Distributor">
<column name="id" type="integer" unsigned="true" autoIncrement="true" primaryKey="true" />
<column name="name" type="varchar" size="128" required="true" unique="true"/>
<column name="country" type="varchar" size="256" />
<column name="address" type="varchar" size="256" />
<column name="zipcode" type="varchar" size="256" />
<column name="city" type="varchar" size="256" />
</table>
<table name="distributions" isCrossRef="true" phpExtraAttributesPrefix="distribution"> <!-- Many to Many -->
<column name="isbn" type="varchar" size="20" primaryKey="true">
<reference table="publications" column="isbn" deleteCascade="true" />
</column>
<column name="distributor_id" type="integer" unsigned="true" primaryKey="true">
<reference table="distributors" column="id" deleteCascade="true" />
</column>
<column name="price" type="float" />
</table>
</database>
Ce qui est généré
model/
└── bookstore/ --> reprend le phpNamespace indiqué
├── Book.php |
├── BooksManager.php |
├── Contribution.php |
├── ContributionType.php |
├── ContributionTypesManager.php |
├── ContributionsManager.php |
├── Contributor.php |
├── ContributorsManager.php |
├── Cover.php |--> ici les classes qu'on utlisera dans notre code
├── CoversManager.php | Ce sont des classes vides qui héritent de leur
├── Distributor.php | équivalent "core"
├── DistributorsManager.php |
├── Publication.php |
├── PublicationsManager.php |
├── Publisher.php |
├── PublishersManager.php |
├── RawDatasManager.php |
├── core/
│ ├── BookCore.php |
│ ├── BooksManagerCore.php |
│ ├── ContributionCore.php |--> ici les classes "core" qui contiennent le code
│ ├── ContributionTypeCore.php | utile pour manipuler nos objets
│ ├── ContributionTypesManagerCore.php |
│ ├── ContributionsManagerCore.php |
│ ├── ContributorCore.php |
│ ├── ContributorsManagerCore.php |
│ ├── CoverCore.php |
│ ├── CoversManagerCore.php |
│ ├── DatabaseConnectionProvider.php |
│ ├── DistributorCore.php |
│ ├── DistributorsManagerCore.php |
│ ├── MSORM.php |
│ ├── PublicationCore.php |
│ ├── PublicationsManagerCore.php |
│ ├── PublisherCore.php |
│ ├── PublishersManagerCore.php |
│ ├── RawDatasManagerCore.php |
│ └── __db_params.conf.php --> un fichier pour paramétrer la connexion à la base
└── exceptions/
├── BookException.php |
├── BooksManagerException.php |
├── ContributionException.php |
├── ContributionTypeException.php |
├── ContributionTypesManagerException.php |
├── ContributionsManagerException.php |
├── ContributorException.php |
├── ContributorsManagerException.php |
├── CoverException.php |--> les classes d'exceptions
├── CoversManagerException.php |
├── DatabaseConnectionProviderException.php |
├── DistributorException.php |
├── DistributorsManagerException.php |
├── PublicationException.php |
├── PublicationsManagerException.php |
├── PublisherException.php |
├── PublishersManagerException.php |
└── RawDatasManagerException.php |
- Chaque table définie dans le fichier xml donne deux classes sauf les tables crossRef
- Pour chaque table qui donne une classe, on définit l'attribut
phpObjectNamequi sera le nom de la classe- on peut aussi définir l'attribut
phpObjectPFNamesi le pluriel n'est pas formé en ajoutant simplement un "s". 'PF' est pour 'plural form'. Cette forme est utilisée pour générer le nom du manager et des méthodes qui servent à récupérer des attributs dans des relations "many".
- on peut aussi définir l'attribut
- On voit qu'il existe une classe supplémentaire
RawDatasManager. C'est une classe qui peut permettre de faire des manipulations directes sans passer par le coté objet.
Comment s'utilise ce modèle ?
Le principe est toujours le même, mais les classes générées contiennent un code et des attributs qui dépendent vraiment du fichier xml de définition.
Exemple :
$books_manager = bookstore\BooksManager::getInstance();
$books = $books_manager->getFilteredList(array('title' => 'My Book'));
$myBook = $books[0]; // on suppose qu'il y en a un et un seul
echo $myBook->getId()." : "$myBook->getTitle().PHP_EOL;
foreach($myBook->getContributions() as $contribution){
$contributor_id = $contribution->getContributorId();
$contributor = $contribution->getContributor();
$type = $contribution->getContributionType();
echo " ".$contributor->getLastName()." ".$contributor->getFisrtName()." (".$contributor_id.") : ".$type->getName().PHP_EOL;
}
$myBook->setTitle('My new title');
$books_manager->update($myBook); // écrit en base de données
Dans l'exemple ci-dessus on voit que pour chaque "objet" défini en xml, on a deux classes php : une classe "Object" et une classe "ObjectsManager".
- La première représente des objets correspondants aux lignes des tables, avec pour attributs, les colonnes (avec leurs getteurs et setteurs), mais aussi les objets issus des relations définies (reference)
- La deuxième est une classe qui permet de gérer ces objets : obtenir un ou plusieurs objets, en vérifier l'existence, en ajouter, en supprimer ou les mettre à jour.
Comme évoqué plus haut, ces deux classes héritent de leurs équivalents "core" qui contiennent tous les attributs et méthodes fournies par défaut.
Ces deux classes peuvent donc tout à fait être enrichies d'autres attributs ou méthodes qui vous manqueraient. Particulièrement la méthode getFilteredList() des managers est assez limitée dans ses possibilités de filtrage justement, et on peut avoir besoin d'écrire une méthode pour extraire certains objets spécifiques.
Les relations entre objets sont prises en comptes lors des ajouts, mises à jour ou suppressions, de sorte que plusieurs objets peuvent être créés, mis à jour ou supprimés en base de données en une seule instruction.
Dans cet exemple $book et $publisher vont être créés à la volée en base de données :
$publication_manager = bookstore\PublicationsManager::getInstance();
$book = new bookstore\Book();
$book->setTitle("My book");
$publisher = new \bookstore\Publisher();
$publisher->setName("Casterman");
$publication = new bookstore\Publication();
$publication->setIsbn("123456789");
$publication->setBook($book);
$publication->setPublisher($publisher);
try{
$publication_manager->add($publication);
}
catch(\Exception $e){
echo $e->getMessage();
}
Il en serait de même pour l'appel à la méthode update.
Si on continue l'exemple précédent, l'objet $cover serait ajouté à la volée :
$cover = new bookstore\Cover();
$cover->setFilename("123456789.jpg")
$publication = $publication_manager->getByIsbn("123456789");
$publication->setCover($cover);
try{
$publication_manager->update($publication);
}
catch(\Exception $e){
echo $e->getMessage();
}
Dans le fichier xml, les références aux tables books et publishers dans la table des publications sont définies avec l'attribut deleteCascade=true. Ainsi si on supprime un livre (idem un editeur), toutes les publications de ce livre (de cet éditeur) seraient supprimées.
$books_manager = bookstore\BooksManager::getInstance();
$books = $books_manager->getFilteredList(array('title' => 'My Book'));
$myBook = $books[0]; // on suppose qu'il y en a un et un seul
$books_manager->delete($myBook);