form.class.inc.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. <?php
  2. // Copyright (C) 2010-2016 Combodo SARL
  3. //
  4. // This file is part of iTop.
  5. //
  6. // iTop is free software; you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // iTop is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with iTop. If not, see <http://www.gnu.org/licenses/>
  18. namespace Combodo\iTop\Form;
  19. use Combodo\iTop\Form\Field\SubFormField;
  20. use \Exception;
  21. use \Dict;
  22. use \Combodo\iTop\Form\Field\Field;
  23. /**
  24. * Description of Form
  25. *
  26. * @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
  27. */
  28. class Form
  29. {
  30. protected $sId;
  31. protected $sTransactionId;
  32. protected $aFields;
  33. protected $aDependencies;
  34. protected $bValid;
  35. protected $aErrorMessages;
  36. protected $iEditableFieldCount;
  37. /**
  38. * Default constructor
  39. *
  40. * @param string $sId
  41. */
  42. public function __construct($sId)
  43. {
  44. $this->sId = $sId;
  45. $this->sTransactionId = null;
  46. $this->aFields = array();
  47. $this->aDependencies = array();
  48. $this->bValid = true;
  49. $this->aErrorMessages = array();
  50. $this->iEditableFieldCount = null;
  51. }
  52. /**
  53. *
  54. * @return string
  55. */
  56. public function GetId()
  57. {
  58. return $this->sId;
  59. }
  60. /**
  61. *
  62. * @param string $sId
  63. * @return \Combodo\iTop\Form\Form
  64. */
  65. public function SetId($sId)
  66. {
  67. // Setting id for the form itself
  68. $this->sId = $sId;
  69. // Then setting formpath to its fields
  70. foreach ($this->aFields as $oField)
  71. {
  72. $oField->SetFormPath($sId);
  73. }
  74. return $this;
  75. }
  76. /**
  77. *
  78. * @return string
  79. */
  80. public function GetTransactionId()
  81. {
  82. return $this->sTransactionId;
  83. }
  84. /**
  85. *
  86. * @param string $sTransactionId
  87. * @return \Combodo\iTop\Form\Form
  88. */
  89. public function SetTransactionId($sTransactionId)
  90. {
  91. $this->sTransactionId = $sTransactionId;
  92. return $this;
  93. }
  94. /**
  95. *
  96. * @return array
  97. */
  98. public function GetFields()
  99. {
  100. return $this->aFields;
  101. }
  102. /**
  103. *
  104. * @return array
  105. */
  106. public function GetDependencies()
  107. {
  108. return $this->aDependencies;
  109. }
  110. /**
  111. * Returns a hash array of "Field id" => "Field value"
  112. *
  113. * @return array
  114. */
  115. public function GetCurrentValues()
  116. {
  117. $aValues = array();
  118. foreach ($this->aFields as $sId => $oField)
  119. {
  120. $aValues[$sId] = $oField->GetCurrentValue();
  121. }
  122. return $aValues;
  123. }
  124. /**
  125. *
  126. * @param array $aValues Must be a hash array of "Field id" => "Field value"
  127. * @return \Combodo\iTop\Form\Form
  128. */
  129. public function SetCurrentValues($aValues)
  130. {
  131. foreach ($aValues as $sId => $value)
  132. {
  133. $oField = $this->GetField($sId);
  134. $oField->SetCurrentValue($value);
  135. }
  136. return $this;
  137. }
  138. /**
  139. * Returns the current validation state of the form (true|false).
  140. * It DOESN'T make the validation, see Validate() instead.
  141. *
  142. * @return boolean
  143. */
  144. public function GetValid()
  145. {
  146. return $this->bValid;
  147. }
  148. /**
  149. * Note : Function is protected as bValid should not be set from outside
  150. *
  151. * @param boolean $bValid
  152. * @return \Combodo\iTop\Form\Form
  153. */
  154. protected function SetValid($bValid)
  155. {
  156. $this->bValid = $bValid;
  157. return $this;
  158. }
  159. /**
  160. *
  161. * @return array
  162. */
  163. public function GetErrorMessages()
  164. {
  165. return $this->aErrorMessages;
  166. }
  167. /**
  168. * Note : Function is protected as aErrorMessages should not be set from outside
  169. *
  170. * @param array $aErrorMessages
  171. * @param string $sFieldId
  172. * @return \Combodo\iTop\Form\Form
  173. */
  174. protected function SetErrorMessages($aErrorMessages, $sFieldId = null)
  175. {
  176. if ($sFieldId === null)
  177. {
  178. $this->aErrorMessages = $aErrorMessages;
  179. }
  180. else
  181. {
  182. $this->aErrorMessages[$sFieldId] = $aErrorMessages;
  183. }
  184. return $this;
  185. }
  186. /**
  187. * If $sFieldId is not set, the $sErrorMessage will be added to the general form messages
  188. *
  189. * Note : Function is protected as aErrorMessages should not be add from outside
  190. *
  191. * @param string $sErrorMessage
  192. * @param string $sFieldId
  193. * @return \Combodo\iTop\Form\Form
  194. */
  195. protected function AddErrorMessage($sErrorMessage, $sFieldId = '_main')
  196. {
  197. if (!isset($this->aErrorMessages[$sFieldId]))
  198. {
  199. $this->aErrorMessages[$sFieldId] = array();
  200. }
  201. $this->aErrorMessages[$sFieldId][] = $sErrorMessage;
  202. return $this;
  203. }
  204. /**
  205. * Note : Function is protected as aErrorMessages should not be set from outside
  206. *
  207. * @return \Combodo\iTop\Form\Form
  208. */
  209. protected function EmptyErrorMessages()
  210. {
  211. $this->aErrorMessages = array();
  212. return $this;
  213. }
  214. /**
  215. *
  216. * @param string $sId
  217. * @return \Combodo\iTop\Form\Field\Field
  218. * @throws Exception
  219. */
  220. public function GetField($sId)
  221. {
  222. if (!array_key_exists($sId, $this->aFields))
  223. {
  224. throw new Exception('Field with ID "' . $sId . '" was not found in the Form.');
  225. }
  226. return $this->aFields[$sId];
  227. }
  228. /**
  229. *
  230. * @param string $sId
  231. * @return boolean
  232. */
  233. public function HasField($sId)
  234. {
  235. return array_key_exists($sId, $this->aFields);
  236. }
  237. /**
  238. *
  239. * @param \Combodo\iTop\Form\Field\Field $oField
  240. * @param array $aDependsOnIds
  241. * @return \Combodo\iTop\Form\Form
  242. */
  243. public function AddField(Field $oField, $aDependsOnIds = array())
  244. {
  245. $oField->SetFormPath($this->sId);
  246. $this->aFields[$oField->GetId()] = $oField;
  247. return $this;
  248. }
  249. /**
  250. *
  251. * @param string $sId
  252. * @return \Combodo\iTop\Form\Form
  253. */
  254. public function RemoveField($sId)
  255. {
  256. if (array_key_exists($sId, $this->aFields))
  257. {
  258. unset($this->aFields[$sId]);
  259. }
  260. return $this;
  261. }
  262. /**
  263. * Returns a array (list) of the fields ordered by their dependencies.
  264. *
  265. * @return array
  266. */
  267. public function GetOrderedFields()
  268. {
  269. // TODO : Do this so it flatten the array
  270. return $this->aFields;
  271. }
  272. /**
  273. * Returns an array of field ids the $sFieldId depends on.
  274. *
  275. * @param string $sFieldId
  276. * @return array
  277. * @throws Exception
  278. */
  279. public function GetFieldDependencies($sFieldId)
  280. {
  281. if (!array_key_exists($sFieldId, $this->aDependencies))
  282. {
  283. throw new Exception('Field with ID "' . $sFieldId . '" had no dependancies declared in the Form.');
  284. }
  285. return $this->aDependencies[$sFieldId];
  286. }
  287. /**
  288. *
  289. * @param string $sFieldId
  290. * @param array $aDependsOnIds
  291. * @return \Combodo\iTop\Form\Form
  292. */
  293. public function AddFieldDependencies($sFieldId, array $aDependsOnIds)
  294. {
  295. foreach ($aDependsOnIds as $sDependsOnId)
  296. {
  297. $this->AddFieldDependency($sFieldId, $sDependsOnId);
  298. }
  299. return $this;
  300. }
  301. /**
  302. *
  303. * @param string $sFieldId
  304. * @param string $sDependsOnId
  305. * @return \Combodo\iTop\Form\Form
  306. */
  307. public function AddFieldDependency($sFieldId, $sDependsOnId)
  308. {
  309. if (!array_key_exists($sFieldId, $this->aDependencies))
  310. {
  311. $this->aDependencies[$sFieldId] = array();
  312. }
  313. $this->aDependencies[$sFieldId][] = $sDependsOnId;
  314. return $this;
  315. }
  316. /**
  317. * Returns a hash array of the fields impacts on other fields. Key being the field that impacts the fields stored in the value as a regular array
  318. * (It kind of reversed the dependencies array)
  319. *
  320. * eg :
  321. * - 'service' => array('subservice', 'template')
  322. * - 'subservice' => array()
  323. * - ...
  324. *
  325. * @return array
  326. */
  327. public function GetFieldsImpacts()
  328. {
  329. $aRes = array();
  330. foreach ($this->aDependencies as $sImpactedFieldId => $aDependentFieldsIds)
  331. {
  332. foreach ($aDependentFieldsIds as $sDependentFieldId)
  333. {
  334. if (!array_key_exists($sDependentFieldId, $aRes))
  335. {
  336. $aRes[$sDependentFieldId] = array();
  337. }
  338. $aRes[$sDependentFieldId][] = $sImpactedFieldId;
  339. }
  340. }
  341. return $aRes;
  342. }
  343. /**
  344. * Returns the number of editable fields in this form.
  345. *
  346. * @return integer
  347. */
  348. public function GetEditableFieldCount($bForce = false)
  349. {
  350. // Count is usally done by the Finalize function but it can be done there if Finalize hasn't been called yet or if we choose to force it.
  351. if (($this->iEditableFieldCount === null) || ($bForce === true))
  352. {
  353. $this->iEditableFieldCount = 0;
  354. foreach ($this->aFields as $oField)
  355. {
  356. if ($oField->IsEditable())
  357. {
  358. $this->iEditableFieldCount++;
  359. }
  360. }
  361. }
  362. return $this->iEditableFieldCount;
  363. }
  364. /**
  365. * Returns true if the form has at least one editable field
  366. *
  367. * @return boolean
  368. */
  369. public function HasEditableFields()
  370. {
  371. return ($this->GetEditableFieldCount() > 0);
  372. }
  373. /**
  374. * Returns true if the form has at least one editable field
  375. *
  376. * @return boolean
  377. */
  378. public function HasVisibleFields()
  379. {
  380. $bRet = false;
  381. foreach ($this->aFields as $oField)
  382. {
  383. if (!$oField->GetHidden())
  384. {
  385. $bRet = true;
  386. break;
  387. }
  388. }
  389. return $bRet;
  390. }
  391. /**
  392. * Forces the form to a read only state by setting read only to true on all its fields
  393. *
  394. * @return \Combodo\iTop\Form\Form
  395. */
  396. public function MakeReadOnly()
  397. {
  398. foreach ($this->GetFields() as $oField)
  399. {
  400. $oField->SetReadOnly(true);
  401. }
  402. return $this;
  403. }
  404. /**
  405. * @param $sFormPath
  406. * @return Form|null
  407. */
  408. public function FindSubForm($sFormPath)
  409. {
  410. $ret = null;
  411. if ($sFormPath == $this->sId)
  412. {
  413. $ret = $this;
  414. }
  415. else
  416. {
  417. foreach ($this->aFields as $oField)
  418. {
  419. if ($oField instanceof SubFormField)
  420. {
  421. $ret = $oField->FindSubForm($sFormPath);
  422. if ($ret !== null) break;
  423. }
  424. }
  425. }
  426. return $ret;
  427. }
  428. /**
  429. * Finalizes each field, following the dependencies so that a field can compute its value or other properties,
  430. * depending on other fields
  431. */
  432. public function Finalize()
  433. {
  434. $aFieldList = array(); // Fields ordered by dependence
  435. // Clone the dependency data : $aDependencies will be truncated as the fields are added to the list
  436. $aDependencies = $this->aDependencies;
  437. $bMadeProgress = true; // Safety net in case of circular references
  438. foreach ($aDependencies as $sImpactedBy => $aSomeFields)
  439. {
  440. foreach ($aSomeFields as $i => $sSomeId)
  441. {
  442. if (!array_key_exists($sSomeId, $this->aFields))
  443. {
  444. throw new Exception('Unmet dependency : Field ' . $sImpactedBy . ' expecting field ' . $sSomeId . ' which is not in the Form');
  445. }
  446. }
  447. }
  448. while ($bMadeProgress && count($aFieldList) < count($this->aFields))
  449. {
  450. $bMadeProgress = false;
  451. foreach ($this->aFields as $sId => $oField)
  452. {
  453. if (array_key_exists($sId, $aFieldList))
  454. continue;
  455. if (isset($aDependencies[$sId]) && count($aDependencies[$sId]) > 0) continue;
  456. // Add the field at the end of the list
  457. $aFieldList[$sId] = $oField;
  458. $bMadeProgress = true;
  459. // Track that this dependency has been solved
  460. foreach ($aDependencies as $sImpactedBy => $aSomeFields)
  461. {
  462. foreach ($aSomeFields as $i => $sSomeId)
  463. {
  464. if ($sSomeId == $sId)
  465. {
  466. unset($aDependencies[$sImpactedBy][$i]);
  467. }
  468. }
  469. }
  470. }
  471. }
  472. if (!$bMadeProgress)
  473. {
  474. throw new Exception('Unmet dependencies (might be a circular reference) : ' . implode(', ', array_keys($aDependencies)));
  475. }
  476. foreach ($aFieldList as $sId => $oField)
  477. {
  478. $oField->OnFinalize();
  479. if ($oField->IsEditable())
  480. {
  481. $this->iEditableFieldCount++;
  482. }
  483. }
  484. }
  485. /**
  486. * Validate the form and return if it's valid or not
  487. *
  488. * @return boolean
  489. */
  490. public function Validate()
  491. {
  492. $this->SetValid(true);
  493. $this->EmptyErrorMessages();
  494. foreach ($this->aFields as $oField)
  495. {
  496. if (!$oField->Validate())
  497. {
  498. $this->SetValid(false);
  499. foreach ($oField->GetErrorMessages() as $sErrorMessage)
  500. {
  501. $this->AddErrorMessage(Dict::S($sErrorMessage), $oField->Getid());
  502. }
  503. }
  504. }
  505. return $this->GetValid();
  506. }
  507. }