[TASK] Update php-cs-fixer to 2.5.0
[Packages/TYPO3.CMS.git] / typo3 / sysext / form / Classes / Domain / Model / FormDefinition.php
1 <?php
2 declare(strict_types=1);
3 namespace TYPO3\CMS\Form\Domain\Model;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It originated from the Neos.Form package (www.neos.io)
9 *
10 * It is free software; you can redistribute it and/or modify it under
11 * the terms of the GNU General Public License, either version 2
12 * of the License, or any later version.
13 *
14 * For the full copyright and license information, please read the
15 * LICENSE.txt file that was distributed with this source code.
16 *
17 * The TYPO3 project - inspiring people to share!
18 */
19
20 use TYPO3\CMS\Core\Utility\ArrayUtility;
21 use TYPO3\CMS\Extbase\Mvc\Web\Request;
22 use TYPO3\CMS\Extbase\Mvc\Web\Response;
23 use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
24 use TYPO3\CMS\Form\Domain\Exception\IdentifierNotValidException;
25 use TYPO3\CMS\Form\Domain\Exception\TypeDefinitionNotFoundException;
26 use TYPO3\CMS\Form\Domain\Finishers\FinisherInterface;
27 use TYPO3\CMS\Form\Domain\Model\Exception\DuplicateFormElementException;
28 use TYPO3\CMS\Form\Domain\Model\Exception\FinisherPresetNotFoundException;
29 use TYPO3\CMS\Form\Domain\Model\Exception\FormDefinitionConsistencyException;
30 use TYPO3\CMS\Form\Domain\Model\FormElements\FormElementInterface;
31 use TYPO3\CMS\Form\Domain\Model\FormElements\Page;
32 use TYPO3\CMS\Form\Domain\Model\Renderable\AbstractCompositeRenderable;
33 use TYPO3\CMS\Form\Domain\Model\Renderable\RenderableInterface;
34 use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
35 use TYPO3\CMS\Form\Exception as FormException;
36 use TYPO3\CMS\Form\Mvc\ProcessingRule;
37
38 /**
39 * This class encapsulates a complete *Form Definition*, with all of its pages,
40 * form elements, validation rules which apply and finishers which should be
41 * executed when the form is completely filled in.
42 *
43 * It is *not modified* when the form executes.
44 *
45 * The Anatomy Of A Form
46 * =====================
47 *
48 * A FormDefinition consists of multiple *Page* ({@link Page}) objects. When a
49 * form is displayed to the user, only one *Page* is visible at any given time,
50 * and there is a navigation to go back and forth between the pages.
51 *
52 * A *Page* consists of multiple *FormElements* ({@link FormElementInterface}, {@link AbstractFormElement}),
53 * which represent the input fields, textareas, checkboxes shown inside the page.
54 *
55 * *FormDefinition*, *Page* and *FormElement* have *identifier* properties, which
56 * must be unique for each given type (i.e. it is allowed that the FormDefinition and
57 * a FormElement have the *same* identifier, but two FormElements are not allowed to
58 * have the same identifier.
59 *
60 * Simple Example
61 * --------------
62 *
63 * Generally, you can create a FormDefinition manually by just calling the API
64 * methods on it, or you use a *Form Definition Factory* to build the form from
65 * another representation format such as YAML.
66 *
67 * /---code php
68 * $formDefinition = $this->objectManager->get(FormDefinition::class, 'myForm');
69 *
70 * $page1 = $this->objectManager->get(Page::class, 'page1');
71 * $formDefinition->addPage($page);
72 *
73 * $element1 = $this->objectManager->get(GenericFormElement::class, 'title', 'Textfield'); # the second argument is the type of the form element
74 * $page1->addElement($element1);
75 * \---
76 *
77 * Creating a Form, Using Abstract Form Element Types
78 * =====================================================
79 *
80 * While you can use the {@link FormDefinition::addPage} or {@link Page::addElement}
81 * methods and create the Page and FormElement objects manually, it is often better
82 * to use the corresponding create* methods ({@link FormDefinition::createPage}
83 * and {@link Page::createElement}), as you pass them an abstract *Form Element Type*
84 * such as *Text* or *Page*, and the system **automatically
85 * resolves the implementation class name and sets default values**.
86 *
87 * So the simple example from above should be rewritten as follows:
88 *
89 * /---code php
90 * $prototypeConfiguration = []; // We'll talk about this later
91 *
92 * $formDefinition = $this->objectManager->get(FormDefinition::class, 'myForm', $prototypeConfiguration);
93 * $page1 = $formDefinition->createPage('page1');
94 * $element1 = $page1->addElement('title', 'Textfield');
95 * \---
96 *
97 * Now, you might wonder how the system knows that the element *Textfield*
98 * is implemented using a GenericFormElement: **This is configured in the $prototypeConfiguration**.
99 *
100 * To make the example from above actually work, we need to add some sensible
101 * values to *$prototypeConfiguration*:
102 *
103 * <pre>
104 * $prototypeConfiguration = [
105 * 'formElementsDefinition' => [
106 * 'Page' => [
107 * 'implementationClassName' => 'TYPO3\CMS\Form\Domain\Model\FormElements\Page'
108 * ],
109 * 'Textfield' => [
110 * 'implementationClassName' => 'TYPO3\CMS\Form\Domain\Model\FormElements\GenericFormElement'
111 * ]
112 * ]
113 * ]
114 * </pre>
115 *
116 * For each abstract *Form Element Type* we add some configuration; in the above
117 * case only the *implementation class name*. Still, it is possible to set defaults
118 * for *all* configuration options of such an element, as the following example
119 * shows:
120 *
121 * <pre>
122 * $prototypeConfiguration = [
123 * 'formElementsDefinition' => [
124 * 'Page' => [
125 * 'implementationClassName' => 'TYPO3\CMS\Form\Domain\Model\FormElements\Page',
126 * 'label' => 'this is the label of the page if nothing is specified'
127 * ],
128 * 'Textfield' => [
129 * 'implementationClassName' => 'TYPO3\CMS\Form\Domain\Model\FormElements\GenericFormElement',
130 * 'label' = >'Default Label',
131 * 'defaultValue' => 'Default form element value',
132 * 'properties' => [
133 * 'placeholder' => 'Text which is shown if element is empty'
134 * ]
135 * ]
136 * ]
137 * ]
138 * </pre>
139 *
140 * Using Preconfigured $prototypeConfiguration
141 * ---------------------------------
142 *
143 * Often, it is not really useful to manually create the $prototypeConfiguration array.
144 *
145 * Most of it comes pre-configured inside the extensions's yaml settings,
146 * and the {@link \TYPO3\CMS\Form\Domain\Configuration\ConfigurationService} contains helper methods
147 * which return the ready-to-use *$prototypeConfiguration*.
148 *
149 * Property Mapping and Validation Rules
150 * =====================================
151 *
152 * Besides Pages and FormElements, the FormDefinition can contain information
153 * about the *format of the data* which is inputted into the form. This generally means:
154 *
155 * - expected Data Types
156 * - Property Mapping Configuration to be used
157 * - Validation Rules which should apply
158 *
159 * Background Info
160 * ---------------
161 * You might wonder why Data Types and Validation Rules are *not attached
162 * to each FormElement itself*.
163 *
164 * If the form should create a *hierarchical output structure* such as a multi-
165 * dimensional array or a PHP object, your expected data structure might look as follows:
166 * <pre>
167 * - person
168 * -- firstName
169 * -- lastName
170 * -- address
171 * --- street
172 * --- city
173 * </pre>
174 *
175 * Now, let's imagine you want to edit *person.address.street* and *person.address.city*,
176 * but want to validate that the *combination* of *street* and *city* is valid
177 * according to some address database.
178 *
179 * In this case, the form elements would be configured to fill *street* and *city*,
180 * but the *validator* needs to be attached to the *compound object* *address*,
181 * as both parts need to be validated together.
182 *
183 * Connecting FormElements to the output data structure
184 * ====================================================
185 *
186 * The *identifier* of the *FormElement* is most important, as it determines
187 * where in the output structure the value which is entered by the user is placed,
188 * and thus also determines which validation rules need to apply.
189 *
190 * Using the above example, if you want to create a FormElement for the *street*,
191 * you should use the identifier *person.address.street*.
192 *
193 * Rendering a FormDefinition
194 * ==========================
195 *
196 * In order to trigger *rendering* on a FormDefinition,
197 * the current {@link \TYPO3\CMS\Extbase\Mvc\Web\Request} needs to be bound to the FormDefinition,
198 * resulting in a {@link \TYPO3\CMS\Form\Domain\Runtime\FormRuntime} object which contains the *Runtime State* of the form
199 * (such as the currently inserted values).
200 *
201 * /---code php
202 * # $currentRequest and $currentResponse need to be available, f.e. inside a controller you would
203 * # use $this->request and $this->response; inside a ViewHelper you would use $this->controllerContext->getRequest()
204 * # and $this->controllerContext->getResponse()
205 * $form = $formDefinition->bind($currentRequest, $currentResponse);
206 *
207 * # now, you can use the $form object to get information about the currently
208 * # entered values into the form, etc.
209 * \---
210 *
211 * Refer to the {@link \TYPO3\CMS\Form\Domain\Runtime\FormRuntime} API doc for further information.
212 *
213 * Scope: frontend
214 * **This class is NOT meant to be sub classed by developers.**
215 */
216 class FormDefinition extends AbstractCompositeRenderable
217 {
218
219 /**
220 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
221 */
222 protected $objectManager;
223
224 /**
225 * The finishers for this form
226 *
227 * @var \TYPO3\CMS\Form\Domain\Finishers\FinisherInterface[]
228 */
229 protected $finishers = [];
230
231 /**
232 * Property Mapping Rules, indexed by element identifier
233 *
234 * @var \TYPO3\CMS\Form\Mvc\ProcessingRule[]
235 */
236 protected $processingRules = [];
237
238 /**
239 * Contains all elements of the form, indexed by identifier.
240 * Is used as internal cache as we need this really often.
241 *
242 * @var \TYPO3\CMS\Form\Domain\Model\FormElements\FormElementInterface[]
243 */
244 protected $elementsByIdentifier = [];
245
246 /**
247 * Form element default values in the format ['elementIdentifier' => 'default value']
248 *
249 * @var array
250 */
251 protected $elementDefaultValues = [];
252
253 /**
254 * Renderer class name to be used.
255 *
256 * @var string
257 */
258 protected $rendererClassName = '';
259
260 /**
261 * @var array
262 */
263 protected $typeDefinitions;
264
265 /**
266 * @var array
267 */
268 protected $validatorsDefinition;
269
270 /**
271 * @var array
272 */
273 protected $finishersDefinition;
274
275 /**
276 * The persistence identifier of the form
277 *
278 * @var string
279 */
280 protected $persistenceIdentifier = null;
281
282 /**
283 * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
284 * @internal
285 */
286 public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
287 {
288 $this->objectManager = $objectManager;
289 }
290
291 /**
292 * Constructor. Creates a new FormDefinition with the given identifier.
293 *
294 * @param string $identifier The Form Definition's identifier, must be a non-empty string.
295 * @param array $prototypeConfiguration overrides form defaults of this definition
296 * @param string $type element type of this form
297 * @param string $persistenceIdentifier the persistence identifier of the form
298 * @throws IdentifierNotValidException if the identifier was not valid
299 * @api
300 */
301 public function __construct(
302 string $identifier,
303 array $prototypeConfiguration = [],
304 string $type = 'Form',
305 string $persistenceIdentifier = null
306 ) {
307 $this->typeDefinitions = isset($prototypeConfiguration['formElementsDefinition']) ? $prototypeConfiguration['formElementsDefinition'] : [];
308 $this->validatorsDefinition = isset($prototypeConfiguration['validatorsDefinition']) ? $prototypeConfiguration['validatorsDefinition'] : [];
309 $this->finishersDefinition = isset($prototypeConfiguration['finishersDefinition']) ? $prototypeConfiguration['finishersDefinition'] : [];
310
311 if (!is_string($identifier) || strlen($identifier) === 0) {
312 throw new IdentifierNotValidException('The given identifier was not a string or the string was empty.', 1477082503);
313 }
314
315 $this->identifier = $identifier;
316 $this->type = $type;
317 $this->persistenceIdentifier = $persistenceIdentifier;
318
319 if ($prototypeConfiguration !== []) {
320 $this->initializeFromFormDefaults();
321 }
322 }
323
324 /**
325 * Initialize the form defaults of the current type
326 *
327 * @throws TypeDefinitionNotFoundException
328 * @internal
329 */
330 protected function initializeFromFormDefaults()
331 {
332 if (!isset($this->typeDefinitions[$this->type])) {
333 throw new TypeDefinitionNotFoundException(sprintf('Type "%s" not found. Probably some configuration is missing.', $this->type), 1474905835);
334 }
335 $typeDefinition = $this->typeDefinitions[$this->type];
336 $this->setOptions($typeDefinition);
337 }
338
339 /**
340 * Set multiple properties of this object at once.
341 * Every property which has a corresponding set* method can be set using
342 * the passed $options array.
343 *
344 * @param array $options
345 * @internal
346 */
347 public function setOptions(array $options)
348 {
349 if (isset($options['rendererClassName'])) {
350 $this->setRendererClassName($options['rendererClassName']);
351 }
352 if (isset($options['renderingOptions'])) {
353 foreach ($options['renderingOptions'] as $key => $value) {
354 if (is_array($value)) {
355 $currentValue = isset($this->getRenderingOptions()[$key]) ? $this->getRenderingOptions()[$key] : [];
356 ArrayUtility::mergeRecursiveWithOverrule($currentValue, $value);
357 $this->setRenderingOption($key, $currentValue);
358 } else {
359 $this->setRenderingOption($key, $value);
360 }
361 }
362 }
363 if (isset($options['finishers'])) {
364 foreach ($options['finishers'] as $finisherConfiguration) {
365 $this->createFinisher($finisherConfiguration['identifier'], isset($finisherConfiguration['options']) ? $finisherConfiguration['options'] : []);
366 }
367 }
368
369 ArrayUtility::assertAllArrayKeysAreValid(
370 $options,
371 ['rendererClassName', 'renderingOptions', 'finishers', 'formEditor']
372 );
373 }
374
375 /**
376 * Create a page with the given $identifier and attach this page to the form.
377 *
378 * - Create Page object based on the given $typeName
379 * - set defaults inside the Page object
380 * - attach Page object to this form
381 * - return the newly created Page object
382 *
383 * @param string $identifier Identifier of the new page
384 * @param string $typeName Type of the new page
385 * @return Page the newly created page
386 * @throws TypeDefinitionNotFoundException
387 * @api
388 */
389 public function createPage(string $identifier, string $typeName = 'Page'): Page
390 {
391 if (!isset($this->typeDefinitions[$typeName])) {
392 throw new TypeDefinitionNotFoundException(sprintf('Type "%s" not found. Probably some configuration is missing.', $typeName), 1474905953);
393 }
394
395 $typeDefinition = $this->typeDefinitions[$typeName];
396
397 if (!isset($typeDefinition['implementationClassName'])) {
398 throw new TypeDefinitionNotFoundException(sprintf('The "implementationClassName" was not set in type definition "%s".', $typeName), 1477083126);
399 }
400 $implementationClassName = $typeDefinition['implementationClassName'];
401 $page = $this->objectManager->get($implementationClassName, $identifier, $typeName);
402
403 if (isset($typeDefinition['label'])) {
404 $page->setLabel($typeDefinition['label']);
405 }
406
407 if (isset($typeDefinition['renderingOptions'])) {
408 foreach ($typeDefinition['renderingOptions'] as $key => $value) {
409 $page->setRenderingOption($key, $value);
410 }
411 }
412
413 ArrayUtility::assertAllArrayKeysAreValid(
414 $typeDefinition,
415 ['implementationClassName', 'label', 'renderingOptions', 'formEditor']
416 );
417
418 $this->addPage($page);
419 return $page;
420 }
421
422 /**
423 * Add a new page at the end of the form.
424 *
425 * Instead of this method, you should often use {@link createPage} instead.
426 *
427 * @param Page $page
428 * @throws FormDefinitionConsistencyException if Page is already added to a FormDefinition
429 * @see createPage
430 * @api
431 */
432 public function addPage(Page $page)
433 {
434 $this->addRenderable($page);
435 }
436
437 /**
438 * Get the Form's pages
439 *
440 * @return array<Page> The Form's pages in the correct order
441 * @api
442 */
443 public function getPages(): array
444 {
445 return $this->renderables;
446 }
447
448 /**
449 * Check whether a page with the given $index exists
450 *
451 * @param int $index
452 * @return bool TRUE if a page with the given $index exists, otherwise FALSE
453 * @api
454 */
455 public function hasPageWithIndex(int $index): bool
456 {
457 return isset($this->renderables[$index]);
458 }
459
460 /**
461 * Get the page with the passed index. The first page has index zero.
462 *
463 * If page at $index does not exist, an exception is thrown. @see hasPageWithIndex()
464 *
465 * @param int $index
466 * @return Page the page, or NULL if none found.
467 * @throws FormException if the specified index does not exist
468 * @api
469 */
470 public function getPageByIndex(int $index)
471 {
472 if (!$this->hasPageWithIndex($index)) {
473 throw new FormException(sprintf('There is no page with an index of %d', $index), 1329233627);
474 }
475 return $this->renderables[$index];
476 }
477
478 /**
479 * Adds the specified finisher to this form
480 *
481 * @param FinisherInterface $finisher
482 * @api
483 */
484 public function addFinisher(FinisherInterface $finisher)
485 {
486 $this->finishers[] = $finisher;
487 }
488
489 /**
490 * @param string $finisherIdentifier identifier of the finisher as registered in the current form (for example: "Redirect")
491 * @param array $options options for this finisher in the format ['option1' => 'value1', 'option2' => 'value2', ...]
492 * @return FinisherInterface
493 * @throws FinisherPresetNotFoundException
494 * @api
495 */
496 public function createFinisher(string $finisherIdentifier, array $options = []): FinisherInterface
497 {
498 if (isset($this->finishersDefinition[$finisherIdentifier]) && is_array($this->finishersDefinition[$finisherIdentifier]) && isset($this->finishersDefinition[$finisherIdentifier]['implementationClassName'])) {
499 $implementationClassName = $this->finishersDefinition[$finisherIdentifier]['implementationClassName'];
500 $defaultOptions = isset($this->finishersDefinition[$finisherIdentifier]['options']) ? $this->finishersDefinition[$finisherIdentifier]['options'] : [];
501 ArrayUtility::mergeRecursiveWithOverrule($defaultOptions, $options);
502
503 $finisher = $this->objectManager->get($implementationClassName);
504 $finisher->setOptions($defaultOptions);
505 $this->addFinisher($finisher);
506 return $finisher;
507 }
508 throw new FinisherPresetNotFoundException('The finisher preset identified by "' . $finisherIdentifier . '" could not be found, or the implementationClassName was not specified.', 1328709784);
509 }
510
511 /**
512 * Gets all finishers of this form
513 *
514 * @return \TYPO3\CMS\Form\Domain\Finishers\FinisherInterface[]
515 * @api
516 */
517 public function getFinishers(): array
518 {
519 return $this->finishers;
520 }
521
522 /**
523 * Add an element to the ElementsByIdentifier Cache.
524 *
525 * @param RenderableInterface $renderable
526 * @throws DuplicateFormElementException
527 * @internal
528 */
529 public function registerRenderable(RenderableInterface $renderable)
530 {
531 if ($renderable instanceof FormElementInterface) {
532 if (isset($this->elementsByIdentifier[$renderable->getIdentifier()])) {
533 throw new DuplicateFormElementException(sprintf('A form element with identifier "%s" is already part of the form.', $renderable->getIdentifier()), 1325663761);
534 }
535 $this->elementsByIdentifier[$renderable->getIdentifier()] = $renderable;
536 }
537 }
538
539 /**
540 * Remove an element from the ElementsByIdentifier cache
541 *
542 * @param RenderableInterface $renderable
543 * @internal
544 */
545 public function unregisterRenderable(RenderableInterface $renderable)
546 {
547 if ($renderable instanceof FormElementInterface) {
548 unset($this->elementsByIdentifier[$renderable->getIdentifier()]);
549 }
550 }
551
552 /**
553 * Get a Form Element by its identifier
554 *
555 * If identifier does not exist, returns NULL.
556 *
557 * @param string $elementIdentifier
558 * @return FormElementInterface The element with the given $elementIdentifier or NULL if none found
559 * @api
560 */
561 public function getElementByIdentifier(string $elementIdentifier)
562 {
563 return isset($this->elementsByIdentifier[$elementIdentifier]) ? $this->elementsByIdentifier[$elementIdentifier] : null;
564 }
565
566 /**
567 * Sets the default value of a form element
568 *
569 * @param string $elementIdentifier identifier of the form element. This supports property paths!
570 * @param mixed $defaultValue
571 * @internal
572 */
573 public function addElementDefaultValue(string $elementIdentifier, $defaultValue)
574 {
575 $this->elementDefaultValues = ArrayUtility::setValueByPath(
576 $this->elementDefaultValues,
577 $elementIdentifier,
578 $defaultValue,
579 '.'
580 );
581 }
582
583 /**
584 * returns the default value of the specified form element
585 * or NULL if no default value was set
586 *
587 * @param string $elementIdentifier identifier of the form element. This supports property paths!
588 * @return mixed The elements default value
589 * @internal
590 */
591 public function getElementDefaultValueByIdentifier(string $elementIdentifier)
592 {
593 return ObjectAccess::getPropertyPath($this->elementDefaultValues, $elementIdentifier);
594 }
595
596 /**
597 * Move $pageToMove before $referencePage
598 *
599 * @param Page $pageToMove
600 * @param Page $referencePage
601 * @api
602 */
603 public function movePageBefore(Page $pageToMove, Page $referencePage)
604 {
605 $this->moveRenderableBefore($pageToMove, $referencePage);
606 }
607
608 /**
609 * Move $pageToMove after $referencePage
610 *
611 * @param Page $pageToMove
612 * @param Page $referencePage
613 * @api
614 */
615 public function movePageAfter(Page $pageToMove, Page $referencePage)
616 {
617 $this->moveRenderableAfter($pageToMove, $referencePage);
618 }
619
620 /**
621 * Remove $pageToRemove from form
622 *
623 * @param Page $pageToRemove
624 * @api
625 */
626 public function removePage(Page $pageToRemove)
627 {
628 $this->removeRenderable($pageToRemove);
629 }
630
631 /**
632 * Bind the current request & response to this form instance, effectively creating
633 * a new "instance" of the Form.
634 *
635 * @param Request $request
636 * @param Response $response
637 * @return FormRuntime
638 * @api
639 */
640 public function bind(Request $request, Response $response): FormRuntime
641 {
642 return $this->objectManager->get(FormRuntime::class, $this, $request, $response);
643 }
644
645 /**
646 * @param string $propertyPath
647 * @return ProcessingRule
648 * @api
649 */
650 public function getProcessingRule(string $propertyPath): ProcessingRule
651 {
652 if (!isset($this->processingRules[$propertyPath])) {
653 $this->processingRules[$propertyPath] = $this->objectManager->get(ProcessingRule::class);
654 }
655 return $this->processingRules[$propertyPath];
656 }
657
658 /**
659 * Get all mapping rules
660 *
661 * @return \TYPO3\CMS\Form\Mvc\ProcessingRule[]
662 * @internal
663 */
664 public function getProcessingRules(): array
665 {
666 return $this->processingRules;
667 }
668
669 /**
670 * @return array
671 * @internal
672 */
673 public function getTypeDefinitions(): array
674 {
675 return $this->typeDefinitions;
676 }
677
678 /**
679 * @return array
680 * @internal
681 */
682 public function getValidatorsDefinition(): array
683 {
684 return $this->validatorsDefinition;
685 }
686
687 /**
688 * Get the persistence identifier of the form
689 *
690 * @return string
691 * @internal
692 */
693 public function getPersistenceIdentifier(): string
694 {
695 return $this->persistenceIdentifier;
696 }
697
698 /**
699 * Set the renderer class name
700 *
701 * @param string $rendererClassName
702 * @api
703 */
704 public function setRendererClassName(string $rendererClassName)
705 {
706 $this->rendererClassName = $rendererClassName;
707 }
708
709 /**
710 * Get the classname of the renderer
711 *
712 * @return string
713 * @api
714 */
715 public function getRendererClassName(): string
716 {
717 return $this->rendererClassName;
718 }
719 }