Programmation PHP/Exemples/MiniCMS/Implémentation

Un livre de Wikilivres.

Nous avons besoin des fonctionnalités :

TODO

[1] Complétion des données
(none) Create
(done) Read
(done) Update
(done) Delete
[2] Gestion des users
[3] Complétion du frameset via templates
(done) injection point replacer
[4] Un webService pour les inputs&query distants
(none) common web services
[5] Parser de snippet
(do ) php snippet acter
(none) class writer & executer
(do ) single actions executer
[6] Vue xhtml fonctionnelle ajaxifiée
(done) xhtml template "nested"


NICE TO HAVE

[1.1] Versionning sur données
(todo) new crop creating passing current to old


Le but étant de fournir un maximum de code de service pour les héritages futur le métier n'est approché qu'en vue de personnaliser le CMS client.


Concernant l'approche modulaire :


La segmentation en composantes (classes ou scriptlets) permets un découpage net et précis des différentes parties du projets développable séparément les unes des autres. L'association au sein d'un IHM ou couche de présentation via contrôleur, venant :

  • avant (pour orienter le développement)
  • pendant (pour stabiliser le développement)
  • après (pour finaliser le développement)


L'interface utilisatrice devant tirer parti des composantes et non l'inverse. La richesse des fonctionnalités est dégagée par le service et enclavé par un code orienté solution client. Il va donc de soi de privilégier proprement, pour enrichir ses propres librairies, de chercher le plus possible à développer ses classes et composantes plutôt que de perdre son temps en métier.

Concevoir du code de haute qualité signifie souvent d'optimiser le code vers une solution à logique 'customer' càd spécialisée, ce qui n'est pas recherché lorsque l'on doit se développer de l'expérience sur le terrain. Au possible, il faut adapté sa modularité et péricliter le service en business pour le client.

Concevoir du code modulaire permets de développer concrètement sans trop d'efforts d'analyse pour les développement XP et sur un grand délais. Il est utile d'avoir conscience de ces deux approches sur le terrain ou lorsque l'on décide d'ouvrir son code au monde. La majorité des développeur ne faisant trop souvent qu'intégrer pour l'argent ou la renommée chez le client le fruit du travail des autres.

'Classes & Fermes de fonctions' contre 'scriptlets'

Les premières sont :

  • plus simple à coder,
  • plus simple à utiliser,
  • plus simple à entretenir,
  • plus simple à spécialiser,
  • plus facilement réutilisable,
  • plus orienté objet,
  • plus susceptible d'être récupéré.

Les secondes sont :

  • plus cryptiques,
  • plus optimisée pour le client,
  • aisément traductible en classes et fermes,
  • plus fonctionnelle,
  • plus procédurales,
  • plus chaotiques[1].
  1. produire un code obscure voir chaotique présente de nombreux avantages. D'autres analystes et collègues y perdront leur temps d'une part, les piping inhérents au code permettant l'implémentation, le hacking, le reroutage et la divergence fonctionnelle de script. Ce qui n'est pas sans avantages.

[1] Complétion des données[modifier | modifier le wikicode]

Pour disposer des fonctionnalités de gestion de données.

  • On a besoin de Getter/Setter de données :
- depuis la vue
- depuis le webservice
- depuis l'URL
  • Pour les fichiers xml


Les fonctionnalités d'appels sont regroupées ci-dessous

<?php

class dataManager
{//
	private $pattern;

	protected $stack, $domDoc;
	//
	public static 
	
		$crop
				= array(
					"std"=> '<?xml version="1.0" encoding="UTF-8"?>
					<crop><topic /><fields /></crop>'
					,""=>''
				),
				
		$data
				= array(
					"root"=>"/miniCMS/Owners/"
				);
				
# TODO GETTER/SETTER	
	
	
	public static function getContentByData($pData)
	/**
	 * get the content from data
	 */
	{}
	
	public static function setContentByData($pData)
	/**
	 * set the content to data
	 */
	{}
	
	public function setCropForCustomer($pPath, $pCrop)
	/**
	 * set crop to customer directory
	 */
	{
		file_put_contents($pPath, $pCrop);
	}
	
# STATE
	
	public function dataManager()
	{
		$this->initialize();			
	}
	
