FormHelper.php 95 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 0.10.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\View\Helper;
  17. use BackedEnum;
  18. use Cake\Core\Configure;
  19. use Cake\Core\Exception\CakeException;
  20. use Cake\Database\Type\EnumLabelInterface;
  21. use Cake\Database\Type\EnumType;
  22. use Cake\Database\TypeFactory;
  23. use Cake\Form\FormProtector;
  24. use Cake\Routing\Router;
  25. use Cake\Utility\Hash;
  26. use Cake\Utility\Inflector;
  27. use Cake\View\Form\ContextFactory;
  28. use Cake\View\Form\ContextInterface;
  29. use Cake\View\Helper;
  30. use Cake\View\StringTemplateTrait;
  31. use Cake\View\View;
  32. use Cake\View\Widget\WidgetInterface;
  33. use Cake\View\Widget\WidgetLocator;
  34. use InvalidArgumentException;
  35. use function Cake\Core\h;
  36. use function Cake\I18n\__;
  37. use function Cake\I18n\__d;
  38. /**
  39. * Form helper library.
  40. *
  41. * Automatic generation of HTML FORMs from given data.
  42. *
  43. * @method string text(string $fieldName, array $options = []) Creates input of type text.
  44. * @method string number(string $fieldName, array $options = []) Creates input of type number.
  45. * @method string email(string $fieldName, array $options = []) Creates input of type email.
  46. * @method string password(string $fieldName, array $options = []) Creates input of type password.
  47. * @method string search(string $fieldName, array $options = []) Creates input of type search.
  48. * @property \Cake\View\Helper\HtmlHelper $Html
  49. * @property \Cake\View\Helper\UrlHelper $Url
  50. * @link https://book.cakephp.org/5/en/views/helpers/form.html
  51. */
  52. class FormHelper extends Helper
  53. {
  54. use IdGeneratorTrait;
  55. use StringTemplateTrait;
  56. /**
  57. * Other helpers used by FormHelper
  58. *
  59. * @var array
  60. */
  61. protected array $helpers = ['Url', 'Html'];
  62. /**
  63. * Default config for the helper.
  64. *
  65. * @var array<string, mixed>
  66. */
  67. protected array $_defaultConfig = [
  68. 'idPrefix' => null,
  69. 'errorClass' => 'form-error',
  70. 'typeMap' => [
  71. 'string' => 'text',
  72. 'text' => 'textarea',
  73. 'uuid' => 'string',
  74. 'datetime' => 'datetime',
  75. 'datetimefractional' => 'datetime',
  76. 'timestamp' => 'datetime',
  77. 'timestampfractional' => 'datetime',
  78. 'timestamptimezone' => 'datetime',
  79. 'date' => 'date',
  80. 'time' => 'time',
  81. 'year' => 'year',
  82. 'boolean' => 'checkbox',
  83. 'float' => 'number',
  84. 'integer' => 'number',
  85. 'tinyinteger' => 'number',
  86. 'smallinteger' => 'number',
  87. 'decimal' => 'number',
  88. 'binary' => 'file',
  89. ],
  90. 'templates' => [
  91. // Used for button elements in button().
  92. 'button' => '<button{{attrs}}>{{text}}</button>',
  93. // Used for checkboxes in checkbox() and multiCheckbox().
  94. 'checkbox' => '<input type="checkbox" name="{{name}}" value="{{value}}"{{attrs}}>',
  95. // Input group wrapper for checkboxes created via control().
  96. 'checkboxFormGroup' => '{{label}}',
  97. // Wrapper container for checkboxes.
  98. 'checkboxWrapper' => '<div class="checkbox">{{label}}</div>',
  99. // Error message wrapper elements.
  100. 'error' => '<div class="error-message" id="{{id}}">{{content}}</div>',
  101. // Container for error items.
  102. 'errorList' => '<ul>{{content}}</ul>',
  103. // Error item wrapper.
  104. 'errorItem' => '<li>{{text}}</li>',
  105. // File input used by file().
  106. 'file' => '<input type="file" name="{{name}}"{{attrs}}>',
  107. // Fieldset element used by allControls().
  108. 'fieldset' => '<fieldset{{attrs}}>{{content}}</fieldset>',
  109. // Open tag used by create().
  110. 'formStart' => '<form{{attrs}}>',
  111. // Close tag used by end().
  112. 'formEnd' => '</form>',
  113. // General grouping container for control(). Defines input/label ordering.
  114. 'formGroup' => '{{label}}{{input}}',
  115. // Wrapper content used to hide other content.
  116. 'hiddenBlock' => '<div style="display:none;">{{content}}</div>',
  117. // Generic input element.
  118. 'input' => '<input type="{{type}}" name="{{name}}"{{attrs}}>',
  119. // Submit input element.
  120. 'inputSubmit' => '<input type="{{type}}"{{attrs}}>',
  121. // Container element used by control().
  122. 'inputContainer' => '<div class="input {{type}}{{required}}">{{content}}</div>',
  123. // Container element used by control() when a field has an error.
  124. 'inputContainerError' => '<div class="input {{type}}{{required}} error">{{content}}{{error}}</div>',
  125. // Label element when inputs are not nested inside the label.
  126. 'label' => '<label{{attrs}}>{{text}}</label>',
  127. // Label element used for radio and multi-checkbox inputs.
  128. 'nestingLabel' => '{{hidden}}<label{{attrs}}>{{input}}{{text}}</label>',
  129. // Legends created by allControls()
  130. 'legend' => '<legend>{{text}}</legend>',
  131. // Multi-Checkbox input set title element.
  132. 'multicheckboxTitle' => '<legend>{{text}}</legend>',
  133. // Multi-Checkbox wrapping container.
  134. 'multicheckboxWrapper' => '<fieldset{{attrs}}>{{content}}</fieldset>',
  135. // Option element used in select pickers.
  136. 'option' => '<option value="{{value}}"{{attrs}}>{{text}}</option>',
  137. // Option group element used in select pickers.
  138. 'optgroup' => '<optgroup label="{{label}}"{{attrs}}>{{content}}</optgroup>',
  139. // Select element,
  140. 'select' => '<select name="{{name}}"{{attrs}}>{{content}}</select>',
  141. // Multi-select element,
  142. 'selectMultiple' => '<select name="{{name}}[]" multiple="multiple"{{attrs}}>{{content}}</select>',
  143. // Radio input element,
  144. 'radio' => '<input type="radio" name="{{name}}" value="{{value}}"{{attrs}}>',
  145. // Wrapping container for radio input/label,
  146. 'radioWrapper' => '{{label}}',
  147. // Textarea input element,
  148. 'textarea' => '<textarea name="{{name}}"{{attrs}}>{{value}}</textarea>',
  149. // Container for submit buttons.
  150. 'submitContainer' => '<div class="submit">{{content}}</div>',
  151. // Confirm javascript template for postLink()
  152. 'confirmJs' => '{{confirm}}',
  153. // selected class
  154. 'selectedClass' => 'selected',
  155. // required class
  156. 'requiredClass' => 'required',
  157. ],
  158. // set HTML5 validation message to custom required/empty messages
  159. 'autoSetCustomValidity' => true,
  160. ];
  161. /**
  162. * Default widgets
  163. *
  164. * @var array<string, array<string>>
  165. */
  166. protected array $_defaultWidgets = [
  167. 'button' => ['Button'],
  168. 'checkbox' => ['Checkbox'],
  169. 'file' => ['File'],
  170. 'label' => ['Label'],
  171. 'nestingLabel' => ['NestingLabel'],
  172. 'multicheckbox' => ['MultiCheckbox', 'nestingLabel'],
  173. 'radio' => ['Radio', 'nestingLabel'],
  174. 'select' => ['SelectBox'],
  175. 'textarea' => ['Textarea'],
  176. 'datetime' => ['DateTime', 'select'],
  177. 'year' => ['Year', 'select'],
  178. '_default' => ['Basic'],
  179. ];
  180. /**
  181. * Constant used internally to skip the securing process,
  182. * and neither add the field to the hash or to the unlocked fields.
  183. *
  184. * @var string
  185. */
  186. public const SECURE_SKIP = 'skip';
  187. /**
  188. * Defines the type of form being created. Set by FormHelper::create().
  189. *
  190. * @var string|null
  191. */
  192. public ?string $requestType = null;
  193. /**
  194. * Locator for input widgets.
  195. *
  196. * @var \Cake\View\Widget\WidgetLocator
  197. */
  198. protected WidgetLocator $_locator;
  199. /**
  200. * Context for the current form.
  201. *
  202. * @var \Cake\View\Form\ContextInterface|null
  203. */
  204. protected ?ContextInterface $_context = null;
  205. /**
  206. * Context factory.
  207. *
  208. * @var \Cake\View\Form\ContextFactory|null
  209. */
  210. protected ?ContextFactory $_contextFactory = null;
  211. /**
  212. * The action attribute value of the last created form.
  213. * Used to make form/request specific hashes for form tampering protection.
  214. *
  215. * @var string
  216. */
  217. protected string $_lastAction = '';
  218. /**
  219. * The supported sources that can be used to populate input values.
  220. *
  221. * `context` - Corresponds to `ContextInterface` instances.
  222. * `data` - Corresponds to request data (POST/PUT).
  223. * `query` - Corresponds to request's query string.
  224. *
  225. * @var array<string>
  226. */
  227. protected array $supportedValueSources = ['context', 'data', 'query'];
  228. /**
  229. * The default sources.
  230. *
  231. * @see FormHelper::$supportedValueSources for valid values.
  232. * @var array<string>
  233. */
  234. protected array $_valueSources = ['data', 'context'];
  235. /**
  236. * Grouped input types.
  237. *
  238. * @var array<string>
  239. */
  240. protected array $_groupedInputTypes = ['radio', 'multicheckbox'];
  241. /**
  242. * Form protector
  243. *
  244. * @var \Cake\Form\FormProtector|null
  245. */
  246. protected ?FormProtector $formProtector = null;
  247. /**
  248. * Construct the widgets and binds the default context providers
  249. *
  250. * @param \Cake\View\View $view The View this helper is being attached to.
  251. * @param array<string, mixed> $config Configuration settings for the helper.
  252. */
  253. public function __construct(View $view, array $config = [])
  254. {
  255. $locator = null;
  256. $widgets = $this->_defaultWidgets;
  257. if (isset($config['locator'])) {
  258. $locator = $config['locator'];
  259. unset($config['locator']);
  260. }
  261. if (isset($config['widgets'])) {
  262. if (is_string($config['widgets'])) {
  263. $config['widgets'] = (array)$config['widgets'];
  264. }
  265. $widgets = $config['widgets'] + $widgets;
  266. unset($config['widgets']);
  267. }
  268. if (isset($config['groupedInputTypes'])) {
  269. $this->_groupedInputTypes = $config['groupedInputTypes'];
  270. unset($config['groupedInputTypes']);
  271. }
  272. parent::__construct($view, $config);
  273. if (!$locator) {
  274. $locator = new WidgetLocator($this->templater(), $this->_View, $widgets);
  275. }
  276. $this->setWidgetLocator($locator);
  277. $this->_idPrefix = $this->getConfig('idPrefix');
  278. }
  279. /**
  280. * Get the widget locator currently used by the helper.
  281. *
  282. * @return \Cake\View\Widget\WidgetLocator Current locator instance
  283. * @since 3.6.0
  284. */
  285. public function getWidgetLocator(): WidgetLocator
  286. {
  287. return $this->_locator;
  288. }
  289. /**
  290. * Set the widget locator the helper will use.
  291. *
  292. * @param \Cake\View\Widget\WidgetLocator $instance The locator instance to set.
  293. * @return $this
  294. * @since 3.6.0
  295. */
  296. public function setWidgetLocator(WidgetLocator $instance)
  297. {
  298. $this->_locator = $instance;
  299. return $this;
  300. }
  301. /**
  302. * Set the context factory the helper will use.
  303. *
  304. * @param \Cake\View\Form\ContextFactory|null $instance The context factory instance to set.
  305. * @param array $contexts An array of context providers.
  306. * @return \Cake\View\Form\ContextFactory
  307. */
  308. public function contextFactory(?ContextFactory $instance = null, array $contexts = []): ContextFactory
  309. {
  310. if ($instance === null) {
  311. return $this->_contextFactory ??= ContextFactory::createWithDefaults($contexts);
  312. }
  313. $this->_contextFactory = $instance;
  314. return $this->_contextFactory;
  315. }
  316. /**
  317. * Returns an HTML form element.
  318. *
  319. * ### Options:
  320. *
  321. * - `type` Form method defaults to autodetecting based on the form context. If
  322. * the form context's isCreate() method returns false, a PUT request will be done.
  323. * - `method` Set the form's method attribute explicitly.
  324. * - `url` The URL the form submits to. Can be a string or a URL array.
  325. * - `encoding` Set the accept-charset encoding for the form. Defaults to `Configure::read('App.encoding')`
  326. * - `enctype` Set the form encoding explicitly. By default `type => file` will set `enctype`
  327. * to `multipart/form-data`.
  328. * - `templates` The templates you want to use for this form. Any templates will be merged on top of
  329. * the already loaded templates. This option can either be a filename in /config that contains
  330. * the templates you want to load, or an array of templates to use.
  331. * - `context` Additional options for the context class. For example the EntityContext accepts a 'table'
  332. * option that allows you to set the specific Table class the form should be based on.
  333. * - `idPrefix` Prefix for generated ID attributes.
  334. * - `valueSources` The sources that values should be read from. See FormHelper::setValueSources()
  335. * - `templateVars` Provide template variables for the formStart template.
  336. *
  337. * @param mixed $context The context for which the form is being defined.
  338. * Can be a ContextInterface instance, ORM entity, ORM resultset, or an
  339. * array of meta data. You can use `null` to make a context-less form.
  340. * @param array<string, mixed> $options An array of html attributes and options.
  341. * @return string An formatted opening FORM tag.
  342. * @link https://book.cakephp.org/5/en/views/helpers/form.html#Cake\View\Helper\FormHelper::create
  343. */
  344. public function create(mixed $context = null, array $options = []): string
  345. {
  346. $append = '';
  347. if ($context instanceof ContextInterface) {
  348. $this->context($context);
  349. } else {
  350. if (empty($options['context'])) {
  351. $options['context'] = [];
  352. }
  353. $options['context']['entity'] = $context;
  354. $context = $this->_getContext($options['context']);
  355. unset($options['context']);
  356. }
  357. $isCreate = $context->isCreate();
  358. $options += [
  359. 'type' => $isCreate ? 'post' : 'put',
  360. 'url' => null,
  361. 'encoding' => strtolower(Configure::read('App.encoding')),
  362. 'templates' => null,
  363. 'idPrefix' => null,
  364. 'valueSources' => null,
  365. ];
  366. if (isset($options['valueSources'])) {
  367. $this->setValueSources($options['valueSources']);
  368. unset($options['valueSources']);
  369. }
  370. if ($options['idPrefix'] !== null) {
  371. $this->_idPrefix = $options['idPrefix'];
  372. }
  373. $templater = $this->templater();
  374. if (!empty($options['templates'])) {
  375. $templater->push();
  376. $method = is_string($options['templates']) ? 'load' : 'add';
  377. $templater->{$method}($options['templates']);
  378. }
  379. unset($options['templates']);
  380. if ($options['url'] === false) {
  381. $url = $this->_View->getRequest()->getRequestTarget();
  382. $action = null;
  383. } else {
  384. $url = $this->_formUrl($context, $options);
  385. $action = $this->Url->build($url);
  386. }
  387. $this->_lastAction($url);
  388. unset($options['url'], $options['idPrefix']);
  389. $htmlAttributes = [];
  390. switch (strtolower($options['type'])) {
  391. case 'get':
  392. $htmlAttributes['method'] = 'get';
  393. break;
  394. // Set enctype for form
  395. case 'file':
  396. $htmlAttributes['enctype'] = 'multipart/form-data';
  397. $options['type'] = $isCreate ? 'post' : 'put';
  398. // Move on
  399. case 'put':
  400. // Move on
  401. case 'delete':
  402. // Set patch method
  403. case 'patch':
  404. $append .= $this->hidden('_method', [
  405. 'name' => '_method',
  406. 'value' => strtoupper($options['type']),
  407. 'secure' => static::SECURE_SKIP,
  408. ]);
  409. // Default to post method
  410. default:
  411. $htmlAttributes['method'] = 'post';
  412. }
  413. if (isset($options['method'])) {
  414. $htmlAttributes['method'] = strtolower($options['method']);
  415. }
  416. if (isset($options['enctype'])) {
  417. $htmlAttributes['enctype'] = strtolower($options['enctype']);
  418. }
  419. $this->requestType = strtolower($options['type']);
  420. if (!empty($options['encoding'])) {
  421. $htmlAttributes['accept-charset'] = $options['encoding'];
  422. }
  423. unset($options['type'], $options['encoding']);
  424. $htmlAttributes += $options;
  425. if ($this->requestType !== 'get') {
  426. $formTokenData = $this->_View->getRequest()->getAttribute('formTokenData');
  427. if ($formTokenData !== null) {
  428. $this->formProtector = $this->createFormProtector($formTokenData);
  429. }
  430. $append .= $this->_csrfField();
  431. }
  432. if (!empty($append)) {
  433. $append = $templater->format('hiddenBlock', ['content' => $append]);
  434. }
  435. $actionAttr = $templater->formatAttributes(['action' => $action, 'escape' => false]);
  436. return $this->formatTemplate('formStart', [
  437. 'attrs' => $templater->formatAttributes($htmlAttributes) . $actionAttr,
  438. 'templateVars' => $options['templateVars'] ?? [],
  439. ]) . $append;
  440. }
  441. /**
  442. * Create the URL for a form based on the options.
  443. *
  444. * @param \Cake\View\Form\ContextInterface $context The context object to use.
  445. * @param array<string, mixed> $options An array of options from create()
  446. * @return array|string The action attribute for the form.
  447. */
  448. protected function _formUrl(ContextInterface $context, array $options): array|string
  449. {
  450. $request = $this->_View->getRequest();
  451. if ($options['url'] === null) {
  452. return $request->getRequestTarget();
  453. }
  454. if (
  455. is_string($options['url']) ||
  456. (is_array($options['url']) &&
  457. isset($options['url']['_name']))
  458. ) {
  459. return $options['url'];
  460. }
  461. $actionDefaults = [
  462. 'plugin' => $this->_View->getPlugin(),
  463. 'controller' => $request->getParam('controller'),
  464. 'action' => $request->getParam('action'),
  465. ];
  466. return (array)$options['url'] + $actionDefaults;
  467. }
  468. /**
  469. * Correctly store the last created form action URL.
  470. *
  471. * @param array|string|null $url The URL of the last form.
  472. * @return void
  473. */
  474. protected function _lastAction(array|string|null $url = null): void
  475. {
  476. $action = Router::url($url, true);
  477. $query = parse_url($action, PHP_URL_QUERY);
  478. $query = $query ? '?' . $query : '';
  479. $path = parse_url($action, PHP_URL_PATH) ?: '';
  480. $this->_lastAction = $path . $query;
  481. }
  482. /**
  483. * Return a CSRF input if the request data is present.
  484. * Used to secure forms in conjunction with CsrfMiddleware.
  485. *
  486. * @return string
  487. */
  488. protected function _csrfField(): string
  489. {
  490. $request = $this->_View->getRequest();
  491. $csrfToken = $request->getAttribute('csrfToken');
  492. if (!$csrfToken) {
  493. return '';
  494. }
  495. return $this->hidden('_csrfToken', [
  496. 'value' => $csrfToken,
  497. 'secure' => static::SECURE_SKIP,
  498. ]);
  499. }
  500. /**
  501. * Closes an HTML form, cleans up values set by FormHelper::create(), and writes hidden
  502. * input fields where appropriate.
  503. *
  504. * Resets some parts of the state, shared among multiple FormHelper::create() calls, to defaults.
  505. *
  506. * @param array<string, mixed> $secureAttributes Secure attributes which will be passed as HTML attributes
  507. * into the hidden input elements generated for the Security Component.
  508. * @return string A closing FORM tag.
  509. * @link https://book.cakephp.org/5/en/views/helpers/form.html#closing-the-form
  510. */
  511. public function end(array $secureAttributes = []): string
  512. {
  513. $out = '';
  514. if ($this->requestType !== 'get' && $this->_View->getRequest()->getAttribute('formTokenData') !== null) {
  515. $out .= $this->secure([], $secureAttributes);
  516. }
  517. $out .= $this->formatTemplate('formEnd', []);
  518. $this->templater()->pop();
  519. $this->requestType = null;
  520. $this->_context = null;
  521. $this->_valueSources = ['data', 'context'];
  522. $this->_idPrefix = $this->getConfig('idPrefix');
  523. $this->formProtector = null;
  524. return $out;
  525. }
  526. /**
  527. * Generates a hidden field with a security hash based on the fields used in
  528. * the form.
  529. *
  530. * If $secureAttributes is set, these HTML attributes will be merged into
  531. * the hidden input tags generated for the Security Component. This is
  532. * especially useful to set HTML5 attributes like 'form'.
  533. *
  534. * @param array $fields If set specifies the list of fields to be added to
  535. * FormProtector for generating the hash.
  536. * @param array<string, mixed> $secureAttributes will be passed as HTML attributes into the hidden
  537. * input elements generated for the Security Component.
  538. * @return string A hidden input field with a security hash, or empty string when
  539. * secured forms are not in use.
  540. */
  541. public function secure(array $fields = [], array $secureAttributes = []): string
  542. {
  543. if (!$this->formProtector) {
  544. return '';
  545. }
  546. foreach ($fields as $field => $value) {
  547. if (is_int($field)) {
  548. $field = $value;
  549. $value = null;
  550. }
  551. $this->formProtector->addField($field, true, $value);
  552. }
  553. $debugSecurity = (bool)Configure::read('debug');
  554. if (isset($secureAttributes['debugSecurity'])) {
  555. $debugSecurity = $debugSecurity && $secureAttributes['debugSecurity'];
  556. unset($secureAttributes['debugSecurity']);
  557. }
  558. $secureAttributes['secure'] = static::SECURE_SKIP;
  559. $tokenData = $this->formProtector->buildTokenData(
  560. $this->_lastAction,
  561. $this->_getFormProtectorSessionId()
  562. );
  563. $tokenFields = array_merge($secureAttributes, [
  564. 'value' => $tokenData['fields'],
  565. ]);
  566. $out = $this->hidden('_Token.fields', $tokenFields);
  567. $tokenUnlocked = array_merge($secureAttributes, [
  568. 'value' => $tokenData['unlocked'],
  569. ]);
  570. $out .= $this->hidden('_Token.unlocked', $tokenUnlocked);
  571. if ($debugSecurity) {
  572. $tokenDebug = array_merge($secureAttributes, [
  573. 'value' => $tokenData['debug'],
  574. ]);
  575. $out .= $this->hidden('_Token.debug', $tokenDebug);
  576. }
  577. return $this->formatTemplate('hiddenBlock', ['content' => $out]);
  578. }
  579. /**
  580. * Get Session id for FormProtector
  581. * Must be the same as in FormProtectionComponent
  582. *
  583. * @return string
  584. */
  585. protected function _getFormProtectorSessionId(): string
  586. {
  587. return $this->_View->getRequest()->getSession()->id();
  588. }
  589. /**
  590. * Add to the list of fields that are currently unlocked.
  591. *
  592. * Unlocked fields are not included in the form protection field hash.
  593. *
  594. * @param string $name The dot separated name for the field.
  595. * @return $this
  596. */
  597. public function unlockField(string $name)
  598. {
  599. $this->getFormProtector()->unlockField($name);
  600. return $this;
  601. }
  602. /**
  603. * Create FormProtector instance.
  604. *
  605. * @param array<string, mixed> $formTokenData Token data.
  606. * @return \Cake\Form\FormProtector
  607. */
  608. protected function createFormProtector(array $formTokenData): FormProtector
  609. {
  610. $session = $this->_View->getRequest()->getSession();
  611. $session->start();
  612. return new FormProtector(
  613. $formTokenData
  614. );
  615. }
  616. /**
  617. * Get form protector instance.
  618. *
  619. * @return \Cake\Form\FormProtector
  620. * @throws \Cake\Core\Exception\CakeException
  621. */
  622. public function getFormProtector(): FormProtector
  623. {
  624. if ($this->formProtector === null) {
  625. throw new CakeException(
  626. '`FormProtector` instance has not been created. Ensure you have loaded the `FormProtectionComponent`'
  627. . ' in your controller and called `FormHelper::create()` before calling `FormHelper::unlockField()`.'
  628. );
  629. }
  630. return $this->formProtector;
  631. }
  632. /**
  633. * Returns true if there is an error for the given field, otherwise false
  634. *
  635. * @param string $field This should be "modelname.fieldname"
  636. * @return bool If there are errors this method returns true, else false.
  637. * @link https://book.cakephp.org/5/en/views/helpers/form.html#displaying-and-checking-errors
  638. */
  639. public function isFieldError(string $field): bool
  640. {
  641. return $this->_getContext()->hasError($field);
  642. }
  643. /**
  644. * Returns a formatted error message for given form field, '' if no errors.
  645. *
  646. * Uses the `error`, `errorList` and `errorItem` templates. The `errorList` and
  647. * `errorItem` templates are used to format multiple error messages per field.
  648. *
  649. * ### Options:
  650. *
  651. * - `escape` boolean - Whether to html escape the contents of the error.
  652. *
  653. * @param string $field A field name, like "modelname.fieldname"
  654. * @param array|string|null $text Error message as string or array of messages. If an array,
  655. * it should be a hash of key names => messages.
  656. * @param array<string, mixed> $options See above.
  657. * @return string Formatted errors or ''.
  658. * @link https://book.cakephp.org/5/en/views/helpers/form.html#displaying-and-checking-errors
  659. */
  660. public function error(string $field, array|string|null $text = null, array $options = []): string
  661. {
  662. if (str_ends_with($field, '._ids')) {
  663. $field = substr($field, 0, -5);
  664. }
  665. $options += ['escape' => true];
  666. $context = $this->_getContext();
  667. if (!$context->hasError($field)) {
  668. return '';
  669. }
  670. $error = $context->error($field);
  671. if (is_array($text)) {
  672. $tmp = [];
  673. foreach ($error as $k => $e) {
  674. if (isset($text[$k])) {
  675. $tmp[] = $text[$k];
  676. } elseif (isset($text[$e])) {
  677. $tmp[] = $text[$e];
  678. } else {
  679. $tmp[] = $e;
  680. }
  681. }
  682. $text = $tmp;
  683. }
  684. if ($text !== null) {
  685. $error = $text;
  686. }
  687. if ($options['escape']) {
  688. $error = h($error);
  689. unset($options['escape']);
  690. }
  691. if (is_array($error)) {
  692. if (count($error) > 1) {
  693. $errorText = [];
  694. foreach ($error as $err) {
  695. $errorText[] = $this->formatTemplate('errorItem', ['text' => $err]);
  696. }
  697. $error = $this->formatTemplate('errorList', [
  698. 'content' => implode('', $errorText),
  699. ]);
  700. } else {
  701. $error = array_pop($error);
  702. }
  703. }
  704. return $this->formatTemplate('error', [
  705. 'content' => $error,
  706. 'id' => $this->_domId($field) . '-error',
  707. ]);
  708. }
  709. /**
  710. * Returns a formatted LABEL element for HTML forms.
  711. *
  712. * Will automatically generate a `for` attribute if one is not provided.
  713. *
  714. * ### Options
  715. *
  716. * - `for` - Set the for attribute, if its not defined the for attribute
  717. * will be generated from the $fieldName parameter using
  718. * FormHelper::_domId().
  719. * - `escape` - Set to `false` to turn off escaping of label text.
  720. * Defaults to `true`.
  721. *
  722. * Examples:
  723. *
  724. * The text and for attribute are generated off of the fieldname
  725. *
  726. * ```
  727. * echo $this->Form->label('published');
  728. * <label for="PostPublished">Published</label>
  729. * ```
  730. *
  731. * Custom text:
  732. *
  733. * ```
  734. * echo $this->Form->label('published', 'Publish');
  735. * <label for="published">Publish</label>
  736. * ```
  737. *
  738. * Custom attributes:
  739. *
  740. * ```
  741. * echo $this->Form->label('published', 'Publish', [
  742. * 'for' => 'post-publish'
  743. * ]);
  744. * <label for="post-publish">Publish</label>
  745. * ```
  746. *
  747. * Nesting an input tag:
  748. *
  749. * ```
  750. * echo $this->Form->label('published', 'Publish', [
  751. * 'for' => 'published',
  752. * 'input' => $this->text('published'),
  753. * ]);
  754. * <label for="post-publish">Publish <input type="text" name="published"></label>
  755. * ```
  756. *
  757. * If you want to nest inputs in the labels, you will need to modify the default templates.
  758. *
  759. * @param string $fieldName This should be "modelname.fieldname"
  760. * @param string|null $text Text that will appear in the label field. If
  761. * $text is left undefined the text will be inflected from the
  762. * fieldName.
  763. * @param array<string, mixed> $options An array of HTML attributes.
  764. * @return string The formatted LABEL element
  765. * @link https://book.cakephp.org/5/en/views/helpers/form.html#creating-labels
  766. */
  767. public function label(string $fieldName, ?string $text = null, array $options = []): string
  768. {
  769. if ($text === null) {
  770. $text = $fieldName;
  771. if (str_ends_with($text, '._ids')) {
  772. $text = substr($text, 0, -5);
  773. }
  774. if (str_contains($text, '.')) {
  775. $fieldElements = explode('.', $text);
  776. $text = array_pop($fieldElements);
  777. }
  778. if (str_ends_with($text, '_id')) {
  779. $text = substr($text, 0, -3);
  780. }
  781. $text = __(Inflector::humanize(Inflector::underscore($text)));
  782. }
  783. if (isset($options['for'])) {
  784. $labelFor = $options['for'];
  785. unset($options['for']);
  786. } else {
  787. $labelFor = $this->_domId($fieldName);
  788. }
  789. $attrs = $options + [
  790. 'for' => $labelFor,
  791. 'text' => $text,
  792. ];
  793. if (isset($options['input'])) {
  794. if (is_array($options['input'])) {
  795. $attrs = $options['input'] + $attrs;
  796. }
  797. return $this->widget('nestingLabel', $attrs);
  798. }
  799. return $this->widget('label', $attrs);
  800. }
  801. /**
  802. * Generate a set of controls for `$fields`. If $fields is empty the fields
  803. * of current model will be used.
  804. *
  805. * You can customize individual controls through `$fields`.
  806. * ```
  807. * $this->Form->allControls([
  808. * 'name' => ['label' => 'custom label']
  809. * ]);
  810. * ```
  811. *
  812. * You can exclude fields by specifying them as `false`:
  813. *
  814. * ```
  815. * $this->Form->allControls(['title' => false]);
  816. * ```
  817. *
  818. * In the above example, no field would be generated for the title field.
  819. *
  820. * @param array $fields An array of customizations for the fields that will be
  821. * generated. This array allows you to set custom types, labels, or other options.
  822. * @param array<string, mixed> $options Options array. Valid keys are:
  823. *
  824. * - `fieldset` Set to false to disable the fieldset. You can also pass an array of params to be
  825. * applied as HTML attributes to the fieldset tag. If you pass an empty array, the fieldset will
  826. * be enabled
  827. * - `legend` Set to false to disable the legend for the generated control set. Or supply a string
  828. * to customize the legend text.
  829. * @return string Completed form controls.
  830. * @link https://book.cakephp.org/5/en/views/helpers/form.html#generating-entire-forms
  831. */
  832. public function allControls(array $fields = [], array $options = []): string
  833. {
  834. $context = $this->_getContext();
  835. $modelFields = $context->fieldNames();
  836. $fields = array_merge(
  837. Hash::normalize($modelFields),
  838. Hash::normalize($fields)
  839. );
  840. return $this->controls($fields, $options);
  841. }
  842. /**
  843. * Generate a set of controls for `$fields` wrapped in a fieldset element.
  844. *
  845. * You can customize individual controls through `$fields`.
  846. * ```
  847. * $this->Form->controls([
  848. * 'name' => ['label' => 'custom label'],
  849. * 'email'
  850. * ]);
  851. * ```
  852. *
  853. * @param array $fields An array of the fields to generate. This array allows
  854. * you to set custom types, labels, or other options.
  855. * @param array<string, mixed> $options Options array. Valid keys are:
  856. *
  857. * - `fieldset` Set to false to disable the fieldset. You can also pass an
  858. * array of params to be applied as HTML attributes to the fieldset tag.
  859. * If you pass an empty array, the fieldset will be enabled.
  860. * - `legend` Set to false to disable the legend for the generated input set.
  861. * Or supply a string to customize the legend text.
  862. * @return string Completed form inputs.
  863. * @link https://book.cakephp.org/5/en/views/helpers/form.html#generating-entire-forms
  864. */
  865. public function controls(array $fields, array $options = []): string
  866. {
  867. $fields = Hash::normalize($fields);
  868. $out = '';
  869. foreach ($fields as $name => $opts) {
  870. if ($opts === false) {
  871. continue;
  872. }
  873. $out .= $this->control($name, (array)$opts);
  874. }
  875. return $this->fieldset($out, $options);
  876. }
  877. /**
  878. * Wrap a set of inputs in a fieldset
  879. *
  880. * @param string $fields the form inputs to wrap in a fieldset
  881. * @param array<string, mixed> $options Options array. Valid keys are:
  882. *
  883. * - `fieldset` Set to false to disable the fieldset. You can also pass an array of params to be
  884. * applied as HTML attributes to the fieldset tag. If you pass an empty array, the fieldset will
  885. * be enabled
  886. * - `legend` Set to false to disable the legend for the generated input set. Or supply a string
  887. * to customize the legend text.
  888. * @return string Completed form inputs.
  889. */
  890. public function fieldset(string $fields = '', array $options = []): string
  891. {
  892. $legend = $options['legend'] ?? true;
  893. $fieldset = $options['fieldset'] ?? true;
  894. $context = $this->_getContext();
  895. $out = $fields;
  896. if ($legend === true) {
  897. $isCreate = $context->isCreate();
  898. $modelName = Inflector::humanize(
  899. Inflector::singularize($this->_View->getRequest()->getParam('controller'))
  900. );
  901. if (!$isCreate) {
  902. $legend = __d('cake', 'Edit {0}', $modelName);
  903. } else {
  904. $legend = __d('cake', 'New {0}', $modelName);
  905. }
  906. }
  907. if ($fieldset !== false) {
  908. if ($legend) {
  909. $out = $this->formatTemplate('legend', ['text' => $legend]) . $out;
  910. }
  911. $fieldsetParams = ['content' => $out, 'attrs' => ''];
  912. if (is_array($fieldset) && !empty($fieldset)) {
  913. $fieldsetParams['attrs'] = $this->templater()->formatAttributes($fieldset);
  914. }
  915. $out = $this->formatTemplate('fieldset', $fieldsetParams);
  916. }
  917. return $out;
  918. }
  919. /**
  920. * Generates a form control element complete with label and wrapper div.
  921. *
  922. * ### Options
  923. *
  924. * See each field type method for more information. Any options that are part of
  925. * $attributes or $options for the different **type** methods can be included in `$options` for control().
  926. * Additionally, any unknown keys that are not in the list below, or part of the selected type's options
  927. * will be treated as a regular HTML attribute for the generated input.
  928. *
  929. * - `type` - Force the type of widget you want. e.g. `type => 'select'`
  930. * - `label` - Either a string label, or an array of options for the label. See FormHelper::label().
  931. * - `options` - For widgets that take options e.g. radio, select.
  932. * - `error` - Control the error message that is produced. Set to `false` to disable any kind of error reporting
  933. * (field error and error messages).
  934. * - `empty` - String or boolean to enable empty select box options.
  935. * - `nestedInput` - Used with checkbox and radio inputs. Set to false to render inputs outside of label
  936. * elements. Can be set to true on any input to force the input inside the label. If you
  937. * enable this option for radio buttons you will also need to modify the default `radioWrapper` template.
  938. * - `templates` - The templates you want to use for this input. Any templates will be merged on top of
  939. * the already loaded templates. This option can either be a filename in /config that contains
  940. * the templates you want to load, or an array of templates to use.
  941. * - `labelOptions` - Either `false` to disable label around nestedWidgets e.g. radio, multicheckbox or an array
  942. * of attributes for the label tag. `selected` will be added to any classes e.g. `class => 'myclass'` where
  943. * widget is checked
  944. *
  945. * @param string $fieldName This should be "modelname.fieldname"
  946. * @param array<string, mixed> $options Each type of input takes different options.
  947. * @return string Completed form widget.
  948. * @link https://book.cakephp.org/5/en/views/helpers/form.html#creating-form-controls
  949. */
  950. public function control(string $fieldName, array $options = []): string
  951. {
  952. $options += [
  953. 'type' => null,
  954. 'label' => null,
  955. 'error' => null,
  956. 'required' => null,
  957. 'options' => null,
  958. 'templates' => [],
  959. 'templateVars' => [],
  960. 'labelOptions' => true,
  961. ];
  962. $options = $this->_parseOptions($fieldName, $options);
  963. $options += ['id' => $this->_domId($fieldName)];
  964. $templater = $this->templater();
  965. $newTemplates = $options['templates'];
  966. if ($newTemplates) {
  967. $templater->push();
  968. $templateMethod = is_string($options['templates']) ? 'load' : 'add';
  969. $templater->{$templateMethod}($options['templates']);
  970. }
  971. unset($options['templates']);
  972. // Hidden inputs don't need aria.
  973. // Multiple checkboxes can't have aria generated for them at this layer.
  974. if ($options['type'] !== 'hidden' && ($options['type'] !== 'select' && !isset($options['multiple']))) {
  975. $isFieldError = $this->isFieldError($fieldName);
  976. $options += [
  977. 'aria-required' => $options['required'] ? 'true' : null,
  978. 'aria-invalid' => $isFieldError ? 'true' : null,
  979. ];
  980. // Don't include aria-describedby unless we have a good chance of
  981. // having error message show up.
  982. if (
  983. str_contains($templater->get('error'), '{{id}}') &&
  984. str_contains($templater->get('inputContainerError'), '{{error}}')
  985. ) {
  986. $options += [
  987. 'aria-describedby' => $isFieldError ? $this->_domId($fieldName) . '-error' : null,
  988. ];
  989. }
  990. if (isset($options['placeholder']) && $options['label'] === false) {
  991. $options += [
  992. 'aria-label' => $options['placeholder'],
  993. ];
  994. }
  995. }
  996. $error = null;
  997. $errorSuffix = '';
  998. if ($options['type'] !== 'hidden' && $options['error'] !== false) {
  999. if (is_array($options['error'])) {
  1000. $error = $this->error($fieldName, $options['error'], $options['error']);
  1001. } else {
  1002. $error = $this->error($fieldName, $options['error']);
  1003. }
  1004. $errorSuffix = empty($error) ? '' : 'Error';
  1005. unset($options['error']);
  1006. }
  1007. $label = $options['label'];
  1008. unset($options['label']);
  1009. $labelOptions = $options['labelOptions'];
  1010. unset($options['labelOptions']);
  1011. $nestedInput = false;
  1012. if ($options['type'] === 'checkbox') {
  1013. $nestedInput = true;
  1014. }
  1015. $nestedInput = $options['nestedInput'] ?? $nestedInput;
  1016. unset($options['nestedInput']);
  1017. if (
  1018. $nestedInput === true
  1019. && $options['type'] === 'checkbox'
  1020. && !array_key_exists('hiddenField', $options)
  1021. && $label !== false
  1022. ) {
  1023. $options['hiddenField'] = '_split';
  1024. }
  1025. /** @var string $input */
  1026. $input = $this->_getInput($fieldName, $options + ['labelOptions' => $labelOptions]);
  1027. if ($options['type'] === 'hidden' || $options['type'] === 'submit') {
  1028. if ($newTemplates) {
  1029. $templater->pop();
  1030. }
  1031. return $input;
  1032. }
  1033. $label = $this->_getLabel($fieldName, compact('input', 'label', 'error', 'nestedInput') + $options);
  1034. if ($nestedInput) {
  1035. $result = $this->_groupTemplate(compact('label', 'error', 'options'));
  1036. } else {
  1037. $result = $this->_groupTemplate(compact('input', 'label', 'error', 'options'));
  1038. }
  1039. $result = $this->_inputContainerTemplate([
  1040. 'content' => $result,
  1041. 'error' => $error,
  1042. 'errorSuffix' => $errorSuffix,
  1043. 'label' => $label,
  1044. 'options' => $options,
  1045. ]);
  1046. if ($newTemplates) {
  1047. $templater->pop();
  1048. }
  1049. return $result;
  1050. }
  1051. /**
  1052. * Generates an group template element
  1053. *
  1054. * @param array<string, mixed> $options The options for group template
  1055. * @return string The generated group template
  1056. */
  1057. protected function _groupTemplate(array $options): string
  1058. {
  1059. $groupTemplate = $options['options']['type'] . 'FormGroup';
  1060. if (!$this->templater()->get($groupTemplate)) {
  1061. $groupTemplate = 'formGroup';
  1062. }
  1063. return $this->formatTemplate($groupTemplate, [
  1064. 'input' => $options['input'] ?? [],
  1065. 'label' => $options['label'],
  1066. 'error' => $options['error'],
  1067. 'templateVars' => $options['options']['templateVars'] ?? [],
  1068. ]);
  1069. }
  1070. /**
  1071. * Generates an input container template
  1072. *
  1073. * @param array<string, mixed> $options The options for input container template
  1074. * @return string The generated input container template
  1075. */
  1076. protected function _inputContainerTemplate(array $options): string
  1077. {
  1078. $inputContainerTemplate = $options['options']['type'] . 'Container' . $options['errorSuffix'];
  1079. if (!$this->templater()->get($inputContainerTemplate)) {
  1080. $inputContainerTemplate = 'inputContainer' . $options['errorSuffix'];
  1081. }
  1082. return $this->formatTemplate($inputContainerTemplate, [
  1083. 'content' => $options['content'],
  1084. 'error' => $options['error'],
  1085. 'label' => $options['label'] ?? '',
  1086. 'required' => $options['options']['required'] ? ' ' . $this->templater()->get('requiredClass') : '',
  1087. 'type' => $options['options']['type'],
  1088. 'templateVars' => $options['options']['templateVars'] ?? [],
  1089. ]);
  1090. }
  1091. /**
  1092. * Generates an input element
  1093. *
  1094. * @param string $fieldName the field name
  1095. * @param array<string, mixed> $options The options for the input element
  1096. * @return array|string The generated input element string
  1097. * or array if checkbox() is called with option 'hiddenField' set to '_split'.
  1098. */
  1099. protected function _getInput(string $fieldName, array $options): array|string
  1100. {
  1101. $label = $options['labelOptions'];
  1102. unset($options['labelOptions']);
  1103. switch (strtolower($options['type'])) {
  1104. case 'select':
  1105. case 'radio':
  1106. case 'multicheckbox':
  1107. $opts = $options['options'];
  1108. if ($opts == null) {
  1109. $opts = [];
  1110. }
  1111. unset($options['options']);
  1112. return $this->{$options['type']}($fieldName, $opts, $options + ['label' => $label]);
  1113. case 'input':
  1114. throw new InvalidArgumentException(sprintf(
  1115. 'Invalid type `input` used for field `%s`.',
  1116. $fieldName
  1117. ));
  1118. default:
  1119. return $this->{$options['type']}($fieldName, $options);
  1120. }
  1121. }
  1122. /**
  1123. * Generates input options array
  1124. *
  1125. * @param string $fieldName The name of the field to parse options for.
  1126. * @param array<string, mixed> $options Options list.
  1127. * @return array<string, mixed> Options
  1128. */
  1129. protected function _parseOptions(string $fieldName, array $options): array
  1130. {
  1131. $needsMagicType = false;
  1132. if (empty($options['type'])) {
  1133. $needsMagicType = true;
  1134. $options['type'] = $this->_inputType($fieldName, $options);
  1135. }
  1136. return $this->_magicOptions($fieldName, $options, $needsMagicType);
  1137. }
  1138. /**
  1139. * Returns the input type that was guessed for the provided fieldName,
  1140. * based on the internal type it is associated too, its name and the
  1141. * variables that can be found in the view template
  1142. *
  1143. * @param string $fieldName the name of the field to guess a type for
  1144. * @param array<string, mixed> $options the options passed to the input method
  1145. * @return string
  1146. */
  1147. protected function _inputType(string $fieldName, array $options): string
  1148. {
  1149. $context = $this->_getContext();
  1150. if ($context->isPrimaryKey($fieldName)) {
  1151. return 'hidden';
  1152. }
  1153. if (str_ends_with($fieldName, '_id')) {
  1154. return 'select';
  1155. }
  1156. $type = 'text';
  1157. $internalType = $context->type($fieldName);
  1158. $map = $this->_config['typeMap'];
  1159. if ($internalType !== null && isset($map[$internalType])) {
  1160. $type = $map[$internalType];
  1161. }
  1162. $fieldName = array_slice(explode('.', $fieldName), -1)[0];
  1163. return match (true) {
  1164. isset($options['checked']) => 'checkbox',
  1165. isset($options['options']) => 'select',
  1166. in_array($fieldName, ['passwd', 'password'], true) => 'password',
  1167. in_array($fieldName, ['tel', 'telephone', 'phone'], true) => 'tel',
  1168. $fieldName === 'email' => 'email',
  1169. isset($options['rows']) || isset($options['cols']) => 'textarea',
  1170. $fieldName === 'year' => 'year',
  1171. default => $type,
  1172. };
  1173. }
  1174. /**
  1175. * Selects the variable containing the options for a select field if present,
  1176. * and sets the value to the 'options' key in the options array.
  1177. *
  1178. * @param string $fieldName The name of the field to find options for.
  1179. * @param array<string, mixed> $options Options list.
  1180. * @return array<string, mixed>
  1181. */
  1182. protected function _optionsOptions(string $fieldName, array $options): array
  1183. {
  1184. if (isset($options['options'])) {
  1185. return $options;
  1186. }
  1187. $internalType = $this->_getContext()->type($fieldName);
  1188. if ($internalType && str_starts_with($internalType, 'enum-')) {
  1189. $dbType = TypeFactory::build($internalType);
  1190. if ($dbType instanceof EnumType) {
  1191. if ($options['type'] !== 'radio') {
  1192. $options['type'] = 'select';
  1193. }
  1194. $options['options'] = $this->enumOptions($dbType->getEnumClassName());
  1195. return $options;
  1196. }
  1197. }
  1198. $pluralize = true;
  1199. if (str_ends_with($fieldName, '._ids')) {
  1200. $fieldName = substr($fieldName, 0, -5);
  1201. $pluralize = false;
  1202. } elseif (str_ends_with($fieldName, '_id')) {
  1203. $fieldName = substr($fieldName, 0, -3);
  1204. }
  1205. $fieldName = array_slice(explode('.', $fieldName), -1)[0];
  1206. $varName = Inflector::variable(
  1207. $pluralize ? Inflector::pluralize($fieldName) : $fieldName
  1208. );
  1209. $varOptions = $this->_View->get($varName);
  1210. if (!is_iterable($varOptions)) {
  1211. return $options;
  1212. }
  1213. if ($options['type'] !== 'radio') {
  1214. $options['type'] = 'select';
  1215. }
  1216. $options['options'] = $varOptions;
  1217. return $options;
  1218. }
  1219. /**
  1220. * Get map of enum value => label for select/radio options.
  1221. *
  1222. * @param class-string<\BackedEnum> $enumClass Enum class name.
  1223. * @return array<int|string, string>
  1224. */
  1225. protected function enumOptions(string $enumClass): array
  1226. {
  1227. assert(is_subclass_of($enumClass, BackedEnum::class));
  1228. $values = [];
  1229. /** @var \BackedEnum $case */
  1230. foreach ($enumClass::cases() as $case) {
  1231. $hasLabel = $case instanceof EnumLabelInterface || method_exists($case, 'label');
  1232. $values[$case->value] = $hasLabel ? $case->label() : $case->name;
  1233. }
  1234. return $values;
  1235. }
  1236. /**
  1237. * Magically set option type and corresponding options
  1238. *
  1239. * @param string $fieldName The name of the field to generate options for.
  1240. * @param array<string, mixed> $options Options list.
  1241. * @param bool $allowOverride Whether it is allowed for this method to
  1242. * overwrite the 'type' key in options.
  1243. * @return array<string, mixed>
  1244. */
  1245. protected function _magicOptions(string $fieldName, array $options, bool $allowOverride): array
  1246. {
  1247. $options += [
  1248. 'templateVars' => [],
  1249. ];
  1250. $options = $this->setRequiredAndCustomValidity($fieldName, $options);
  1251. $typesWithOptions = ['text', 'number', 'radio', 'select'];
  1252. $magicOptions = (in_array($options['type'], ['radio', 'select'], true) || $allowOverride);
  1253. if ($magicOptions && in_array($options['type'], $typesWithOptions, true)) {
  1254. $options = $this->_optionsOptions($fieldName, $options);
  1255. }
  1256. if ($allowOverride && str_ends_with($fieldName, '._ids')) {
  1257. $options['type'] = 'select';
  1258. if (!isset($options['multiple']) || ($options['multiple'] && $options['multiple'] !== 'checkbox')) {
  1259. $options['multiple'] = true;
  1260. }
  1261. }
  1262. return $options;
  1263. }
  1264. /**
  1265. * Set required attribute and custom validity JS.
  1266. *
  1267. * @param string $fieldName The name of the field to generate options for.
  1268. * @param array<string, mixed> $options Options list.
  1269. * @return array<string, mixed> Modified options list.
  1270. */
  1271. protected function setRequiredAndCustomValidity(string $fieldName, array $options): array
  1272. {
  1273. $context = $this->_getContext();
  1274. if (!isset($options['required']) && $options['type'] !== 'hidden') {
  1275. $options['required'] = $context->isRequired($fieldName);
  1276. }
  1277. $message = $context->getRequiredMessage($fieldName);
  1278. $message = h($message);
  1279. if ($options['required'] && $message) {
  1280. $options['templateVars']['customValidityMessage'] = $message;
  1281. if ($this->getConfig('autoSetCustomValidity')) {
  1282. $options['data-validity-message'] = $message;
  1283. $options['oninvalid'] = "this.setCustomValidity(''); "
  1284. . 'if (!this.value) this.setCustomValidity(this.dataset.validityMessage)';
  1285. $options['oninput'] = "this.setCustomValidity('')";
  1286. }
  1287. }
  1288. return $options;
  1289. }
  1290. /**
  1291. * Generate label for input
  1292. *
  1293. * @param string $fieldName The name of the field to generate label for.
  1294. * @param array<string, mixed> $options Options list.
  1295. * @return string|false Generated label element or false.
  1296. */
  1297. protected function _getLabel(string $fieldName, array $options): string|false
  1298. {
  1299. if ($options['type'] === 'hidden') {
  1300. return false;
  1301. }
  1302. $label = $options['label'] ?? null;
  1303. if ($label === false && $options['type'] === 'checkbox') {
  1304. return $options['input'];
  1305. }
  1306. if ($label === false) {
  1307. return false;
  1308. }
  1309. return $this->_inputLabel($fieldName, $label, $options);
  1310. }
  1311. /**
  1312. * Extracts a single option from an options array.
  1313. *
  1314. * @param string $name The name of the option to pull out.
  1315. * @param array<string, mixed> $options The array of options you want to extract.
  1316. * @param mixed $default The default option value
  1317. * @return mixed the contents of the option or default
  1318. */
  1319. protected function _extractOption(string $name, array $options, mixed $default = null): mixed
  1320. {
  1321. if (array_key_exists($name, $options)) {
  1322. return $options[$name];
  1323. }
  1324. return $default;
  1325. }
  1326. /**
  1327. * Generate a label for an input() call.
  1328. *
  1329. * $options can contain a hash of id overrides. These overrides will be
  1330. * used instead of the generated values if present.
  1331. *
  1332. * @param string $fieldName The name of the field to generate label for.
  1333. * @param array<string, mixed>|string|null $label Label text or array with label attributes.
  1334. * @param array<string, mixed> $options Options for the label element.
  1335. * @return string Generated label element
  1336. */
  1337. protected function _inputLabel(string $fieldName, array|string|null $label = null, array $options = []): string
  1338. {
  1339. $options += ['id' => null, 'input' => null, 'nestedInput' => false, 'templateVars' => []];
  1340. $labelAttributes = ['templateVars' => $options['templateVars']];
  1341. if (is_array($label)) {
  1342. $labelText = null;
  1343. if (isset($label['text'])) {
  1344. $labelText = $label['text'];
  1345. unset($label['text']);
  1346. }
  1347. $labelAttributes = array_merge($labelAttributes, $label);
  1348. } else {
  1349. $labelText = $label;
  1350. }
  1351. $labelAttributes['for'] = $options['id'];
  1352. if (in_array($options['type'], $this->_groupedInputTypes, true)) {
  1353. $labelAttributes['for'] = false;
  1354. }
  1355. if ($options['nestedInput']) {
  1356. $labelAttributes['input'] = $options['input'];
  1357. }
  1358. if (isset($options['escape'])) {
  1359. $labelAttributes['escape'] = $options['escape'];
  1360. }
  1361. return $this->label($fieldName, $labelText, $labelAttributes);
  1362. }
  1363. /**
  1364. * Creates a checkbox input widget.
  1365. *
  1366. * ### Options:
  1367. *
  1368. * - `value` - the value of the checkbox
  1369. * - `checked` - boolean indicate that this checkbox is checked.
  1370. * - `hiddenField` - boolean|string. Set to false to disable a hidden input from
  1371. * being generated. Passing a string will define the hidden input value.
  1372. * - `disabled` - create a disabled input.
  1373. * - `default` - Set the default value for the checkbox. This allows you to start checkboxes
  1374. * as checked, without having to check the POST data. A matching POST data value, will overwrite
  1375. * the default value.
  1376. *
  1377. * @param string $fieldName Name of a field, like this "modelname.fieldname"
  1378. * @param array<string, mixed> $options Array of HTML attributes.
  1379. * @return array<string>|string An HTML text input element.
  1380. * @link https://book.cakephp.org/5/en/views/helpers/form.html#creating-checkboxes
  1381. */
  1382. public function checkbox(string $fieldName, array $options = []): array|string
  1383. {
  1384. $options += ['hiddenField' => true, 'value' => 1];
  1385. // Work around value=>val translations.
  1386. $value = $options['value'];
  1387. unset($options['value']);
  1388. $options = $this->_initInputField($fieldName, $options);
  1389. $options['value'] = $value;
  1390. $output = '';
  1391. if ($options['hiddenField'] !== false && is_scalar($options['hiddenField'])) {
  1392. $hiddenOptions = [
  1393. 'name' => $options['name'],
  1394. 'value' => $options['hiddenField'] !== true
  1395. && $options['hiddenField'] !== '_split'
  1396. ? (string)$options['hiddenField'] : '0',
  1397. 'form' => $options['form'] ?? null,
  1398. 'secure' => false,
  1399. ];
  1400. if (isset($options['disabled']) && $options['disabled']) {
  1401. $hiddenOptions['disabled'] = 'disabled';
  1402. }
  1403. $output = $this->hidden($fieldName, $hiddenOptions);
  1404. }
  1405. if ($options['hiddenField'] === '_split') {
  1406. unset($options['hiddenField'], $options['type']);
  1407. return ['hidden' => $output, 'input' => $this->widget('checkbox', $options)];
  1408. }
  1409. unset($options['hiddenField'], $options['type']);
  1410. return $output . $this->widget('checkbox', $options);
  1411. }
  1412. /**
  1413. * Creates a set of radio widgets.
  1414. *
  1415. * ### Attributes:
  1416. *
  1417. * - `value` - Indicates the value when this radio button is checked.
  1418. * - `label` - Either `false` to disable label around the widget or an array of attributes for
  1419. * the label tag. `selected` will be added to any classes e.g. `'class' => 'myclass'` where widget
  1420. * is checked
  1421. * - `hiddenField` - boolean|string. Set to false to not include a hidden input with a value of ''.
  1422. * Can also be a string to set the value of the hidden input. This is useful for creating
  1423. * radio sets that are non-continuous.
  1424. * - `disabled` - Set to `true` or `disabled` to disable all the radio buttons. Use an array of
  1425. * values to disable specific radio buttons.
  1426. * - `empty` - Set to `true` to create an input with the value '' as the first option. When `true`
  1427. * the radio label will be 'empty'. Set this option to a string to control the label value.
  1428. *
  1429. * @param string $fieldName Name of a field, like this "modelname.fieldname"
  1430. * @param iterable $options Radio button options array.
  1431. * @param array<string, mixed> $attributes Array of attributes.
  1432. * @return string Completed radio widget set.
  1433. * @link https://book.cakephp.org/5/en/views/helpers/form.html#creating-radio-buttons
  1434. */
  1435. public function radio(string $fieldName, iterable $options = [], array $attributes = []): string
  1436. {
  1437. $attributes['options'] = $options;
  1438. $attributes['idPrefix'] = $this->_idPrefix;
  1439. $generatedHiddenId = false;
  1440. if (!isset($attributes['id'])) {
  1441. $attributes['id'] = true;
  1442. $generatedHiddenId = true;
  1443. }
  1444. $attributes = $this->_initInputField($fieldName, $attributes);
  1445. $hiddenField = $attributes['hiddenField'] ?? true;
  1446. unset($attributes['hiddenField']);
  1447. $hidden = '';
  1448. if ($hiddenField !== false && is_scalar($hiddenField)) {
  1449. $hidden = $this->hidden($fieldName, [
  1450. 'value' => $hiddenField === true ? '' : (string)$hiddenField,
  1451. 'form' => $attributes['form'] ?? null,
  1452. 'name' => $attributes['name'],
  1453. 'id' => $attributes['id'],
  1454. ]);
  1455. }
  1456. if ($generatedHiddenId) {
  1457. unset($attributes['id']);
  1458. }
  1459. $radio = $this->widget('radio', $attributes);
  1460. return $hidden . $radio;
  1461. }
  1462. /**
  1463. * Missing method handler - implements various simple input types. Is used to create inputs
  1464. * of various types. e.g. `$this->Form->text();` will create `<input type="text">` while
  1465. * `$this->Form->range();` will create `<input type="range">`
  1466. *
  1467. * ### Usage
  1468. *
  1469. * ```
  1470. * $this->Form->search('User.query', ['value' => 'test']);
  1471. * ```
  1472. *
  1473. * Will make an input like:
  1474. *
  1475. * `<input type="search" id="UserQuery" name="User[query]" value="test">`
  1476. *
  1477. * The first argument to an input type should always be the fieldname, in `Model.field` format.
  1478. * The second argument should always be an array of attributes for the input.
  1479. *
  1480. * @param string $method Method name / input type to make.
  1481. * @param array $params Parameters for the method call
  1482. * @return string Formatted input method.
  1483. * @throws \Cake\Core\Exception\CakeException When there are no params for the method call.
  1484. */
  1485. public function __call(string $method, array $params): string
  1486. {
  1487. if (empty($params)) {
  1488. throw new CakeException(sprintf('Missing field name for `FormHelper::%s`.', $method));
  1489. }
  1490. $options = $params[1] ?? [];
  1491. $options['type'] = $options['type'] ?? $method;
  1492. $options = $this->_initInputField($params[0], $options);
  1493. return $this->widget($options['type'], $options);
  1494. }
  1495. /**
  1496. * Creates a textarea widget.
  1497. *
  1498. * ### Options:
  1499. *
  1500. * - `escape` - Whether the contents of the textarea should be escaped. Defaults to true.
  1501. *
  1502. * @param string $fieldName Name of a field, in the form "modelname.fieldname"
  1503. * @param array<string, mixed> $options Array of HTML attributes, and special options above.
  1504. * @return string A generated HTML text input element
  1505. * @link https://book.cakephp.org/5/en/views/helpers/form.html#creating-textareas
  1506. */
  1507. public function textarea(string $fieldName, array $options = []): string
  1508. {
  1509. $options = $this->_initInputField($fieldName, $options);
  1510. unset($options['type']);
  1511. return $this->widget('textarea', $options);
  1512. }
  1513. /**
  1514. * Creates a hidden input field.
  1515. *
  1516. * @param string $fieldName Name of a field, in the form of "modelname.fieldname"
  1517. * @param array<string, mixed> $options Array of HTML attributes.
  1518. * @return string A generated hidden input
  1519. * @link https://book.cakephp.org/5/en/views/helpers/form.html#creating-hidden-inputs
  1520. */
  1521. public function hidden(string $fieldName, array $options = []): string
  1522. {
  1523. $options += ['required' => false, 'secure' => true];
  1524. $secure = $options['secure'];
  1525. unset($options['secure']);
  1526. $options = $this->_initInputField($fieldName, array_merge(
  1527. $options,
  1528. ['secure' => static::SECURE_SKIP]
  1529. ));
  1530. if ($secure === true && $this->formProtector) {
  1531. $this->formProtector->addField(
  1532. $options['name'],
  1533. true,
  1534. $options['val'] === false ? '0' : (string)$options['val']
  1535. );
  1536. }
  1537. $options['type'] = 'hidden';
  1538. return $this->widget('hidden', $options);
  1539. }
  1540. /**
  1541. * Creates file input widget.
  1542. *
  1543. * @param string $fieldName Name of a field, in the form "modelname.fieldname"
  1544. * @param array<string, mixed> $options Array of HTML attributes.
  1545. * @return string A generated file input.
  1546. * @link https://book.cakephp.org/5/en/views/helpers/form.html#creating-file-inputs
  1547. */
  1548. public function file(string $fieldName, array $options = []): string
  1549. {
  1550. $options += ['secure' => true];
  1551. $options = $this->_initInputField($fieldName, $options);
  1552. unset($options['type']);
  1553. return $this->widget('file', $options);
  1554. }
  1555. /**
  1556. * Creates a `<button>` tag.
  1557. *
  1558. * ### Options:
  1559. *
  1560. * - `type` - Value for "type" attribute of button. Defaults to "submit".
  1561. * - `escapeTitle` - HTML entity encode the title of the button. Defaults to true.
  1562. * - `escape` - HTML entity encode the attributes of button tag. Defaults to true.
  1563. * - `confirm` - Confirm message to show. Form execution will only continue if confirmed then.
  1564. *
  1565. * @param string $title The button's caption. Not automatically HTML encoded
  1566. * @param array<string, mixed> $options Array of options and HTML attributes.
  1567. * @return string A HTML button tag.
  1568. * @link https://book.cakephp.org/5/en/views/helpers/form.html#creating-button-elements
  1569. */
  1570. public function button(string $title, array $options = []): string
  1571. {
  1572. $options += [
  1573. 'type' => 'submit',
  1574. 'escapeTitle' => true,
  1575. 'escape' => true,
  1576. 'secure' => false,
  1577. 'confirm' => null,
  1578. ];
  1579. $options['text'] = $title;
  1580. $confirmMessage = $options['confirm'];
  1581. unset($options['confirm']);
  1582. if ($confirmMessage) {
  1583. $confirm = $this->_confirm('return true;', 'return false;');
  1584. $options['data-confirm-message'] = $confirmMessage;
  1585. $options['onclick'] = $this->templater()->format('confirmJs', [
  1586. 'confirmMessage' => h($confirmMessage),
  1587. 'confirm' => $confirm,
  1588. ]);
  1589. }
  1590. return $this->widget('button', $options);
  1591. }
  1592. /**
  1593. * Create a `<button>` tag with a surrounding `<form>` that submits via POST as default.
  1594. *
  1595. * This method creates a `<form>` element. So do not use this method in an already opened form.
  1596. * Instead use FormHelper::submit() or FormHelper::button() to create buttons inside opened forms.
  1597. *
  1598. * ### Options:
  1599. *
  1600. * - `data` - Array with key/value to pass in input hidden
  1601. * - `method` - Request method to use. Set to 'delete' or others to simulate
  1602. * HTTP/1.1 DELETE (or others) request. Defaults to 'post'.
  1603. * - `form` - Array with any option that FormHelper::create() can take
  1604. * - Other options is the same of button method.
  1605. * - `confirm` - Confirm message to show. Form execution will only continue if confirmed then.
  1606. *
  1607. * @param string $title The button's caption. Not automatically HTML encoded
  1608. * @param array|string $url URL as string or array
  1609. * @param array<string, mixed> $options Array of options and HTML attributes.
  1610. * @return string A HTML button tag.
  1611. * @link https://book.cakephp.org/5/en/views/helpers/form.html#creating-standalone-buttons-and-post-links
  1612. */
  1613. public function postButton(string $title, array|string $url, array $options = []): string
  1614. {
  1615. $formOptions = ['url' => $url];
  1616. if (isset($options['method'])) {
  1617. $formOptions['type'] = $options['method'];
  1618. unset($options['method']);
  1619. }
  1620. if (isset($options['form']) && is_array($options['form'])) {
  1621. $formOptions = $options['form'] + $formOptions;
  1622. unset($options['form']);
  1623. }
  1624. $out = $this->create(null, $formOptions);
  1625. if (isset($options['data']) && is_array($options['data'])) {
  1626. foreach (Hash::flatten($options['data']) as $key => $value) {
  1627. $out .= $this->hidden($key, ['value' => $value]);
  1628. }
  1629. unset($options['data']);
  1630. }
  1631. $out .= $this->button($title, $options);
  1632. $out .= $this->end();
  1633. return $out;
  1634. }
  1635. /**
  1636. * Creates an HTML link, but access the URL using the method you specify
  1637. * (defaults to POST). Requires javascript to be enabled in browser.
  1638. *
  1639. * This method creates a `<form>` element. If you want to use this method inside of an
  1640. * existing form, you must use the `block` option so that the new form is being set to
  1641. * a view block that can be rendered outside of the main form.
  1642. *
  1643. * If all you are looking for is a button to submit your form, then you should use
  1644. * `FormHelper::button()` or `FormHelper::submit()` instead.
  1645. *
  1646. * ### Options:
  1647. *
  1648. * - `data` - Array with key/value to pass in input hidden
  1649. * - `method` - Request method to use. Set to 'delete' to simulate
  1650. * HTTP/1.1 DELETE request. Defaults to 'post'.
  1651. * - `confirm` - Confirm message to show. Form execution will only continue if confirmed then.
  1652. * - `block` - Set to true to append form to view block "postLink" or provide
  1653. * custom block name.
  1654. * - Other options are the same of HtmlHelper::link() method.
  1655. * - The option `onclick` will be replaced.
  1656. *
  1657. * @param string $title The content to be wrapped by <a> tags.
  1658. * @param array|string|null $url Cake-relative URL or array of URL parameters, or
  1659. * external URL (starts with http://)
  1660. * @param array<string, mixed> $options Array of HTML attributes.
  1661. * @return string An `<a>` element.
  1662. * @link https://book.cakephp.org/5/en/views/helpers/form.html#creating-standalone-buttons-and-post-links
  1663. */
  1664. public function postLink(string $title, array|string|null $url = null, array $options = []): string
  1665. {
  1666. $options += ['block' => null, 'confirm' => null];
  1667. $requestMethod = 'POST';
  1668. if (!empty($options['method'])) {
  1669. $requestMethod = strtoupper($options['method']);
  1670. unset($options['method']);
  1671. }
  1672. $confirmMessage = $options['confirm'];
  1673. unset($options['confirm']);
  1674. $formName = str_replace('.', '', uniqid('post_', true));
  1675. $formOptions = [
  1676. 'name' => $formName,
  1677. 'style' => 'display:none;',
  1678. 'method' => 'post',
  1679. ];
  1680. if (isset($options['target'])) {
  1681. $formOptions['target'] = $options['target'];
  1682. unset($options['target']);
  1683. }
  1684. $templater = $this->templater();
  1685. $restoreAction = $this->_lastAction;
  1686. $this->_lastAction($url);
  1687. $restoreFormProtector = $this->formProtector;
  1688. $action = $templater->formatAttributes([
  1689. 'action' => $this->Url->build($url),
  1690. 'escape' => false,
  1691. ]);
  1692. $out = $this->formatTemplate('formStart', [
  1693. 'attrs' => $templater->formatAttributes($formOptions) . $action,
  1694. ]);
  1695. $out .= $this->hidden('_method', [
  1696. 'value' => $requestMethod,
  1697. 'secure' => static::SECURE_SKIP,
  1698. ]);
  1699. $out .= $this->_csrfField();
  1700. $formTokenData = $this->_View->getRequest()->getAttribute('formTokenData');
  1701. if ($formTokenData !== null) {
  1702. $this->formProtector = $this->createFormProtector($formTokenData);
  1703. }
  1704. $fields = [];
  1705. if (isset($options['data']) && is_array($options['data'])) {
  1706. foreach (Hash::flatten($options['data']) as $key => $value) {
  1707. $fields[$key] = $value;
  1708. $out .= $this->hidden($key, ['value' => $value, 'secure' => static::SECURE_SKIP]);
  1709. }
  1710. unset($options['data']);
  1711. }
  1712. $out .= $this->secure($fields);
  1713. $out .= $this->formatTemplate('formEnd', []);
  1714. $this->_lastAction = $restoreAction;
  1715. $this->formProtector = $restoreFormProtector;
  1716. if ($options['block']) {
  1717. if ($options['block'] === true) {
  1718. $options['block'] = __FUNCTION__;
  1719. }
  1720. $this->_View->append($options['block'], $out);
  1721. $out = '';
  1722. }
  1723. unset($options['block']);
  1724. $url = '#';
  1725. $onClick = 'document.' . $formName . '.submit();';
  1726. if ($confirmMessage) {
  1727. $onClick = $this->_confirm($onClick, '');
  1728. $onClick = $onClick . 'event.returnValue = false; return false;';
  1729. $onClick = $this->templater()->format('confirmJs', [
  1730. 'confirmMessage' => h($confirmMessage),
  1731. 'formName' => $formName,
  1732. 'confirm' => $onClick,
  1733. ]);
  1734. $options['data-confirm-message'] = $confirmMessage;
  1735. } else {
  1736. $onClick .= ' event.returnValue = false; return false;';
  1737. }
  1738. $options['onclick'] = $onClick;
  1739. $out .= $this->Html->link($title, $url, $options);
  1740. return $out;
  1741. }
  1742. /**
  1743. * Creates a submit button element. This method will generate `<input>` elements that
  1744. * can be used to submit, and reset forms by using $options. image submits can be created by supplying an
  1745. * image path for $caption.
  1746. *
  1747. * ### Options
  1748. *
  1749. * - `type` - Set to 'reset' for reset inputs. Defaults to 'submit'
  1750. * - `templateVars` - Additional template variables for the input element and its container.
  1751. * - Other attributes will be assigned to the input element.
  1752. *
  1753. * @param string|null $caption The label appearing on the button OR if string contains :// or the
  1754. * extension .jpg, .jpe, .jpeg, .gif, .png use an image if the extension
  1755. * exists, AND the first character is /, image is relative to webroot,
  1756. * OR if the first character is not /, image is relative to webroot/img.
  1757. * @param array<string, mixed> $options Array of options. See above.
  1758. * @return string A HTML submit button
  1759. * @link https://book.cakephp.org/5/en/views/helpers/form.html#creating-buttons-and-submit-elements
  1760. */
  1761. public function submit(?string $caption = null, array $options = []): string
  1762. {
  1763. $caption ??= __d('cake', 'Submit');
  1764. $options += [
  1765. 'type' => 'submit',
  1766. 'secure' => false,
  1767. 'templateVars' => [],
  1768. ];
  1769. if (isset($options['name']) && $this->formProtector) {
  1770. $this->formProtector->addField(
  1771. $options['name'],
  1772. $options['secure']
  1773. );
  1774. }
  1775. unset($options['secure']);
  1776. $isUrl = str_contains($caption, '://');
  1777. $isImage = preg_match('/\.(jpg|jpe|jpeg|gif|png|ico)$/', $caption);
  1778. $type = $options['type'];
  1779. unset($options['type']);
  1780. if ($isUrl || $isImage) {
  1781. $type = 'image';
  1782. if ($this->formProtector) {
  1783. $unlockFields = ['x', 'y'];
  1784. if (isset($options['name'])) {
  1785. $unlockFields = [
  1786. $options['name'] . '_x',
  1787. $options['name'] . '_y',
  1788. ];
  1789. }
  1790. foreach ($unlockFields as $ignore) {
  1791. $this->unlockField($ignore);
  1792. }
  1793. }
  1794. }
  1795. if ($isUrl) {
  1796. $options['src'] = $caption;
  1797. } elseif ($isImage) {
  1798. if ($caption[0] !== '/') {
  1799. $url = $this->Url->webroot(Configure::read('App.imageBaseUrl') . $caption);
  1800. } else {
  1801. $url = $this->Url->webroot(trim($caption, '/'));
  1802. }
  1803. $url = $this->Url->assetTimestamp($url);
  1804. $options['src'] = $url;
  1805. } else {
  1806. $options['value'] = $caption;
  1807. }
  1808. $input = $this->formatTemplate('inputSubmit', [
  1809. 'type' => $type,
  1810. 'attrs' => $this->templater()->formatAttributes($options),
  1811. 'templateVars' => $options['templateVars'],
  1812. ]);
  1813. return $this->formatTemplate('submitContainer', [
  1814. 'content' => $input,
  1815. 'templateVars' => $options['templateVars'],
  1816. ]);
  1817. }
  1818. /**
  1819. * Returns a formatted SELECT element.
  1820. *
  1821. * ### Attributes:
  1822. *
  1823. * - `multiple` - show a multiple select box. If set to 'checkbox' multiple checkboxes will be
  1824. * created instead.
  1825. * - `empty` - If true, the empty select option is shown. If a string,
  1826. * that string is displayed as the empty element.
  1827. * - `escape` - If true contents of options will be HTML entity encoded. Defaults to true.
  1828. * - `val` The selected value of the input.
  1829. * - `disabled` - Control the disabled attribute. When creating a select box, set to true to disable the
  1830. * select box. Set to an array to disable specific option elements.
  1831. *
  1832. * ### Using options
  1833. *
  1834. * A simple array will create normal options:
  1835. *
  1836. * ```
  1837. * $options = [1 => 'one', 2 => 'two'];
  1838. * $this->Form->select('Model.field', $options));
  1839. * ```
  1840. *
  1841. * While a nested options array will create optgroups with options inside them.
  1842. * ```
  1843. * $options = [
  1844. * 1 => 'bill',
  1845. * 'fred' => [
  1846. * 2 => 'fred',
  1847. * 3 => 'fred jr.'
  1848. * ]
  1849. * ];
  1850. * $this->Form->select('Model.field', $options);
  1851. * ```
  1852. *
  1853. * If you have multiple options that need to have the same value attribute, you can
  1854. * use an array of arrays to express this:
  1855. *
  1856. * ```
  1857. * $options = [
  1858. * ['text' => 'United states', 'value' => 'USA'],
  1859. * ['text' => 'USA', 'value' => 'USA'],
  1860. * ];
  1861. * ```
  1862. *
  1863. * @param string $fieldName Name attribute of the SELECT
  1864. * @param iterable $options Array of the OPTION elements (as 'value'=>'Text' pairs) to be used in the
  1865. * SELECT element
  1866. * @param array<string, mixed> $attributes The HTML attributes of the select element.
  1867. * @return string Formatted SELECT element
  1868. * @see \Cake\View\Helper\FormHelper::multiCheckbox() for creating multiple checkboxes.
  1869. * @link https://book.cakephp.org/5/en/views/helpers/form.html#creating-select-pickers
  1870. */
  1871. public function select(string $fieldName, iterable $options = [], array $attributes = []): string
  1872. {
  1873. $attributes += [
  1874. 'disabled' => null,
  1875. 'escape' => true,
  1876. 'hiddenField' => true,
  1877. 'multiple' => null,
  1878. 'secure' => true,
  1879. 'empty' => null,
  1880. ];
  1881. if ($attributes['empty'] === null && $attributes['multiple'] !== 'checkbox') {
  1882. $required = $this->_getContext()->isRequired($fieldName);
  1883. $attributes['empty'] = $required === null ? false : !$required;
  1884. }
  1885. if ($attributes['multiple'] === 'checkbox') {
  1886. unset($attributes['multiple'], $attributes['empty']);
  1887. return $this->multiCheckbox($fieldName, $options, $attributes);
  1888. }
  1889. unset($attributes['label']);
  1890. // Secure the field if there are options, or it's a multi select.
  1891. // Single selects with no options don't submit, but multiselects do.
  1892. if (
  1893. $attributes['secure'] &&
  1894. empty($options) &&
  1895. empty($attributes['empty']) &&
  1896. empty($attributes['multiple'])
  1897. ) {
  1898. $attributes['secure'] = false;
  1899. }
  1900. $attributes = $this->_initInputField($fieldName, $attributes);
  1901. $attributes['options'] = $options;
  1902. $hidden = '';
  1903. if ($attributes['multiple'] && $attributes['hiddenField']) {
  1904. $hiddenAttributes = [
  1905. 'name' => $attributes['name'],
  1906. 'value' => '',
  1907. 'form' => $attributes['form'] ?? null,
  1908. 'secure' => false,
  1909. ];
  1910. $hidden = $this->hidden($fieldName, $hiddenAttributes);
  1911. }
  1912. unset($attributes['hiddenField'], $attributes['type']);
  1913. return $hidden . $this->widget('select', $attributes);
  1914. }
  1915. /**
  1916. * Creates a set of checkboxes out of options.
  1917. *
  1918. * ### Options
  1919. *
  1920. * - `escape` - If true contents of options will be HTML entity encoded. Defaults to true.
  1921. * - `val` The selected value of the input.
  1922. * - `class` - When using multiple = checkbox the class name to apply to the divs. Defaults to 'checkbox'.
  1923. * - `disabled` - Control the disabled attribute. When creating checkboxes, `true` will disable all checkboxes.
  1924. * You can also set disabled to a list of values you want to disable when creating checkboxes.
  1925. * - `hiddenField` - Set to false to remove the hidden field that ensures a value
  1926. * is always submitted.
  1927. * - `label` - Either `false` to disable label around the widget or an array of attributes for
  1928. * the label tag. `selected` will be added to any classes e.g. `'class' => 'myclass'` where
  1929. * widget is checked
  1930. *
  1931. * Can be used in place of a select box with the multiple attribute.
  1932. *
  1933. * @param string $fieldName Name attribute of the SELECT
  1934. * @param iterable $options Array of the OPTION elements
  1935. * (as 'value'=>'Text' pairs) to be used in the checkboxes element.
  1936. * @param array<string, mixed> $attributes The HTML attributes of the select element.
  1937. * @return string Formatted SELECT element
  1938. * @see \Cake\View\Helper\FormHelper::select() for supported option formats.
  1939. */
  1940. public function multiCheckbox(string $fieldName, iterable $options, array $attributes = []): string
  1941. {
  1942. $attributes += [
  1943. 'disabled' => null,
  1944. 'escape' => true,
  1945. 'hiddenField' => true,
  1946. 'secure' => true,
  1947. ];
  1948. $generatedHiddenId = false;
  1949. if (!isset($attributes['id'])) {
  1950. $attributes['id'] = true;
  1951. $generatedHiddenId = true;
  1952. }
  1953. $attributes = $this->_initInputField($fieldName, $attributes);
  1954. $attributes['options'] = $options;
  1955. $attributes['idPrefix'] = $this->_idPrefix;
  1956. $hidden = '';
  1957. if ($attributes['hiddenField']) {
  1958. $hiddenAttributes = [
  1959. 'name' => $attributes['name'],
  1960. 'value' => '',
  1961. 'secure' => false,
  1962. 'disabled' => $attributes['disabled'] === true || $attributes['disabled'] === 'disabled',
  1963. 'id' => $attributes['id'],
  1964. ];
  1965. $hidden = $this->hidden($fieldName, $hiddenAttributes);
  1966. }
  1967. unset($attributes['hiddenField']);
  1968. if ($generatedHiddenId) {
  1969. unset($attributes['id']);
  1970. }
  1971. return $hidden . $this->widget('multicheckbox', $attributes);
  1972. }
  1973. /**
  1974. * Returns a SELECT element for years
  1975. *
  1976. * ### Attributes:
  1977. *
  1978. * - `empty` - If true, the empty select option is shown. If a string,
  1979. * that string is displayed as the empty element.
  1980. * - `order` - Ordering of year values in select options.
  1981. * Possible values 'asc', 'desc'. Default 'desc'
  1982. * - `value` The selected value of the input.
  1983. * - `max` The max year to appear in the select element.
  1984. * - `min` The min year to appear in the select element.
  1985. *
  1986. * @param string $fieldName The field name.
  1987. * @param array<string, mixed> $options Options & attributes for the select elements.
  1988. * @return string Completed year select input
  1989. * @link https://book.cakephp.org/5/en/views/helpers/form.html#creating-year-inputs
  1990. */
  1991. public function year(string $fieldName, array $options = []): string
  1992. {
  1993. $options += [
  1994. 'empty' => true,
  1995. ];
  1996. $options = $this->_initInputField($fieldName, $options);
  1997. unset($options['type']);
  1998. return $this->widget('year', $options);
  1999. }
  2000. /**
  2001. * Generate an input tag with type "month".
  2002. *
  2003. * ### Options:
  2004. *
  2005. * See dateTime() options.
  2006. *
  2007. * @param string $fieldName The field name.
  2008. * @param array<string, mixed> $options Array of options or HTML attributes.
  2009. * @return string
  2010. */
  2011. public function month(string $fieldName, array $options = []): string
  2012. {
  2013. $options += [
  2014. 'value' => null,
  2015. ];
  2016. $options = $this->_initInputField($fieldName, $options);
  2017. $options['type'] = 'month';
  2018. return $this->widget('datetime', $options);
  2019. }
  2020. /**
  2021. * Generate an input tag with type "datetime-local".
  2022. *
  2023. * ### Options:
  2024. *
  2025. * - `value` | `default` The default value to be used by the input.
  2026. * If set to `true` current datetime will be used.
  2027. *
  2028. * @param string $fieldName The field name.
  2029. * @param array<string, mixed> $options Array of options or HTML attributes.
  2030. * @return string
  2031. */
  2032. public function dateTime(string $fieldName, array $options = []): string
  2033. {
  2034. $options += [
  2035. 'value' => null,
  2036. ];
  2037. $options = $this->_initInputField($fieldName, $options);
  2038. $options['type'] = 'datetime-local';
  2039. $options['fieldName'] = $fieldName;
  2040. return $this->widget('datetime', $options);
  2041. }
  2042. /**
  2043. * Generate an input tag with type "time".
  2044. *
  2045. * ### Options:
  2046. *
  2047. * See dateTime() options.
  2048. *
  2049. * @param string $fieldName The field name.
  2050. * @param array<string, mixed> $options Array of options or HTML attributes.
  2051. * @return string
  2052. */
  2053. public function time(string $fieldName, array $options = []): string
  2054. {
  2055. $options += [
  2056. 'value' => null,
  2057. ];
  2058. $options = $this->_initInputField($fieldName, $options);
  2059. $options['type'] = 'time';
  2060. return $this->widget('datetime', $options);
  2061. }
  2062. /**
  2063. * Generate an input tag with type "date".
  2064. *
  2065. * ### Options:
  2066. *
  2067. * See dateTime() options.
  2068. *
  2069. * @param string $fieldName The field name.
  2070. * @param array<string, mixed> $options Array of options or HTML attributes.
  2071. * @return string
  2072. */
  2073. public function date(string $fieldName, array $options = []): string
  2074. {
  2075. $options += [
  2076. 'value' => null,
  2077. ];
  2078. $options = $this->_initInputField($fieldName, $options);
  2079. $options['type'] = 'date';
  2080. return $this->widget('datetime', $options);
  2081. }
  2082. /**
  2083. * Sets field defaults and adds field to form security input hash.
  2084. * Will also add the error class if the field contains validation errors.
  2085. *
  2086. * ### Options
  2087. *
  2088. * - `secure` - boolean whether the field should be added to the security fields.
  2089. * Disabling the field using the `disabled` option, will also omit the field from being
  2090. * part of the hashed key.
  2091. * - `default` - mixed - The value to use if there is no value in the form's context.
  2092. * - `disabled` - mixed - Either a boolean indicating disabled state, or the string in
  2093. * a numerically indexed value.
  2094. * - `id` - mixed - If `true` it will be auto generated based on field name.
  2095. *
  2096. * This method will convert a numerically indexed 'disabled' into an associative
  2097. * array value. FormHelper's internals expect associative options.
  2098. *
  2099. * The output of this function is a more complete set of input attributes that
  2100. * can be passed to a form widget to generate the actual input.
  2101. *
  2102. * @param string $field Name of the field to initialize options for.
  2103. * @param array<string, mixed>|array<string> $options Array of options to append options into.
  2104. * @return array<string, mixed> Array of options for the input.
  2105. */
  2106. protected function _initInputField(string $field, array $options = []): array
  2107. {
  2108. $options += ['fieldName' => $field];
  2109. if (!isset($options['secure'])) {
  2110. $options['secure'] = $this->_View->getRequest()->getAttribute('formTokenData') === null ? false : true;
  2111. }
  2112. $context = $this->_getContext();
  2113. if (isset($options['id']) && $options['id'] === true) {
  2114. $options['id'] = $this->_domId($field);
  2115. }
  2116. if (!isset($options['name'])) {
  2117. $endsWithBrackets = '';
  2118. if (str_ends_with($field, '[]')) {
  2119. $field = substr($field, 0, -2);
  2120. $endsWithBrackets = '[]';
  2121. }
  2122. $parts = explode('.', $field);
  2123. $first = array_shift($parts);
  2124. $options['name'] = $first . (!empty($parts) ? '[' . implode('][', $parts) . ']' : '') . $endsWithBrackets;
  2125. }
  2126. if (isset($options['value']) && !isset($options['val'])) {
  2127. $options['val'] = $options['value'];
  2128. unset($options['value']);
  2129. }
  2130. if (!isset($options['val'])) {
  2131. $valOptions = [
  2132. 'default' => $options['default'] ?? null,
  2133. 'schemaDefault' => $options['schemaDefault'] ?? true,
  2134. ];
  2135. $options['val'] = $this->getSourceValue($field, $valOptions);
  2136. }
  2137. if (!isset($options['val']) && isset($options['default'])) {
  2138. $options['val'] = $options['default'];
  2139. }
  2140. unset($options['value'], $options['default']);
  2141. if ($options['val'] instanceof BackedEnum) {
  2142. $options['val'] = $options['val']->value;
  2143. }
  2144. if ($context->hasError($field)) {
  2145. $options = $this->addClass($options, $this->_config['errorClass']);
  2146. }
  2147. $isDisabled = $this->_isDisabled($options);
  2148. if ($isDisabled) {
  2149. $options['secure'] = self::SECURE_SKIP;
  2150. }
  2151. return $options;
  2152. }
  2153. /**
  2154. * Determine if a field is disabled.
  2155. *
  2156. * @param array<string, mixed> $options The option set.
  2157. * @return bool Whether the field is disabled.
  2158. */
  2159. protected function _isDisabled(array $options): bool
  2160. {
  2161. if (!isset($options['disabled'])) {
  2162. return false;
  2163. }
  2164. if (is_scalar($options['disabled'])) {
  2165. return $options['disabled'] === true || $options['disabled'] === 'disabled';
  2166. }
  2167. if (!isset($options['options'])) {
  2168. return false;
  2169. }
  2170. if (is_array($options['options'])) {
  2171. // Simple list options
  2172. $first = $options['options'][array_keys($options['options'])[0]];
  2173. if (is_scalar($first)) {
  2174. return array_diff($options['options'], $options['disabled']) === [];
  2175. }
  2176. // Complex option types
  2177. if (is_array($first)) {
  2178. $disabled = array_filter(
  2179. $options['options'],
  2180. fn ($i) => in_array($i['value'], $options['disabled'], true)
  2181. );
  2182. return count($disabled) > 0;
  2183. }
  2184. }
  2185. return false;
  2186. }
  2187. /**
  2188. * Add a new context type.
  2189. *
  2190. * Form context types allow FormHelper to interact with
  2191. * data providers that come from outside CakePHP. For example
  2192. * if you wanted to use an alternative ORM like Doctrine you could
  2193. * create and connect a new context class to allow FormHelper to
  2194. * read metadata from doctrine.
  2195. *
  2196. * @param string $type The type of context. This key
  2197. * can be used to overwrite existing providers.
  2198. * @param callable $check A callable that returns an object
  2199. * when the form context is the correct type.
  2200. * @return void
  2201. */
  2202. public function addContextProvider(string $type, callable $check): void
  2203. {
  2204. $this->contextFactory()->addProvider($type, $check);
  2205. }
  2206. /**
  2207. * Get the context instance for the current form set.
  2208. *
  2209. * If there is no active form null will be returned.
  2210. *
  2211. * @param \Cake\View\Form\ContextInterface|null $context Either the new context when setting, or null to get.
  2212. * @return \Cake\View\Form\ContextInterface The context for the form.
  2213. */
  2214. public function context(?ContextInterface $context = null): ContextInterface
  2215. {
  2216. if ($context instanceof ContextInterface) {
  2217. $this->_context = $context;
  2218. }
  2219. return $this->_getContext();
  2220. }
  2221. /**
  2222. * Find the matching context provider for the data.
  2223. *
  2224. * If no type can be matched a NullContext will be returned.
  2225. *
  2226. * @param mixed $data The data to get a context provider for.
  2227. * @return \Cake\View\Form\ContextInterface Context provider.
  2228. * @throws \RuntimeException when the context class does not implement the
  2229. * ContextInterface.
  2230. */
  2231. protected function _getContext(mixed $data = []): ContextInterface
  2232. {
  2233. if (isset($this->_context) && empty($data)) {
  2234. return $this->_context;
  2235. }
  2236. $data += ['entity' => null];
  2237. return $this->_context = $this->contextFactory()
  2238. ->get($this->_View->getRequest(), $data);
  2239. }
  2240. /**
  2241. * Add a new widget to FormHelper.
  2242. *
  2243. * Allows you to add or replace widget instances with custom code.
  2244. *
  2245. * @param string $name The name of the widget. e.g. 'text'.
  2246. * @param \Cake\View\Widget\WidgetInterface|array|string $spec Either a string class
  2247. * name or an object implementing the WidgetInterface.
  2248. * @return void
  2249. */
  2250. public function addWidget(string $name, WidgetInterface|array|string $spec): void
  2251. {
  2252. $this->_locator->add([$name => $spec]);
  2253. }
  2254. /**
  2255. * Render a named widget.
  2256. *
  2257. * This is a lower level method. For built-in widgets, you should be using
  2258. * methods like `text`, `hidden`, and `radio`. If you are using additional
  2259. * widgets you should use this method render the widget without the label
  2260. * or wrapping div.
  2261. *
  2262. * @param string $name The name of the widget. e.g. 'text'.
  2263. * @param array $data The data to render.
  2264. * @return string
  2265. */
  2266. public function widget(string $name, array $data = []): string
  2267. {
  2268. $secure = null;
  2269. if (isset($data['secure'])) {
  2270. $secure = $data['secure'];
  2271. unset($data['secure']);
  2272. }
  2273. $widget = $this->_locator->get($name);
  2274. $out = $widget->render($data, $this->context());
  2275. if (
  2276. $this->formProtector !== null &&
  2277. isset($data['name']) &&
  2278. $secure !== null &&
  2279. $secure !== self::SECURE_SKIP
  2280. ) {
  2281. foreach ($widget->secureFields($data) as $field) {
  2282. $this->formProtector->addField($field, $secure);
  2283. }
  2284. }
  2285. return $out;
  2286. }
  2287. /**
  2288. * Restores the default values built into FormHelper.
  2289. *
  2290. * This method will not reset any templates set in custom widgets.
  2291. *
  2292. * @return void
  2293. */
  2294. public function resetTemplates(): void
  2295. {
  2296. $this->setTemplates($this->_defaultConfig['templates']);
  2297. }
  2298. /**
  2299. * Event listeners.
  2300. *
  2301. * @return array<string, mixed>
  2302. */
  2303. public function implementedEvents(): array
  2304. {
  2305. return [];
  2306. }
  2307. /**
  2308. * Gets the value sources.
  2309. *
  2310. * Returns a list, but at least one item, of valid sources, such as: `'context'`, `'data'` and `'query'`.
  2311. *
  2312. * @return array<string> List of value sources.
  2313. */
  2314. public function getValueSources(): array
  2315. {
  2316. return $this->_valueSources;
  2317. }
  2318. /**
  2319. * Validate value sources.
  2320. *
  2321. * @param array<string> $sources A list of strings identifying a source.
  2322. * @return void
  2323. * @throws \InvalidArgumentException If sources list contains invalid value.
  2324. */
  2325. protected function validateValueSources(array $sources): void
  2326. {
  2327. $diff = array_diff($sources, $this->supportedValueSources);
  2328. if ($diff) {
  2329. array_walk($diff, fn (&$x) => $x = "`$x`");
  2330. array_walk($this->supportedValueSources, fn (&$x) => $x = "`$x`");
  2331. throw new InvalidArgumentException(sprintf(
  2332. 'Invalid value source(s): %s. Valid values are: %s.',
  2333. implode(', ', $diff),
  2334. implode(', ', $this->supportedValueSources)
  2335. ));
  2336. }
  2337. }
  2338. /**
  2339. * Sets the value sources.
  2340. *
  2341. * You need to supply one or more valid sources, as a list of strings.
  2342. * Order sets priority.
  2343. *
  2344. * @see FormHelper::$supportedValueSources for valid values.
  2345. * @param array<string>|string $sources A string or a list of strings identifying a source.
  2346. * @return $this
  2347. * @throws \InvalidArgumentException If sources list contains invalid value.
  2348. */
  2349. public function setValueSources(array|string $sources)
  2350. {
  2351. $sources = (array)$sources;
  2352. $this->validateValueSources($sources);
  2353. $this->_valueSources = $sources;
  2354. return $this;
  2355. }
  2356. /**
  2357. * Gets a single field value from the sources available.
  2358. *
  2359. * @param string $fieldname The fieldname to fetch the value for.
  2360. * @param array<string, mixed> $options The options containing default values.
  2361. * @return mixed Field value derived from sources or defaults.
  2362. */
  2363. public function getSourceValue(string $fieldname, array $options = []): mixed
  2364. {
  2365. $valueMap = [
  2366. 'data' => 'getData',
  2367. 'query' => 'getQuery',
  2368. ];
  2369. foreach ($this->getValueSources() as $valuesSource) {
  2370. if ($valuesSource === 'context') {
  2371. $val = $this->_getContext()->val($fieldname, $options);
  2372. if ($val !== null) {
  2373. return $val;
  2374. }
  2375. }
  2376. if (isset($valueMap[$valuesSource])) {
  2377. $method = $valueMap[$valuesSource];
  2378. $value = $this->_View->getRequest()->{$method}($fieldname);
  2379. if ($value !== null) {
  2380. return $value;
  2381. }
  2382. }
  2383. }
  2384. return null;
  2385. }
  2386. }