Un bean
est une instance d'une classe, instanciable par constructeur simple (sans argument), aux propriétés accessibles par mutateurs et accesseurs. Le nom bean
vient de JavaBean, bien que la définition et l'usage soient détournés de l'original.
Dans Change, le bean (f_mvc_Bean
) est associé à un model (f_mvc_BeanModel
) qui renseigne le code le manipulant sur ses propriétés : existence, type, contraintes et libellé, et possède en général un unique identifiant géré par la propriété id.
Le bean est notamment utilisé dans le processus de peuplement des formulaires et comme raccourci fonctionnel dans la récupération des données d'une requête :
Les documents Change sont des beans et peuvent être utilisés en tant que tel ; c'est d'ailleurs le cas le plus courant. Parfois cependant, il pourra générer un bean à partir de rien en générant du code PHP ou en utilisant un “Beans dynamiques”.
Associer un bean à une action d'un bloc se fait :
$request
et $response
),getXXXBeanInfo()
, qui doit retourner un tableau associatif contenant des valeurs pour les clefs className
et beanName
. Cette méthode est préférée dans les cas plus dynamiques où la classe du bean n'est pas déterminée “en dur” dans le code du bloc.Une fois le bean associé à une action d'un bloc, celle-ci reçoit l'objet en question en paramètre, proprement peuplé en fonction des paramètres de la requête.
Dans l'extrait de code suivant, le bean nommé “mydoc” et de classe mymodule_persistentdocument_mydoc
a été associé à l'action “save” du bloc :
/** * @param f_mvc_Request $request * @param f_mvc_Response $response * @return String */ function executeSave($request, $response, mymodule_persistentdocument_mydoc $mydoc) { // ... do something with $mydoc }
La même association aurait pu se faire implémentant getSaveBeanInfo() :
function executeSave($request, $response, $mydoc) { // ... do something with $mydoc } function getSaveBeanInfo() { return array("className" => "mymodule_persistentdocument_mydoc", "beanName" => "mydoc"); }
Une fois l'association faite, tout appel à executeSave()
par le contrôleur de blocs sera précédé d'une instanciation d'un objet de la classe mymodule_persistentdocument_mydoc
et du peuplement de l'objet avec les paramètres de la requête.
Le processus de peuplement d'un bean intervient juste avant l'appel à la méthode associée et repose par défaut sur la méthode BeanUtils::populate()
à laquelle sont transmis les paramètres de requête ($request→getParameters()
).
Il s'agit d'initialiser correctement les propriétés typées d'un objet étant données des valeurs de type String en entrée, ce qui peut impliquer des conversions de type.
Le peuplement standard d'un bean de type mymodule_persistentdocument_mydoc
est équivalent au code suivant :
$mydoc = BeanUtils::getNewBeanInstance("mymodule_persistentdocument_mydoc"); BeanUtils::populate($mydoc, $request->getParameters());
BeanUtils::populate()
peuple le bean avec les valeurs du tableau en cherchant les propriétés correspondant aux clefs du tableau.
Ainsi, si la requête contient les paramètres “label” et “description”, correspondant à des propriétés du document mymodule_persistentdocument_mydoc
, le controlleur executera un code équivalent à :
$mydoc = mymodule_MydocService::getInstance()->getNewDocumentInstance(); $mydoc->setLabel($request->getParameter("label")); $mydoc->setDescription($request->getParameter("description"));
De plus, si beanId
est un paramètre de la requête, celui-ci sera utilisé pour instancier l'objet mymodule_persistentdocument_mydoc
ayant l'identifiant en question :
$mydoc = BeanUtils::getBeanInstance("mymodule_persistentdocument_mydoc", $request->getParameter("beanId")); BeanUtils::populate($mydoc, $request->getParameters());
Code équivalent à :
$mydoc = mymodule_MydocService::getInstance()->getDocumentInstance($request->getParameter("beanId")); $mydoc->setLabel($request->getParameter("label")); $mydoc->setDescription($request->getParameter("description"));
Ainsi, si la requête contient suffisamment d'informations et que validateSaveInput()
est bien implémentée, le bloc pourra se contenter d'appeler save()
sur l'objet qui lui aura été transmis pour créer ou mettre à jour un document “projet”.
À partir de la version 3.6.5, le mode de peuplement "strict" impose ce filtrage explicite pour les documents.
À noter également que pour des raisons de sécurité, les utilisateurs backoffice ne sont pas “peuplables” automatiquement, de même que les propriétés critiques des utilisateurs frontoffice autres que l'utilisateur connecté (mot de passe, email, login). En cas de besoin, ces mises à jour devront donc être faites à la main plutôt que par peuplement automatique.
Renseigner une propriété d'un bean implique parfois des conversions : du simple “cast” de chaine vers entier à la récupération de documents à partir d'une collection d'identifiants.
A cet effet, une propriété peut déclarer un convertisseur (Cf. BeanPropertyInfo::getConverter()
). Le convertisseur est alors utilisé dans les deux sens : de la requête vers le bean (lors du peuplement d'un bean à partir de la requête) ou du bean vers la requête (lors du peuplement d'un formulaire à partir d'un bean par ex.). Un convertisseur implémente l'interface BeanValueConverter
:
interface BeanValueConverter { /** * @param Mixed $value * @return Mixed */ public function convertFromRequestToBeanValue($value); /** * @param Mixed $value * @return Mixed */ public function convertFromBeanToRequestValue($value); }
Le framework fournit des convertisseurs de base, disponibles dans le dossier f_mvc/bean/converters/
:
Par défaut, l'ensemble des propriétés du bean sont peuplées si un paramètre correctement nommé à une valeur compatible. Cet ensemble peut être restreint :
get<NomBean>BeanInclude()
.get<NomBean>BeanExclude()
.
À partir de la version 3.6.5, un mode de peuplement dit “strict” est activé par défaut. Il impose que pour tout bean de type document, la liste des liste des propriétés “peuplables” soit donnée explicitement (dans le cas contraire une exception est lancée).
Ce mode impose de respecter les précautions essentielles de sécurité. Il est cependant désactivable par configuration projet (essentiellement afin de permettre les migrations automatiques depuis les version 3.6.x antérieures) via la configuration projet en définissant la clé modules/website/useBeanPopulateStrictMode
à false
.
Pour maîtriser complètement le processus de peuplement, implémenter populate<BeanName>Bean() : array<String, String>
.
Important : cette méthode reçoit une instance du bean, agit dessus pour le peupler et renvoie les valeurs qui n'ont pas pu être affectées sous la forme d'un tableau associatif “nom de propriété ⇒ valeur invalide”. BeanUtils::populate()
renvoie ce tableau et BeantUtils::setProperty()
renvoie false
si la propriété existe et qu'elle n'a pu être remplie avec la valeur fournie.
Exemple : le bean mydoc
attaché à executeSave()
est peuplé avec la méthode populateMydocBean()
qui utilise le processus standard et effectue des traitements spécifiques
/** * @param f_mvc_Request $request * @param f_mvc_Response $response * @return String */ function executeSave($request, $response, mymodule_persistentdocument_mydoc $mydoc) { // here the ticket bean was populated using populateMydocBean() method } /** * @param mymodule_persistentdocument_mydoc $mydoc * @param f_mvc_Request $request * @return array<String, String> */ function populateMydocBean(mymodule_persistentdocument_mydoc $mydoc, $request) { // standard population process, excluding some properties $excludedProperties = array("privPropName1", "privPropName2"); $invalidProperties = BeanUtils::populate($mydoc, $request->getParameters(), null, $excludedProperties); // custom things ... return $invalidProperties; }
Il est possible d'agir sur le bean pour différents traitements en fin de processus de peuplement en déclarant un “BeanPopulateFilter”. Ce mécanisme agit sur le peuplement du bean, que la méthode populate<BeanName>Bean
ait été spécialisée ou non :
WEBSITE_POST_POPULATE_FILTERS
avec des noms de classes implémentant website_BeanPopulateFilter
interface website_BeanPopulateFilter { /** * @param f_mvc_Bean $bean * @param website_BlockActionRequest $request */ function execute($bean, $request); }
Ce mécanisme est notamment utilisé par l'extension PHPTal change:uploadfield
qui permet de peupler un bean avec des documents media_persistentdocument_tmpfile
à partir de fichier téléchargés vers le serveur :
<propName>
) le postfilter media_FileBeanPopulateFilter
: WEBSITE_POST_POPULATE_FILTERS[<propName>] = media_FileBeanPopulateFilter
,<propName>
,media_FileBeanPopulateFilter
récupère les fichiers depuis $_FILES
, les transforme en media_persistentdocument_tmpfile
et peuple la propriété <propName>
du bean avec.
Le développeur pourra placer dans un formulaire un champ caché définissant des valeurs de WEBSITE_POST_POPULATE_FILTERS
, en “dur” dans le gabarit ou au travers une extension PHPTal spécifique.
Les beans sont utilisés pour peupler des formulaires :
L'outil ligne de commande changedev.php
permet de générer rapidement un formulaire basique à partir d'un bean grâce à la commande generate-bean-frontoffice-form
:
Usage: changedev.php generate-bean-frontoffice-form <className> <outputFileName> [options] where options in: --stdout: output the generated html to standard output
Ainsi, pour le bean news_persistentdocument_news
:
intsimoa@srbswebrd:~/change4/change$ changedev.php generate-bean-frontoffice-form news_persistentdocument_news --stdout
La commande generate-bean-frontoffice-form
génère le formulaire suivant :
<form change:form="beanClass news_persistentdocument_news; beanName news"> <div change:errors=""></div> <tal change:field="name beanId" hidden="true" /> <ol> <li><input change:field="name label" /></li> <li><input change:field="name startpublicationdate" /></li> <li><input change:field="name metastring" /></li> <li><input change:field="name date" /></li> <li><input change:field="name summary" /></li> <li><input change:field="name text" /></li> <li><input change:field="name linkedpage" /></li> <li><input change:field="name listvisual" /></li> <li><input change:field="name detailvisual" /></li> <li><input change:field="name datetimeinfo" /></li> <li><input change:field="name place" /></li> <li><input change:field="name accessmap" /></li> <li><input change:field="name contact" /></li> <li><input change:field="name attachment" /></li> <li><input change:field="name priority" /></li> <li><input change:field="name publicationyear" /></li> <li><input change:field="name publicationmonth" /></li> <li><input change:field="name publicationweek" /></li> <li><input change:field="name archiveyear" /></li> <li><input change:field="name archivemonth" /></li> <li><input change:field="name archiveweek" /></li> <li><input change:field="name startarchivedate" /></li> </ol> <p> <input change:submit="label &modules.website.frontoffice.form.Submit;"/> </p> </form>
Le formulaire généré place l'ensemble des propriétés du document les unes en dessous des autres et ne fixe aucunement les contrôles HTML utilisés. Le contrôle par défaut déterminé par le type de la propriété sera utilisé.
Reste alors au développeur de déplacer, supprimer des champs, d'éventuellement fixer le contrôle utilisé pour un champ donné ou encore de régler des paramètres de mise en forme (nombre de colonnes ou de lignes pour une propriété de type LongString
ou XHTMLFragment
par exemple).
L'extension PHPTal change:field
permet la sélection d'un contrôle en fonction du type de la propriété.
Le tableau suivant montre l'association “type de propriété ⇒ type de contrôle” :
Type de propriété | Type de contrôle | Equivalent HTML |
---|---|---|
Boolean | change:booleaninput | input[@type = 'radio'], value = {oui, non} |
Lob | change:textarea | <textarea /> |
LongString | change:textarea | - |
XHTMLFragment | change:richtextinput | <textarea /> + éditeur js wysiwyg (fckeditor) |
DateTime | change:dateinput | input[@type = 'text'] + contrôle js date |
Date | change:dateinput | - |
Document | change:documentinput | <select /> |
Champ associé à une liste | change:selectinput ou ensemble de change:checkboxinput si @display = “checkbox” | <select /> |
String | change:textinput | input[@type = 'text'] |
Autres | change:textinput | - |
L'attribut beanName
de change:form
indique le nom de l'objet à utiliser. Dans l'exemple suivant, un bloc transmet un bean sous le nom “mydoc” :
class mymodule_SomeBlockAction { function execute($request, $response) { $somebean = ...; $somebean->setSomeProperty($somePropertyValue); $request->setAttribute("mydoc", $somebean); } }
et la vue utilise l'objet transmis,
<form change:form="beanName mydoc"> <input change:field="name someProperty" /> ... </form>
L'attribut beanClass
permet à change:form
d'instancier un bean et de l'utiliser si celui-ci n'est pas disponible dans la requête. Le cas d'utilisation typique est l'affichage d'un formulaire d'insertion, cas où l'objet n'existe pas encore :
<form change:form="beanClass mymodule_persistentdocument_mydoc"> <input change:field="name label" /> ... </form>
Si beanClass
est défini et beanName
non, change:form
utilise le nom de la classe pour déterminer beanName. Ainsi “beanClass=mymodule_persistentdocument_mydoc” implique “beanName=mydoc”.
Le libellé des champs est déterminé par la méthode BeanPropertyInfo::getLabelKey()
modules.<moduleName>.document.<documentName>.<PropertyName>
,f_mvc_DynBean
), cette clef est modules.<moduleName>.document.<shortclassname>.<PropertyName>
Dans le cas de bean aggrégeant d'autres bean, la notation pointée “<nomSousObject>.<nomPropriété>” permet de désigner
Dans l'exemple suivant, le bean mymodule_Mydynbean
déclare une propriété subBean
de type mymodule_MySubBean
:
class mymodule_Mydynbean { private $label; private $subBean; /** * @return String */ function getLabel() { return $this->label; } /** * @param String $value */ function setLabel($value) { $this->label = $value; } /** * @return mymodule_MySubBean */ function getSubBean() { return $this->subBean; } /** * @param mymodule_MySubBean $value */ function setSubBean($value) { $this->subBean = $value; } } class mymodule_MySubBean { private $stringProperty; private $intProperty; /** * @return String */ function getStringProperty() { return $this->stringProperty; } /** * @param String $value */ function setStringProperty($value) { $this->stringProperty = $value; } /** * @return Integer */ function getIntegerProperty() { return $this->intProperty; } /** * @param Integer $value */ function setIntegerProperty($value) { $this->intProperty = $value; } }
Le formulaire suivant utilise les propriétés stringProperty
et integerProperty
du bean subBean
:
<form change:form="beanClass mymodule_Mydynbean"> <input change:field="name label" /> <input change:field="subBean.stringProperty" /> <input change:field="subBean.integerProperty" /> </form>
La classe f_mvc_DynBean
permet des implémentations rapides de f_mvc_Bean
et f_mvc_BeanModel
à partir de classes simples. f_mvc_DynBean
inspecte la classe et retient les propriétés typées (par commentaire phpdoc) comme éléments du modèle.
Une propriété est retenue comme élément du modèle :
get<PropertyName>()
) et un mutateur (setPropertyName()
) publics sont définis. Si oui, les annotations des éléments suivants sont inspectés (dans l'ordre) pour déterminer le type de la propriété :@param <Type> $nomParam
)@return <Type>
)@var <Type>
)
De la classe suivante seront retenue les propriétés “aString”, de type String
et “anInteger”, de type Integer
; la propriété “nonAnnotated” ne sera pas retenue :
class mymodule_Mybean { private $aString; /** * @var Integer */ public $anInteger; private $nonAnnotated; /** * @param String $value */ public function setAString($value) { $this->aString = $value; } /** * @return String */ public function getAString() { return $this->aString; } public function setNonAnnoted($nonAnnotated) { $this->nonAnnotated = $nonAnnotated; } public function getNonAnnoted() { return $this->nonAnnotated; } }
Le tableau suivant résume les annotations de type supportées :
Annotation de type (insensible à la casse) | Equivalent Change | Annotation spécifique |
---|---|---|
int ou integer | Integer | N.A. |
float ou double | Double | N.A. |
boolean | Boolean | N.A. |
string | Dépend de l'annotation @type, String par défaut | @type{XHTMLFRAGMENT, LONGSTRNG, LOB} |
date_datetime | DateTime | N.A. |
date_date | DateTime | N.A. |
<MODULE>_persistentdocument_<DOCUMENT>, <MODULE>_persistentdocument_<DOCUMENT>[] | modules_<MODULE>/<DOCUMENT> | N.A. |
<UN_NOM_DE_CLASSE>, <UN_NOM_DE_CLASSE>[] | N.A. | N.A. |
Les éléments de la classes (accesseurs, mutateurs et propriétés) peuvent être annotés :
Nom | Signification | Valeurs | Exemple |
---|---|---|---|
@constraint | Les contraintes à appliquer à la propriété (en plus de la conversion de type) | Noms de validateurs + paramètres séparés par ';' | @constraints(min:0;max:100) |
@required | Marque une propriété comme obligatoire | N.A. | @required |
@listId | Lie la propriété à une liste (du module list), qui définit les valeurs possibles de la propriété | Un identifiant de liste | @listId(modules_website/templates) |
Issu du module blocksdemo
, la classe blocksdemo_AnObject
montre l'utilisation de la plupart des annotations :
class blocksdemo_AnObject { /** * @required * @var Integer */ public $anInteger; /** * @var String */ public $aString; /** * @constraints(min:0;max:100) * @var Float */ public $aDecimal; /** * @required * @var Boolean */ public $aBoolean; /** * @var date_Date */ public $aDate; /** * @var date_DateTime */ public $aDateTime; /** * @var news_persistentdocument_news */ public $news; /** * @constraints(minSize:2;maxSize:3) * @var news_persistentdocument_news[] */ public $newsArray; /** * @required * @listId(modules_blocksdemo/documentarray) * @var news_persistentdocument_news[] */ public $news2Array; /** * @type(LONGSTRING) * @var String */ public $aLongString; /** * @type(XHTMLFRAGMENT) * @var String */ public $anXHTMLFragment; /** * @listId(modules_website/templates) * @var String */ public $anEnumeratedString; /** * @var media_persistentdocument_media */ public $aMedia; /** * @var media_persistentdocument_media[] */ public $medias; /** * @var website_persistentdocument_page */ public $aPage; /** * @var media_persistentdocument_file */ public $aFile; /** * @constraints(mimetype:image/*) * @var media_persistentdocument_file */ public $anImage; /** * @var media_persistentdocument_file[] */ public $files; }
Et le gabarit suivant :
<form change:form="beanClass blocksdemo_AnObject; id anObjectForm"> <ul change:errors="" /> <ol> <li><input change:field="name aString" labeled="true" /></li> <li><input change:field="name aDecimal" labeled="true" /></li> <li><input change:field="name anInteger" labeled="true" /></li> <li><input change:field="name aBoolean" labeled="true" /></li> <li><input change:field="name aDate" labeled="true" /></li> <li><input change:field="name aDateTime" labeled="true" /></li> <li><input change:field="name anEnumeratedString" labeled="true" nopreamble="true" /></li> <li><input change:field="name news" labeled="true" /></li> <li><input change:field="name news.label" labeled="true" /></li> <li><input change:field="name newsArray" labeled="true" /></li> <li><input change:field="name news2Array" labeled="true" /></li> <li><input change:field="name aLongString" labeled="true" /></li> <li><input change:field="name anXHTMLFragment" labeled="true" /></li> <li><input change:documentpicker="name aMedia" labeled="true" /></li> <li><input change:documentpicker="name aPage" labeled="true" /></li> <li><input change:documentpicker="name medias" labeled="true" /></li> <li><input change:uploadfield="name aFile" labeled="true" /></li> <li><input change:uploadfield="name anImage" labeled="true" /></li> <li><input change:uploadfield="name files" labeled="true" accept="png,gif,jpg,jpeg" /></li> <li><input change:submit="" /></li> </ol> </form>
Donne le formulaire suivant :
L'aggréation de beans est simplifiée par l'usage des DynBean. La classe blocksdemo_Userbean
aggrège un document users_persistentdocument_user
et les propriétés password
et confirmPassword
permettant d'assoir l'édition de l'utilisateur et de son mot de passe simplement 1) :
/** * @constraints(propEq:password,confirmPassword) */ class blocksdemo_UserBean { /** * @var users_persistentdocument_user */ public $user; /** * @var String */ public $password; /** * @requiredIf(password) * @var String */ public $confirmPassword; function getId() { if ($this->user !== null) { return $this->user->getId(); } return null; } static function getInstanceById($userId) { $bean = new self(); $bean->user = DocumentHelper::getDocumentInstance($userId); return $bean; } }
Le formulaire utilise la notation pointée “user.*” pour les propriétés de l'utlisateur :
<form change:form="beanClass blocksdemo_UserBean; id userForm" tal:condition="beanId"> <ul change:errors="" /> <ol> <li> <input change:field="name beanId" /> <input change:field="name user.title" labeled="true" /> </li> <li><input change:field="name user.firstname" labeled="true" /></li> <li><input change:field="name user.lastname" labeled="true" /></li> <li><input change:field="name user.email" labeled="true" size="30" /></li> <li><input change:passwordinput="name password" labeled="true" /></li> <li><input change:passwordinput="name confirmPassword" labeled="true" /></li> <li><input change:submit="" /></li> </ol> </form>
Et le bloc blocksdemo_BlockDynbeanuserAction
d'utiliser le bean blocksdemo_UserBean
. Notez l'utilisation de BeanUtils::getSubBeanValidationRules()
pour récupérer les règles de validation du document users_persistentdocument_user
:
/** * blocksdemo_BlockDynbeanuserAction * @package modules.blocksdemo.lib.blocks */ class blocksdemo_BlockDynbeanuserAction extends website_BlockAction { /** * @see website_BlockAction::execute() * * @param f_mvc_Request $request * @param f_mvc_Response $response * @return String */ function execute($request, $response) { return $this->getSubmitInputViewName(); } function getSubmitInputViewName() { return "Form"; } function getSubmitInputValidationRules() { $rules = BeanUtils::getBeanValidationRules("blocksdemo_UserBean"); $rules = array_merge($rules, BeanUtils::getSubBeanValidationRules("blocksdemo_UserBean", "user", array("firstname", "lastname", "email"))); return $rules; } /** * @see website_BlockAction::execute() * * @param f_mvc_Request $request * @param f_mvc_Response $response * @return String */ function executeSubmit($request, $response, blocksdemo_UserBean $userBean) { $request->setAttribute("anObjectVarExport", var_export($userBean, true)); return website_BlockView::SUCCESS; } }