	public function initialize()
	/**
	 * init
	 */
	{
		$this->domDoc = new DOMDocument();
		if(@$this->data['path'])
		{
			$this->domDoc->load( $this->data['path'] );
		}
		
	return $this->domDoc;
	}
	
	public function saveDocument()
	/**
	 * save document
	 */
	{
		$this->domDoc->save($this->data['path']) ;
	}
}

?>


On implémente ces fonctions utilitaires à utilities.class.php

	public static function searchNodesByContent($pDocument, $pQueries)
	/**
	 * return node matching request
	 */
	{
		
		$_fields = $pDocument->getElementsByTagName('fields');

		/* ceci est un exemple de mauvais code de service, 
                   il ne fonctionne que pour un nombre limité de structure de crop
		   c'est donc un code métier à surcharger. */
		foreach ($_fields->item(0)->childNodes as $u)
		{
			if ($u->nodeType != XML_TEXT_NODE) 
				foreach ( $u->childNodes as $v  )
				{
					if( $v->nodeName == $pQueries['node'] 
                                             && ! utilities::isContent ( $pQueries['value'] 
                                             , $v->nodeValue ) )
					
                                              $_fields->item(0)->removeChild($u) ;
				}
		}
		
		/* OBSOLÈTE - Malheureusement, ce code ne marche pas une fois appelée par getNodes 
		for($i=0; $i<count($pNodeSet); $i++)
		{
			$_content = $pNodeSet[$i]->childNodes ;
			
			// cette fonction ne descend qu'à un niveau 
			foreach($_content as $v)
			{
				if( $v->nodeName == $pQueries['node'] 
                                     && ! utilities::isContent ( $pQueries['value'] 
                                     , $v->nodeValue ) )
				{
					$pNodeSet[$i]->parentNode->removeChild($pNodeSet[$i]) ;
				}
			}
		}
		*/		

	return $pDocument ;
	}

	public static function searchNodesByAttribute($pDocument, $pQueries)
	/**
	 * return node matching request by attribute
	 */
	{
		
		$_fields = $pDocument->getElementsByTagName('fields');
		
		foreach ($_fields->item(0)->childNodes as $u)
		{
			if ($u->nodeType != XML_TEXT_NODE)
				if( !($u->getAttribute($pQueries['attribute']) 
                                         == $pQueries['value'])  ) // 1:1 match
				      $_fields->item(0)->removeChild($u) ;
		}

	return $pDocument ;
	}


On implémente la méthode getContentByData du dataManager

	public static function getContentByData($pDocument, $pData)
	/**
	 * get the content from data
	 */
	{

                /* DEPRECIEE - code pour l'appel à getNodes déprécié pour searchNodes
		if($pData['node'])
		{
			
			if($pData['node'] != "*")
			{
				
				$expr = '//'.$pData['node'].'/..' ;
				
			} else {
				
				return $pDocument ;
				
			}
			
		} else {
			
			return false ;
			
		}
		*/

	return utilities::searchNodes($pDocument, $pData) ;
	}

#     Qui devient ici capable de distinguer 
#     recherche de contenu d'avec les attributs de nœuds

	public static function getContentByData($pDocument, $pData)
	/**
	 * get the content from data
	 */
	{
		if(@$pData['attribute'])
		{
			return utilities::searchNodesByAttribute($pDocument, $pData) ;
		}
		else
		{	
			return utilities::searchNodesByContent($pDocument, $pData) ;	
		}
	}


Un test du getContentByData nous rend nominal pour cette phase dans cette modélisation métier. Le refactoring du business en services serait apprécié...


<?php

require_once "./Cms/Classes/dataManager.class.php";
require_once "./Cms/Classes/utilities.class.php";

//echo '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />';

$_manager = new dataManager();

//
$_manager->data['path'] =  $_manager->data['root']  . "Customer1/Contents/crop1.xml"; //

// init manager
$_document = $_manager->initialize();

# QUERY TESTS
 	
	// setting de valeur à récupérer
	$_manager->data['query_1'] = array("node"=>"subject","value"=>"potager");
	$_manager->data['query_2'] = array("node"=>"content","value"=>"molle2");
	
	// queries test
	$res = dataManager::getContentByData(
		dataManager::getContentByData($_document, $_manager->data['query_1'])
		,$_manager->data['query_22']);

