Rev 1451 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed
<?php/*** Le web service observations récupère toutes les information pour une observation:* images, votes sur image et protocole, commentaires, votes sur commentaires, ...** @category php 5.2* @author Raphaël Droz <raphael@tela-botanica.org>* @copyright Copyright (c) 2013, Tela Botanica (accueil@tela-botanica.org)* @license http://www.cecill.info/licences/Licence_CeCILL_V2-fr.txt Licence CECILL* @license http://www.gnu.org/licenses/gpl.html Licence GNU-GPL* @see http://www.tela-botanica.org/wikini/eflore/wakka.php?wiki=ApiIdentiplante01Observations** @config-depend: "url_image" (dans configurations/config_observations.ini)* ex: http://www.tela-botanica.org/appli:cel-img:%09dXL.jpg*/// http://localhost/del/services/0.1/observations/#id => une observation donnée et ses images, SANS LES propositions & nombre de commentairerequire_once(dirname(__FILE__) . '/../DelTk.php');class Observation {/* Map les champs MySQL vers les champs utilisés dans le JSON pour les clients pourchacune des différentes tables utilisées pour le chargement de résultats ci-dessous.- chargerObservation() (v_del_image)- chargerVotesImage() (del_image_vote, del_image_protocole)- chargerCommentaires() (del_commentaire_vote, del_commentaire).Si la valeur vaut 1, aucun alias ne sera défini (nom du champ d'origine), cf consulter() */static $mappings = array('observations' => array( // v_del_image"id_observation" => 1,"date_observation" => 1,"date_transmission" => 1,"famille" => "determination.famille","nom_sel" => "determination.ns","nom_sel_nn" => "determination.nn","nom_referentiel" => "determination.referentiel","nt" => "determination.nt","ce_zone_geo" => "id_zone_geo","zone_geo" => 1,"lieudit" => 1,"station" => 1,"milieu" => 1,"ce_utilisateur" => "auteur.id","mots_cles_texte" => "mots_cles_texte","commentaire" => 1),/* exclus car issus de la jointure annuaire:"nom" => "auteur.nom", // XXX: jointure annuaire"prenom" => "auteur.prenom", // XXX: jointure annuaire"courriel" => "observateur", // XXX: jointure annuaire *//* présents dans cel_obs mais exclus:ordre, nom_ret, nom_ret_nn, latitude, longitude, altitude, geodatumtransmission, date_creation, date_modificationabondance, certitude, phenologie, code_insee_calcule */'images' => array( // v_del_image"id_image" => 1,"hauteur" => 1,// "largeur" => 1, inutile semble-t-il"date_prise_de_vue" => "date"),/* présents dans cel_images mais exclus:i_commentaire, nom_original, publiable_eflore, i_mots_cles_texte, i_ordre,i_ce_utilisateur, i_prenom_utilisateur, i_nom_utilisateur, i_courriel_utilisateur */'protocoles' => array( // del_image_protocole"ce_protocole" => "protocole.id","id_protocole" => "protocole.id","intitule" => "protocole.intitule","descriptif" => "protocole.descriptif","tag" => "protocole.tag"),/* See desc del_commentaire_vote & desc del_image_vote;Les deux schémas sont similaires, à l'exception de ce_protocolespécifique à del_image_vote et ce_proposition => ce_image */'votes' => array( // del_image_vote et del_commentaire_vote"id_vote" => "vote.id","ce_proposition" => "proposition.id","ce_image" => "image.id","ce_utilisateur" => "auteur.id", // attention, conflit avec commentaire, cf ci-dessous"valeur" => "vote","date" => 1, // attention, conflit avec commentaire, cf ci-dessous// absents du JSON, et pourtant présents dans services/configurations/config_mapping_votes.ini// (nécessiterait une propre jointure sur del_utilisateur)/* "nom" => "auteur.nom","prenom" => "auteur.prenom","courriel" => "auteur.courriel" */),'commentaires' => array( // del_commentaire"id_commentaire" => 1,"ce_observation" => "observation","ce_proposition" => "proposition","ce_commentaire_parent" => "id_parent",// les deux alias suivants sont particuliers afin d'éviter un conflit d'alias// lors des jointures avec del_commentaire_vote ci-dessus// (cf cas particulier dans la boucle de chargerCommentaires())"ce_utilisateur" => "__auteur_com","date" => "__date_com","texte" => 1,"utilisateur_nom" => "auteur.nom","utilisateur_prenom" => "auteur.prenom","utilisateur_courriel" => "auteur.courriel","nom_sel" => 1,"nom_sel_nn" => 1,"nom_ret_nn" => 1,"nom_referentiel" => 1,"proposition_initiale" => 1),);private $conteneur;private $gestionBdd;private $bdd;public function __construct(Conteneur $conteneur = null) {$this->conteneur = $conteneur == null ? new Conteneur() : $conteneur;$this->conteneur->chargerConfiguration('config_votes.ini');$this->conteneur->chargerConfiguration('config_mapping_votes.ini');$this->conteneur->chargerConfiguration('config_mapping_commentaires.ini');$this->gestionBdd = $conteneur->getGestionBdd();$this->bdd = $this->gestionBdd->getBdd();}/*** Méthode principale de la classe.* Lance la récupération des images dans la base et les place dans un objet ResultatService* pour l'afficher.* @param array $ressources les ressources situées après l'url de base (ex : http://url/ressource1/ressource2)* @param array $parametres les paramètres situés après le ? dans l'url* */public function consulter($ressources, $parametres) {if (!$ressources || count($ressources) != 1 ) {throw new Exception("Le service observation accepte un unique identifiant d'observation", RestServeur::HTTP_CODE_ERREUR);}// initialise les mappings:// substitue les valeurs à 1 par le nom de la clef (pas d'alias de champ)array_walk(self::$mappings['observations'], create_function('&$val, $k', 'if($val==1) $val = $k;'));array_walk(self::$mappings['images'], create_function('&$val, $k', 'if($val==1) $val = $k;'));array_walk(self::$mappings['protocoles'], create_function('&$val, $k', 'if($val==1) $val = $k;'));array_walk(self::$mappings['votes'], create_function('&$val, $k', 'if($val==1) $val = $k;'));array_walk(self::$mappings['commentaires'], create_function('&$val, $k', 'if($val==1) $val = $k;'));// Gestion des configuration du script$idobs = $ressources[0];$protocole = isset($parametres['protocole']) && is_numeric($parametres['protocole']) ?intval($parametres['protocole']) : NULL;// 1) récupération de l'observation (et de ses images (v_del_image est une vue utilisant des INNER JOIN))$liaisons = self::chargerObservation($this->bdd, $idobs);if(!$liaisons) {header('HTTP/1.0 404 Not Found');// don't die (phpunit)throw(new Exception());}// 2) réassocie les images "à plat" à leur observation (merge)// TODO: appliquer le formattage dépendant de la configuration en fin de processus$observations = self::reformateObservationSimpleIndex($liaisons, $this->conteneur->getParametre('url_images'));// bien que dans notre cas il n'y ait qu'une seule observation, issue de plusieurs images// dans $liaisons, $observation est un tableau (cf reformateObservation).// Considérons la chose comme telle au cas où le webservice doivent demain demander une paire// d'observations (... convergence avec ListeObservations & DelTk)$observation = array_pop($observations);// 3) charge les données de votes et protocoles associés aux imagesif($observation['images']) {$votes = self::chargerVotesImage($this->bdd, $observation['images'], $protocole);// 3") merge/reformate les données retournéesself::mapVotesToImages($votes,$observation['images']);}// 4) charge les commentaires et les votes associés// modifie/créé $observation['commentaires']self::chargerCommentaires($this->bdd, $observation);// désindexe le tableau (tel qu'apparement attendu par les applis), c'est une exception// corriger l'appli cliente pour utiliser les index puis supprimer cette ligne$observation['images'] = array_values($observation['images']);// autre élément de post-processing: le ce_utilisateur de l'observation non-numeric...if(!is_numeric($observation['auteur.id'])) $observation['auteur.id'] = "0";if(!isset($observation['auteur.nom'])) $observation['auteur.nom'] = '[inconnu]';if(isset($parametres['justthrow'])) return $observation;// Mettre en forme le résultat et l'envoyer pour affichage$resultat = new ResultatService();$resultat->corps = $observation;return $resultat;}static function chargerObservation($db, $idobs) {// prenom_utilisateur, nom_utilisateur, courriel_utilisateur sont exclus du mapping// car nous utilisons une construction SQL élaborée pour eux (cf IFNULL ci-dessous)$obs_fields = DelTk::sqlFieldsToAlias(self::$mappings['observations'], NULL, 'dob');$image_fields = DelTk::sqlFieldsToAlias(self::$mappings['images'], NULL, 'dob');// champs de l'annuaire (del_utilisateur): id_utilisateur prenom, nom, courriel$annuaire_fields = implode(', ', array("IFNULL(du.prenom, prenom_utilisateur) AS `auteur.prenom`","IFNULL(du.nom, nom_utilisateur) AS `auteur.nom`","IFNULL(du.courriel, courriel_utilisateur) AS observateur"));return $db->recupererTous(sprintf('SELECT %s, %s, %s FROM v_del_image as dob'.' LEFT JOIN del_utilisateur du ON CAST(du.id_utilisateur AS CHAR) = CAST(dob.ce_utilisateur AS CHAR)'.' WHERE dob.id_observation = %d -- %s',$obs_fields, $image_fields, $annuaire_fields, $idobs, __FILE__ . ':' . __LINE__));}// Charger les images et leurs votes associésstatic function chargerVotesImage($db, $images, $protocole = NULL) {if(!$images) return NULL;$select = array('votes' =>array('id_vote', 'ce_image', 'ce_protocole', 'ce_utilisateur', 'valeur', 'date', /* del_image_vote */),'protocole' =>array('id_protocole', 'intitule', 'descriptif', 'tag' /* del_image_protocole */ ));$vote_fields = DelTk::sqlFieldsToAlias(self::$mappings['votes'], $select['votes'], 'v'); // "v": cf alias dans la requête$proto_fields = DelTk::sqlFieldsToAlias(self::$mappings['protocoles'], $select['protocole'], 'p');$where = array();$where[] = sprintf('v.ce_image IN (%s)',implode(',', array_values(array_map(create_function('$a', 'return $a["id_image"];'), $images))));if ($protocole) {$where[] = "v.ce_protocole = $protocole";}return $db->recupererTous(sprintf('SELECT %s, %s FROM del_image_vote AS v'.' INNER JOIN del_image_protocole p ON v.ce_protocole = p.id_protocole'.' WHERE %s -- %s',$vote_fields, $proto_fields,$where ? implode(' AND ', $where) : 1,__FILE__ . ':' . __LINE__));}/*** Formater une observation depuis une ligne liaison* @param $liaison liaison issue de la recherche* @return $observation l'observation mise en forme* Exemple: vote, au sortir de MySQL contient:* 'xxx' => 'blah', 'descriptif' => 'foo', 'valeur' => 3* et le tableau de mapping contient:* descriptif = protocole.descriptif, valeur = vote* Alors $retour[ contient:** */static function mapVotesToImages($votes, &$images) {if(!$votes) return;// pour chaque voteforeach ($votes as $vote) {$imgid = $vote['image.id'];$protoid = $vote['protocole.id'];// un vote sans image associée ? est-ce possible ?// if(!isset($images[$imgid])) continue;if(!array_key_exists('protocoles_votes', $images[$imgid]) ||!array_key_exists($protoid, $images[$imgid]['protocoles_votes'])) {// extrait les champs spécifique au protocole (le LEFT JOIN de chargerVotesImage les ramène en doublons$protocole = array_intersect_key($vote, array_flip(self::$mappings['protocoles']));$images[$imgid]['protocoles_votes'][$protoid] = $protocole;}$vote = array_intersect_key($vote, array_flip(self::$mappings['votes']));$images[$imgid]['protocoles_votes'][$protoid]['votes'][$vote['vote.id']] = $vote;}}// Charger les commentaires et leurs votes associésstatic function chargerCommentaires($db, &$observation) {$select = array('votes' =>array('id_vote', 'ce_proposition', 'ce_utilisateur', 'valeur', 'date' /* del_commentaire_vote */),'commentaires' =>array('id_commentaire', 'ce_observation', 'ce_proposition', 'ce_commentaire_parent', 'texte','ce_utilisateur', 'utilisateur_prenom', 'utilisateur_nom', 'utilisateur_courriel','nom_sel', 'nom_sel_nn', 'nom_ret', 'nom_ret_nn', 'nt', 'famille', 'nom_referentiel', 'date','proposition_initiale'));$vote_fields = DelTk::sqlFieldsToAlias(self::$mappings['votes'], $select['votes'], 'cv'); // "v": cf alias dans la requête$comment_fields = DelTk::sqlFieldsToAlias(self::$mappings['commentaires'], $select['commentaires'], 'dc');$commentaires = $db->recupererTous(sprintf("SELECT %s, %s FROM del_commentaire as dc".// LEFT JOIN optionnel, mais explicatif:// on ne récupère des infos de vote que pour les commentaires comportant un// nom_sel "valide"" LEFT JOIN del_commentaire_vote cv"." ON cv.ce_proposition = dc.id_commentaire AND dc.nom_sel != '' AND dc.nom_sel IS NOT NULL"." WHERE ce_observation = %d -- %s",$comment_fields, $vote_fields,$observation['id_observation'],__FILE__ . ':' . __LINE__));if(!$commentaires) return;// les commentaires réunifiées et dont les votes sont mergés$ret = array();foreach ($commentaires as $comment) {$commentid = $comment['id_commentaire'];$voteid = $comment['vote.id'];if(!array_key_exists($commentid, $ret)) {$comment_extract = array_intersect_key($comment, array_flip(self::$mappings['commentaires']));// cas particulier: conflit d'aliases avec del_commentaire_vote$comment_extract['auteur.id'] = $comment_extract['__auteur_com'];$comment_extract['date'] = $comment_extract['__date_com'];unset($comment_extract['__auteur_com'], $comment_extract['__date_com']);// toujours un éléments "votes", quand bien même il n'y en aurait pas$comment_extract['votes'] = array();$ret[$commentid] = $comment_extract;}if(!$comment['nom_sel'] || ! $voteid) continue;$vote = array_intersect_key($comment, array_flip(self::$mappings['votes']));$ret[$commentid]['votes'][$voteid] = $vote;}$observation['commentaires'] = $ret;}// cf ListeObservation::reformateObservation() et ListeImages2::reformateImagesDoubleIndex()// (trop de variétés de formatage, à unifier côté client pour unifier côté backend ...)static function reformateObservationSimpleIndex($obs, $url_pattern = '') {// XXX: cf Observation.php::consulter(), nous pourriouns ici// conserver les valeurs vides (pour les phptests notamment, ou non)$obs = array_map('array_filter', $obs);$obs_merged = array();foreach($obs as $o) {$id = $o['id_observation'];$image = array_intersect_key($o, array_flip(array('id_image', 'date', 'hauteur' , 'largeur', 'nom_original')));$image['binaire.href'] = sprintf($url_pattern, $image['id_image']);unset($o['id_image'], $o['date'], $o['hauteur'], $o['largeur'], $o['nom_original']);if(!isset($obs_merged[$id])) $obs_merged[$id] = $o;$obs_merged[$id]['images'][$image['id_image']] = $image;}return $obs_merged;}}