Classe ISO2709 pour le PHP

# Une classe PHP pour manipuler des enregistrements au format ISO2709

classe iso2709_record (public release 0.0.6)
par François Lemarchand. 2003 balno@users.sourceforge.net

En préambule, je voudrais dédier ce travail à Hervé Le Crosnier. Au début des années 90, j'ai eu la chance de suivre un stage d'initiation à HTML assuré par lui. Il est plus que probable qu'il ne s'en souvienne pas, mais depuis ce stage, quand je mesure le chemin parcouru, je ne peux que dire merci Hervé, un grand MERCI.

La classe PHP dont il est question ici se pose deux objectifs : Dans un premier temps, il s'agit de lire des enregistrements ISO2709 afin de récupérer des informations. Dans un second temps, et pour être complétement efficiente, la classe devrait idéalement nous permettre de constituer de nouveaux enregistrements respectant ce format.

## Rappel sur le format ISO2709

Le format d'échange ISO 2709 fournit une norme pour l'échange de données bibliographiques. Il offre une interface unifiée permettant la description d'enregistrements organisés en champs et sous-champs.

Ce format définit pour un enregistrement 3 zones :

- le guide
- le répertoire
- la zone des données

- Le guide contient des informations sur la nature de l'enregistrement : le type de notice, le genre de document décrit, le statut de la notice, etc. Il contient aussi diverses informations sur la structure ISO 2709 de l'enregistrement : la longueur de l'enregistrement, la position dans l'enregistrement où se trouvent les données, etc. Ce guide est constitué d'une série de 24 caractères qui se décomposent ainsi
  1. caractères 0 à 4 : la longueur de l'enregistrement
  2. caractère 5 : le statut de la notice bibliographique
  3. caractère 6 : le type de document
  4. caractère 7 : le type de la notice
  5. caractère 8 : le niveau hiérarchique de la notice
  6. caractère 9 : non-défini
  7. caractère 10 : la longueur des indicateurs (2 en UNIMARC)
  8. caractère 11 : la longueur du code de sous-zone (2 en UNIMARC)
  9. caractères 12 à 16 : position dans l'enregistrement du début de la zone de données
  10. caractère 17 : le niveau de catalogage
  11. caractère 18 : la forme du catalogage descriptif
  12. caractère 19 : non-défini
  13. caractère 20 : le nombre de caractères du répertoire indiquant la longueur d'un champ (4 en UNIMARC)
  14. caractère 21 : le nombre de caractères du répertoire indiquant la position d'un champ (5 en UNIMARC)
  15. caractère 22 : le "nombre de caractères de la partie relative à l'application de chaque article du répertoire" (Je ne sais pas trop à quoi cela sert, c'est toujours 0 en UNIMARC)
  16. caractère 23 : non-défini
- Le répertoire est une sorte de table des matières de l'enregistrement. Il contient pour chacun des champs de celui-ci le label du champ (sur 3 caractères), le nombre de caractères du champ (sur le nombre de caractères défini par la position 20 du guide) et la position du début du champ dans la zone de données (sur le nombre de caractères défini par la position 21 du guide). Prenons un exemple qui sera peut-être plus parlant. Pour le champ 001 d'un enregistrement qui compte 12 caractères on aura la mention suivante dans le répertoire : 001001200000. On peut décomposer cette mention ainsi : le label est sur trois caractères, c'est 001. La longueur du champ est sur 4 caractères : 0012. Enfin l'adresse du champ est sur 5 caractères : 00000. Celle-ci est fixée à 0 car c'est l'"offset", la position en nombre de caractères par rapport au début de la zone des données (le début de cette zone est défini dans les positions 12 à 16 du label). Pour accéder au contenu de ce champ, nous avons donc toutes les informations requises : sa longueur (second élément de son entrée dans la "table des matières"), son label, et l'endroit où il débute dans l'enregistrement (obtenu par l'addition de la position du début des données indiquée dans le guide et du troisième élément trouvé dans le répertoire).
- La zone des données contient donc l'ensemble des champs de l'enregistrement mis bout à bout suivant l'ordre défini dans le répertoire. Dans ces champs, des caractères spéciaux sont utilisés pour marquer le début des sous-champs et la fin du champ (ces caractères sont inclus dans la longueur indiquée par le répertoire). Certains champs commencent par des indicateurs définis dans la norme UNIMARC ou autre. La fin de l'enregistrement lui-même est marquée par un caractère spécial.

Comme on peut le voir, la structure d'un enregistrement au format ISO 2709 est assez simple une fois que l'on en a compris le principe. La manipulation de tels enregistrements se résume à des opérations élémentaires (décomposition de l'enregistrement, consultation de la "table des matières" pour accéder à un champ, etc.) La difficulté principale reste de maintenir la synchronisation entre les trois zones de l'enregistrement lors de l'insertion ou de la suppression de champs. La classe présentée ici à pour but d'accomplir ces accès et cette synchronisation de manière relativement transparente pour le programmeur.

## Propriétés

Comme toute classe PHP, la classe iso2709_record comprend des propriétés qui peuvent être accédées par votre code. Certaines de ces propriétés sont utilisées pour la gestion interne de l'objet défini dans la classe.

| propriété | type | description
--- | --- | --- | ---
enregistrement ISO2709 | full_record | string | enregistrement complet
guide | chaîne | portion 'guide'
directory | chaîne | portion 'répertoire'
data | chaîne | portion 'zone de données'
tableaux internes | inner_guide | tableau associatif | guide sous forme de tableau associatif structure: `$this->inner_guide = array( rl => 'longueur de l'enregistrement (int)', rs => 'état de la notice (1 car.)', dt => 'type de document (1 car.)', bl => 'type de la notice (1 car.)', hl => 'niveau hiérarchique (1 car.)', pos9 => 'indéfini. contient un espace', il => 'longueur indicateurs (int)', sl => 'longueur code sous-champ (int)', ba => 'adresse de base des données (int)', el => 'niveau de catalogage (1 car.)', ru => 'forme du catal. descriptif (1 car.)', pos19 => 'indéfini. contient un espace', dm1 => 'infos pour le répertoires (toujours 4)', dm2 => 'infos pour le répertoires (toujours 5)', dm3 => 'infos pour le répertoires (toujours 0)', pos23 => 'indéfini. contient un espace' );`
inner_directory | tableau multi-dimensionnel | tableau correspondant au répertoire de l'enregistrement structure: `$this->inner_guide[x] = array( label => 'label du champ de rang x (string)', length => 'longueur du champ de rang x', adress => 'offset du champ dans l'enregistrement' );`
inner_data | tableau multi-dimensionnel | tableau correspondant au répertoire de l'enregistrement structure: `$this->inner_data[x] = array( label => 'label du champ de rang x (string)', content => 'contenu du champ' );`
caractères spéciaux | record_end | chaîne | caractère de fin de notice (IS3 de l'ISO 6630)
field_end | chaîne | caractère de fin de champ (IS2 de l'ISO 6630)
subfield_begin | chaîne | caractère de début de sous-champ (IS1 de l'ISO 6630)
NSB_begin | chaîne | caractère débutant un NSB (Non Sorting Block)
NSB_end | chaîne | caractère de fin NSB (Non Sorting Block)
note Les propriétés décrivant des caractères spéciaux ont toutes dans cette classe un équivalent préfixé par rgx_ contenant les mêmes caractères sous une forme utilisable dans des expressions régulières compatibles PERL.
divers | update_mode | entier | mode de mise à jour de l'enregistrement
errors | tableau | tableau contenant les messages d'erreurs liés au format de l'enregistrement (méthode valid()) ou aux modifications effectuées sur celui-ci.

Dans l'état actuel des choses, la gestion des objets de PHP ne comprend aucune notion de propriétes ou méthodes publiques ou privées. Les propriétés de la classe sont donc toutes accessibles depuis n'importe quel endroit de votre code. Cependant, je ne recommande pas de modifier les propriétés préfixées par inner_ sans bien réfléchir avant. Ces variables sont en effet utilisées de manière interne dans la classe pour la gestion de l'enregistrement. On peut donc les modifier, mais sans garantie quant à la synchronisation de l'ensemble.

## Méthodes

### Le constructeur : iso2709_record

prototype : `iso2709_record([string record], [flag update_mode])`

Le constructeur est appelé lors de la création de la classe. Il admet deux paramètres :

- Un enregistrement ISO2709 sous la forme d'une chaîne. La chaîne passée en paramètre peut être vide, auquel cas un nouvel enregistrement sera créé.
- Un drapeau indiquant le mode de mise à jour de l'enregistrement. Ce drapeau peut prendre les valeurs AUTO_UPDATE ou USER_UPDATE, la valeur par défaut étant AUTO_UPDATE. Ce drapeau permet de régler le comportement de la classe lors de l'insertion ou de l'effacement de champs dans l'enregsitrement. Avec la valeur AUTO_UPDATE, les tableaux internes et le contenu des variables de l'enregistrement sont remis à jour à chaque insertion ou suppression, ce qui ralentit celles-ci. A contrario, avec la valeur fixée à USER_UPDATE, le script appelant doit gérer ces mises à jour par l'intermédiaire de la méthode update(). Pour clarifier ceci, on peut considérer que s'il s'agit de lire ou d'ajouter juste quelques champs, on peut laisser la valeur de ce paramètre à AUTO_UPDATE. Par contre, pour la création d'un enregistrement ou de nombreuses modifications, il est grandement préférable de retenir USER_UPDATE. Si cela est le cas, il ne faudra pas omettre l'appel de la méthode update() après les modifications, la gestion des mises à jour internes à la classe étant de la responsabilité du programmeur et non plus de celle-çi.

### Obtenir le contenu d'un sous-champ ou d'un champ : get_field

prototype : `get_subfield(string label [, mixed refs])`

La fonction get_subfield retourne le contenu du champ spécifié par le paramètres qui lui sont passés. L'appel de la méthode peut être effectué de différentes manières. Le paramètre label est l'étiquette du champ. Ce peut être une expression régulière (le point est alors un caractère "joker". ex : 7.0 indique les champs 700 et 710).

La valeur retournée par la fonction est un tableau dont la structure est définie par les paramètres passés (ref).

1. Si le seul paramètre fourni est label, la méthode retourne un tableau dont les éléments sont constitués par les contenus entiers des champs correspondant à label.
2. Si seule une étiquette de sous-champ est fournie en plus du label, le tableau retourné sera comme précédemment un tableau dont les éléments sont constitués par les contenus entiers des champs correspondant à label et à l'étiquette fournie.
3. Si plusieurs étiquettes de sous-champs sont fournies, le tableau retourné sera un tableau multi-dimensionnel dont les éléments sont des tableaux associatifs contenant les valeurs des sous-champs spécifiés.

exemples d'appels valides de get_subfield :

`$isbn = $myRecord->get_subfield('010')`
  retourne un tableau constitué par les contenus des champs 010 de l'enregistrement. Les indices de ce tableau sont numériques.
  ex.

        $isbn[0] = 'contenu du premier champ 010'
        $isbn[1] = 'contenu du second champ 010'
        ...

`$nom_auteur = $myRecord->get_subfield('7..', a)`
  retourne un tableau constitué par les contenus des sous-champs $a des champs commencant par 7 de l'enregistrement. Les indices de ce tableau sont numériques.
  ex.

        $nom_auteur[0] = 'contenu du sous-champ 700$a'
        $nom_auteur[1] = 'contenu du sous-champ 701$a'
        ...
        $nom_auteur[x] = 'contenu du dernier sous-champ 712$a'

`$nom_auteur = $myRecord->get_subfield('7..', a, b)`
  retourne un tableau indexé dont les élements sont le contenu des sous-champs $a et $b des champs commencant par 7 de l'enregistrement. Les indices de ce tableau sont numériques et chacun de ses éléments est un tableau associatif.
  ex.

        $nom_auteur[0][a] = 'contenu du sous-champ 700$a'
        $nom_auteur[0][b] = 'contenu du sous-champ 700$b'
        $nom_auteur[1][a] = 'contenu du sous-champ 701$a'
        ...
        $nom_auteur[x][a] = 'contenu du dernier sous-champ 712$a'
        $nom_auteur[x][b] = 'contenu du dernier sous-champ 712$b'

Avec ceci, on doit en principe pouvoir accéder n'importe quel sous-champ de l'enregistrement. Si vous avez des besoins plus complexes, rien ne vous empêche de 'parser' directement les propriétes de la classe (cf. section propriétés).

### Ajout d'un champ : add_field

prototype : `add_field(string label, string indicators, mixed field_content)`

Cette méthode permet l'ajout d'un champ à un enregistrement ISO2709. On peut l'appeler de différentes manières : le nombre de paramètres est variable. On doit fournir en premier lieu l'étiquette du champ et les éventuels indicateurs, ceux-çi pouvant être de longueur nulle si le champ n'a pas d'indicateurs.

Le passage du contenu du champ se fait par le paramètre field_content sous la forme d'un nombre variable de chaînes comprenant alternativement l'étiquette du sous-champ et le contenu du sous-champ. Si une seule chaîne est fournie, le champ est réputé n'avoir pas d'étiquette de sous-champ et contenir uniquement la chaîne fournie.

La seconde possibilité est de passer le contenu du champ sous la forme d'un tableau multi-dimensionnel dont chaque ligne est un array constitué par l'étiquette du sous-champ et le contenu du sous-champ.

exemples d'appels valides de add_field :

`$myRecord->add_field('001', '', '01-0002977')`
  ajoute un champ 001 ayant pour contenu 01-0002977 et ne comprenant pas d'indicateurs.
`$myRecord->add_field(200, '1 ', a, 'Minable le pingouin', f, 'Texte d'Helen Lester')`
  ajoute un champ 200 ayant un champ $a, un champ $f et dont le premier indicateur est 1.
`$monChamp[0] = array( a => 'Lester');
$monChamp[1] = array( b => 'Helen');
$myRecord->add_field(700, ' 1', $monChamp);`
  Passage des arguments par un tableau. On insére un champ 700 avec $a et $b avec un indicateur décrivant une entrée au nom de famille.

La méthode add_field() retourne TRUE en cas de succès et FALSE en cas d'échec (mauvais format pour les paramètres). Dans ce dernier cas, un message est ajouté au tableau des erreurs (cf. méthode show_errors).

### Suppression d'un champ : delete_field

prototype : `delete_field(string label)`

La méthode delete_field() est à utiliser pour supprimer un champ de l'enregistrement ISO2709. Son fonctionnement est relativement simple : on passe comme paramètre l'étiquette du champ à supprimer. Il est possible de supprimer plusieurs champs en un seul appel dans la mesure où le paramètre label peut être une expression régulière. En pratique, on peut considérer que le point est un caractère "joker".

exemples d'appels valides de delete_field :

`$myRecord->delete_field('001')`
  supprime le ou les champs 001.
`$myRecord->delete_field('71.')`
  supprime les champs de l'enregistrement dont les labels commencent par 71 (710, 711, 712...).
`$myRecord->delete_field('..2')`
  supprime les champs de l'enregistrement dont les labels finissent par 2 (012, 022, etc.). (c'est idiot, mais sait-on jamais...)

La méthode delete_field() retourne TRUE en cas de succès et FALSE en cas d'échec (mauvais format pour le label). Dans ce dernier cas, un message est ajouté au tableau des erreurs (cf. méthode show_errors).

### Gestion du guide : set_rs, set_dt, set_bl, set_hl, set_el, set_ru

prototypes :
`set_rs(string/char value)`
`set_dt(string/char value)`
`set_bl(string/char value)`
`set_hl(string/char value)`
`set_el(string/char value)`
`set_ru(string/char value)`

Cet ensemble de méthodes permet de positionner les valeurs non calculées du guide de l'enregistrement. Pour connaître le codage des valeurs concernées, je vous invite à vous reporter aux spécifications du format UNIMARC (ou autre).

Nomenclature des fonctions :

fonction | position concernée | élément | description
--- | --- | --- | ---
set_rs() | 5 | record status | Etat de la notice
set_dt() | 6 | document type | Type de document
set_bl() | 7 | bibliographic level | Type de la notice
set_hl() | 8 | hierarchical level | Niveau hiérarchique
set_el() | 17 | encoding level | Niveau de catalogage
set_ru() | 18 | record update | Forme du catalogage descriptif

L'appel de ces méthodes est on ne peut plus simple : on passe le code souhaité comme paramètre unique. Le guide (inner_guide) est modifié. Cependant, la mise à jour de l'enregistrement est toujours gérée suivant les modes USER_UPDATE et AUTO_UPDATE. Si la classe a été déclarée avec le mode USER_UPDATE, la modification ne sera effective qu'après l'appel de la méthode update().

### Mise à jour de l'enregistrement : update

prototype : `update(void)`

Cette méthode n'est à invoquer que si la classe a été créée avec le drapeau update_mode fixé à USER_UPDATE. Si ce n'est pas le cas, l'appel à cette méthode sera effectué à chaque opération d'insertion ou d'effacement effectuée sur l'enregistrement, ce qui rend superflue une mise à jour par le script appelant (cf. le constructeur : iso2709_record).

Le rôle de cette méthode est de synchroniser l'enregistrement courant avec sa représentation interne dans la classe. Les propriétés full_record, guide et directory sont mises à jour à partir du contenu des tableaux inner_data, inner_guide, inner_directory.

Cette méthode ne retourne rien.

### Vérification du format de l'enregistrement : valid

prototype : `valid(void)`

Cette méthode vérifie le format ISO2709 de l'enregistrement. Elle retournera TRUE si l'enregistrement courant est valide et FALSE dans le cas contraire. Attention Cette méthode valide le format ISO2709 uniquement. Aucune erreur de sera générée si votre enregistrement contient, par exemple, 2 champs 700, ce qui est interdit par le standard UNIMARC.

### Gestion des caractères accentués et autres : ISO_encode et ISO_decode

prototypes :
`ISO_encode(string chaine)`
`ISO_decode(string chaine)`

Ces deux méthodes assurent la conversion des caractères codés suivant le format ISO 5426 vers le format ISO 8859-1.

La fonction ISO_encode convertit le format ISO 8859-1 vers le format ISO 5426 et ISO_decode fait l'inverse.

Ces deux fonctions retournent la chaîne passée en paramètre mais convertie suivant le cas.

De mon point de vue, ces deux méthodes sont implémentées de manière 'partielle mais suffisante', ce qui veut dire que cette implémentation n'est pas complète dans la mesure où j'ai énormément de mal à trouver des informations suffisantes sur le format ISO 5426 et l'ISO 646. Pour l'anecdote, la fonction ISO_decode est adaptée d'un bout de code C posté sur le forum de l'application bien connue recode et la fonction ISO_encode en découle directement. Si des personnes avaient plus de documentation sur le sujet, je leur serais reconnaissant de m'en faire part. L'implémentation actuelle est a peu près satisfaisante pour les langues latines et romanes, mais incomplète pour l'allemand et les alphabets 'exotiques'.

## Un cas pratique

Voici un exemple d'utilisation très simple destiné à clarifier un peu tout ça. Il illustre la création d'un enregistrement ISO 2709 minimal. Les commentaires sont dans le script.

`<? // inclusion du fichier de la classe require('./iso2709.inc'); // on crée un nouvel objet vide $record = new iso2709_record('', AUTO_UPDATE); // on crée le champ 100 qui est obligatoire en UNIMARC $champ = date('Ymd').'d||||||||||||uy0frea03||||ba'; // ce champ est inséré dans l'enregistrement $record->add_field('100', '', a, champ); // on ajoute un champ 200 $resp[0] = array(a, "Travailler, moi ? jamais !"); $resp[1] = array(e, "l'abolition du travail"); $resp[2] = array(f, "Bob Black"); $resp[3] = array(g, "traduit de l'anglais par Julius Van Daal"); $record->add_field('200', '1 ', $resp); // on ajoute un champ 700 $record->add_field(700, ' 1', a, 'Black', b, 'Bob'); // champ 010 : l'ISBN $record->add_field('010', '', a, '2-84405-000-X'); // champ 210 : adresse bibliographique $record->add_field(210, '', a, '[s.l]', c, 'L'esprit frappeur', d, '1997'); // c'est fini, vous avez votre enregistrement. // affichage de l'enregistrement ISO 2709 : print $record->full_record; ?>`