# OUTPUT
echo $res->saveXML();

?>


-> Sur l'input crop1.xml
<?xml version="1.0" encoding="UTF-8"?>
<crop>
    <topic>Cultiver son potager</topic>
    <scope>légumes potager soupe</scope>
    <fields>
        <field id="k_01">
            <subject>les légumes du potager</subject>
            <content>Le potager contient une terre molle. 
                     Parsemé ça et là, on trouve carottes, 
                     petits poids et potirons
            </content>
        </field>
        <field id="k_02">
            <subject>les légumes du test</subject>
            <content>Le potager contient une terre molle. 
                     Parsemé ça et là, on Test carottes, 
                     petits poids et potirons</content>
        </field>
        <field id="k_03">
            <subject>les légumes du potager</subject>
            <content>Le potager contient une terre molle2. 
                     Parsemé ça et là, on trouve carottes, 
                     petits poids et potirons</content>
        </field>
    </fields>
</crop>


-> On a pour résultat :
<?xml version="1.0" encoding="UTF-8" ?> 
<crop>
  <topic>Cultiver son potager</topic> 
  <scope>légumes potager soupe</scope> 
  <fields>
    <field id="k_03">
      <subject>les légumes du potager</subject> 
      <content>Le potager contient une terre molle2. 
               Parsemé ça et là, on trouve carottes, petits poids et potirons
      </content> 
    </field>
  </fields>
</crop>


Le deuxième test de get par l'id est également concluant

# QUERY TESTS
 	
	// setting de valeur à récupérer
	$_manager->data['query_1'] = array("node"=>"subject", "value"=>"potager");
	$_manager->data['query_2'] = array("attribute"=>"id", "value"=>"k_03");
	
	// query test
	$res = dataManager::getContentByData(
		dataManager::getContentByData($_document, $_manager->data['query_1'])
		,$_manager->data['query_2']);
	
# OUTPUT
echo $res->saveXML();


Son résultat étant :

<?xml version="1.0" encoding="UTF-8" ?> 
<crop>
  <topic>Cultiver son potager</topic> 
  <scope>légumes potager soupe</scope> 
  <fields>
    <field id="k_03">
      <subject>les légumes du potager</subject> 
      <content>Le potager contient une terre molle2. 
               Parsemé ça et là, on trouve carottes, 
               petits poids et potirons
      </content> 
    </field>
  </fields>
</crop>


On implémente à utilities.class.php les méthodes :

	public static function writeContentByAttribute($pDocument, $pQueries)
	/**
	 * write node content matching request by attribute
	 */
	{
		
		$_fields = $pDocument->getElementsByTagName('fields');
		$_flg = false ;
		foreach ($_fields->item(0)->childNodes as $u)
		{
			if ($u->nodeType != XML_TEXT_NODE)
			{
				if( $u->getAttribute($pQueries['attribute']) 
                                       == $pQueries['value']  ) // 1:1 match
				{
					$v = $u->getElementsByTagName($pQueries['node']) ;
					$v->item(0)->nodeValue = $pQueries['replacement'] ;
					$_flg = true ;
				}
			}
		}
		
	return $_flg ;
	}
	
	public static function deleteContentByContent($pDocument, $pQueries)
	/**
	 * delete node matching request by attribute
	 */
	{
		
		$_fields = $pDocument->getElementsByTagName('fields');
		$_flg = false ;
		foreach ($_fields->item(0)->childNodes as $u)
		{
			if ($u->nodeType != XML_TEXT_NODE)
			{
				foreach ( $u->childNodes as $v  )
				{
					if( $v->nodeName == $pQueries['node'] 
                                               && utilities::isContent ( $pQueries['value'] , 
                                               $v->nodeValue ) )
					{
						$_fields->item(0)->removeChild($u) ;
						$_flg = true ;
					}
				}
			}
		}
		
	return $_flg ;
	}

	public static function deleteContentByAttribute($pDocument, $pQueries)
	/**
	 * delete  node matching request by attribute
	 */
	{
		$_fields = $pDocument->getElementsByTagName('fields');
		$_flg = false ;
		foreach ($_fields->item(0)->childNodes as $u)
		{
			if ($u->nodeType != XML_TEXT_NODE)
				if( $u->getAttribute($pQueries['attribute']) 
                                       == $pQueries['value'] ) // 1:1 match
				{
					$_fields->item(0)->removeChild($u) ;
					$_flg = true ;
				}
		}
	
	return $_flg ;
	}


et à la classe dataManager.class.php

	public function setContentByData($pDocument,$pData)
	/**
	 * set the content to data
	 */
	{
		if(@$pData['attribute'])
		{
			utilities::writeContentByAttribute($pDocument, $pData) ;
		}
		else
		{
			utilities::writeContentByContent($pDocument, $pData) ;
		}
		$this->saveDocument();
	}


Le test suivant permet de valider la phase d'écriture :

# QUERY TESTS
 	
	// setting de valeur à récupérer
	$_manager->data['query_1'] = array("attribute"=>"id", "value"=>"k_03","node"=>"subject",
         "replacement"=>"this is the new subject");
	
	// query test
	$_manager->setContentByData($_document, $_manager->data['query_1']);


avec le resultat

        <field id="k_03">
            <subject>this is the new subject</subject>
            <content>Le potager contient une terre molle2. 
                     Parsemé ça et là, on trouve carottes, 
                     petits poids et potirons
            </content>
        </field>
Liste des défauts[modifier | modifier le wikicode]
  • searchContents / getContentByData est du code métier
-> le transcrire en code de service
  • pour la suppression, delete reste à implémenter correctement
-> à dataManager

[2] Gestion des utilisateurs[modifier | modifier le wikicode]

La gestion des utilisateurs suivant le modèle ne demande que l'extraction de la ligne utilisateur pour recueillir ses infos.


Dans la classe utilities.class.php on rajoute ces deux méthodes :

	public static function getAttributesContents($pNode)
	/**
	 * retourne les attributs dans un tableau
	 */
	{
		foreach ($pNode->attributes as $attrName => $attrNode)
		{
		        $_tab[$attrName] = $attrNode->value ;
		}
		    
	return $_tab ;
	}
	
	public static function getLineByAttribute($pDocument, $pQueries)
	/**
	 * return node matching request by attribute
	 */
	{
		
		$_users = $pDocument->getElementsByTagName('user');
		
		foreach ($_users as $u)
		{
			if ($u->nodeType != XML_TEXT_NODE)
			{
				$_flg = true ;
				foreach($pQueries as $_qa )
				{
					if( !($u->getAttribute($_qa['attribute']) 
                                                == $_qa['value']) || !$_flg  )
					{ // 1:1 match
						
						$_flg = false ; 
						
					}
				}
				if($_flg)
					return utilities::getAttributesContents($u) ;
			}
		}
		
	return false ;
	}


Un test concluant de ces méthodes nous rend opérationnel pour cette phase

<?php

require_once "./Cms/Classes/dataManager.class.php";
require_once "./Cms/Classes/utilities.class.php";

$_manager = new dataManager();

//
$_manager->data['path'] =  $_manager->data['root']  . "Customer1/Contents/user1.xml"; //

# QUERY TESTS
 	
	$_manager->data['query_1'] = array(
			array("attribute"=>"login", "value"=>"user2")
			, array("attribute"=>"pass", "value"=>"pass2"));
	
	// query test
	$_res = utilities::getLineByAttribute($_manager->initialize(), $_manager->data['query_1']) ;
	
# OUTPUT
print_r_html($_res);

?>


Résultat :

Array
(
    [login] => user2
    [pass] => pass2
    [lastname] => name2
    [forname] => forName2
    [rights] => u
    [customer] => customer2
)
Liste des défauts[modifier | modifier le wikicode]

Cette gestion des utilisateurs ne prend actuellement que les attributs de nœuds...

-> il serait utile de pourvoir des données supplémentaires dans le nœud user.

[3] Complétion du frameset[modifier | modifier le wikicode]

  • Pour commencer nous déclarons une classe utilities.class.php qui reprendra toutes les fonctionnalités utiles pour notre solution


<?php 

function print_r_html($data,$return_data=false)
{
    $data = print_r($data,true);
    $data = str_replace( " ","&nbsp;", $data);
    $data = str_replace( "\r\n","<br/>\r\n", $data);
    $data = str_replace( "\r","<br/>\r", $data);
    $data = str_replace( "\n","<br/>\n", $data);
 
    if (!$return_data)
        echo $data;
    else
        return $data;
}

class utilities
{
	
	
   public static function getNodes($pDomDoc, $pXpathString)
   /**
    * get the node from DomDoc with XpathString
    */
   {
       $xp = new DOMXPath($pDomDoc);
       $xp->registerNamespace('x', 'http://www.w3.org/1999/xhtml');
       $xp->registerNamespace('xhtml', 'http://www.w3.org/1999/xhtml');
       $xp->registerNamespace('i18n', 'http://apache.org/cocoon/i18n/2.1');
	
       $ret = array();
       $nodes = $xp->query($pXpathString);
       foreach ($nodes as $node)
       {
           array_push($ret, $node);
       }
   }
	
   public static function isContent($pPattern, $pFeed)
   /**
   * check is pattern in feed
   */
   {
       return preg_match('/('.$pPattern.')/',$pFeed)?true:false;
   }

}

?>
  • La classe templateControler regroupe les fonctions sur les templates. Le contenu à publier est segmenté en flux.
  • Son objectif est de :
- reformater,
- assembler,
- publier.
<?php

class templateControler
{
/**
 * Control the templates
 *
 * @var (mixed) ($domDoc, $data, $stack)
 */
	
	// 
	private $pattern 
		= array(
			'inc'=>'/{{inc:[a-zA-Z]*[\[\]\*]*}}/'		// inclusion patterns
			,''=>''
		);
	
	protected $domDoc, $stack;
	
	//
	public $data;
	
	/*
	public function __construct()
	{//
		$this->domDoc = new DOMDocument();
		$this->initialize();
	}
	*/
	
	/**
	 * Enter description here...
	 *
	 * @param unknown_type $pVar
	 * @return templateControler
	 */
	public function templateControler($pVar)
	{
		$this->domDoc = new DOMDocument();
		$this->setter($pVar);
		$this->initialize();
	}
	
	/**
	 * Enter description here...
	 *
	 * @param unknown_type $pVar
	 */
	public function setter($pVar)
	{//
		 $this->data = $pVar;
	}
	
// TODO GETTER
	
	public function initialize()
	{//
		$this->domDoc->load( $this->data['path'] );
	}
	
	/**
	 * Enter description here...
	 *
	 */
	public function getNode($pXPath) //"//html/body/*[@class='text']";
	{//
		return $this->data['feed'] = utilities::getNodes($this->domDoc, $pXPath);
	}

	public function getAnchors($pFeed)
	{// 
		preg_match_all($this->pattern['inc'], $pFeed, $this->data['result']);
		
	return $this->data['result']; // on prefere les données centralisées dans la classe
	}
	
	public function publicTemplate()
	{//
		return $this->domDoc->saveXML();
	}
	
}

Le test suivant permet de voir si on prend bien les ancres d'injection, par exemple, pour le nœud frameset/content

//
$tmp = new templateControler(array("path"=>"object.xml"));
$tst = $tmp->getNode("//template/frameset/content");

print_r_html($tmp->getAnchors($tst[0]->nodeValue));
Array
(
    [0] => Array
        (
            [0] => {{inc:styles[*]}}
            [1] => {{inc:scripts[*]}}
            [2] => {{inc:bodies[*]}}
        )

)

On implémente ces deux nouvelles fonctions à templateControler

	/**
	 * getContent retourne le contenu du/des nœuds demandés
	 * 
	 * > (nodeName, nodeId) - (string) >
	 */
	public function getContent($nodeName,$nodeId)
	{//
		// set expression
		$_expr = "//" . $nodeName . ($nodeId&&$nodeId!='*'?"[id='$nodeId']":"");
		$set = $this->getNode($_expr);
		
		$content=null;
		foreach($set as $k=>$v)
		{
			$content .= "<" . $nodeName . ">" . trim($v->nodeValue) . "</" . $nodeName . ">";
		}
		
	return $content?$content:"";
	}
	
	/**
	 * replace anchor by content
	 */
	public function anchorContentReplacer()
	{//
		
		$i=0;
		foreach($this->data['result'][0] as $k=>$v)
		{
			// formatting anchor's syllabes
			$v = str_replace(array('{','[','}',']'),array('',' ','',''),$v);
			$tmp = explode(":",$v);$_ = explode(' ',$tmp[1]);
			
			// replacing
			$this->data['result'][0][$i++] 
                             = array("node"=>$_[0],"value"=>$this->getContent($_[0], $_[1]));
		}
	}
	
// TODO : getNodeById pour disposer d'un getElementById moins spécialisé et fonctionnel ici.

Le test suivant permet de voir si on remplace bien l'ancre par son contenu :

require_once "templateControler.class.php";

//
$tmp = new templateControler(array("path"=>"template.xml"));

$tmp->getNode("//template/frameset/content");

$tmp->getAnchors(
		$tmp
			->data['feed'][0]
			->nodeValue);

$tmp->anchorContentReplacer();

print_r_html(
	$tmp->data['result']
	);
Array
(
    [0] => Array
        (
            [0] => Array
                (
                    [node] => styles
                    [value] => 
                
                
                    body
                    {
                        background-color:red;
                        font-family:verdana;
                        
                        padding:10px;
                    }
                    
                    .style_01
                    {
                        /* some style in */
                    }
                
            
                )

            [1] => Array
                (
                    [node] => scripts
                    [value] => 
                
                
            
                )

            [2] => Array
                (
                    [node] => bodies
                    [value] => 
                )

        )

)

Le contenu est bien localisé dans le template, par conséquent l'objectif de cette phase est atteint. On est opérationnel pour l'injection.


  • La classe snippetActer regroupe les fonctions de computation des snippets.
class SnippetActer
{//
 	
	protected $_type,$_content,$_stack;
 	
# GET/SET
	
	public function setType()
	{//
		list($_1,$_2)=explode('/',$this->_type);
		$this->actCode(array($_1,$_2));
	}
 	
	public function setContent($pContent)
	{//
		$this->_content = $pContent;
	}
 	
# STATE

	/*
	 * act statements
	 */
	public function actCode($pType)
	{//
		switch(true)
		{
			case $pType[0]=="text"&&$pType[0]=="php":
//				
				$this->write();
				break;
 
			case $pType[0]=="text"&&$pType[0]=="javascript":
				$this->inject();
//
				break;
 
			case $pType[0]=="action"&&$pType[0]=="php":
				$this->unstack();$this->act();
				break;
		}
	}
 
	/*
	 * stack statements
	 */
	public function unstack()
	{//
		$_this->_stack = explode(";",$this->_content);
	}
 
	/*
	 * execute statements
	 */
	public function act()
	{//
		foreach($this->_stack as $v)
			eval($v);
	}

}
Liste des défauts[modifier | modifier le wikicode]
  • Les snippets actifs à la volée peuvent ralentir l'output
-> l'ajaxification peut fournir le contenu en deux temps (données avant/après traitements)
  • Cette classe reste à faire...

[4] Un webservice[modifier | modifier le wikicode]

Le webService propose par défaut une synergie avec d'autres solutions, sites, applications. Des fonctionnalités distantes permettent l'interfaçage logicielle avec le serveur de données, option qui deviendra basique pour les systèmes orientés données.

<?php
/*
	webservice servker
*/
	
# no chache in testing
		ini_set("soap.wsdl_cache_enabled", "0");
		 
# wsdl
		$serveurSOAP = new SoapServer('service.wsdl');
		 
# provided services

		/* inqueries services 
			in :
			out :
		*/
		$serveurSOAP->addFunction('request');
		// ...
		
		/* high level services 
			in :
			out :
		*/
		$serveurSOAP->addFunction('harvest');
		// ...
		
		/* low level services 
			in : 
			out :
		*/
		$serveurSOAP->addFunction('deploy');
		$serveurSOAP->addFunction('f_01');
		$serveurSOAP->addFunction('f_02');
		// ...
		 
# server start 
		if ($_SERVER['REQUEST_METHOD'] == 'POST')
		{
			$serveurSOAP->handle();
		}
		 
# services integration
		require_once "./Services/request.srv.php";
		require_once "./Services/service_01.srv.php";
		require_once "./Services/service_02.srv.php";
		
# online status checker
		function listen($pIn)
		{
			return 'Copy : ' . $pIn;
		}
		
?>
Liste des défauts[modifier | modifier le wikicode]