<commit_before><?php

namespace Sympathy\Form;

use Symfony\Component\Translation\TranslatorInterface as Translator;
use DateTime;

/**
 * \Sympathy\Form\Form can be used to validate user input of any origin (POST data, CLI or SOAP/REST)
 *
 * The form classes return localized validation messages and pass on the form definition
 * to controllers that render the forms to HTML using view templates and interact with
 * models (see \Sympathy\Model).
 *
 * A major advantage of this modular approach is that developers can use unit testing to instantly
 * find bugs and tune the validation rules without the need for an HTML front-end and manual user
 * input.
 *
 * Form classes can inherit their definitions from each other. If needed, the validation behavior
 * can be changed using standard object-oriented methodologies (e.g. overwriting or extending
 * the parent methods).
 *
 * =============================| Description of form definition parameters |===========================================
 * caption              Field title (used for form rendering and in validation messages)
 * type                 Data type: int, numeric, scalar, list, bool, string, email, ip, url, date, datetime, time and switch
 * type_params          Optional parameters for data type validation
 * options              Array of possible values for the field (for select lists or radio button groups)
 * min                  Minimum value for numbers/dates, length for strings or number of elements for lists
 * max                  Maximum value for numbers/dates, length for strings or number of elements for lists
 * required             Field cannot be empty
 * readonly             User is not allowed to change the field
 * hidden               User can not see the field
 * default              Default value
 * checkbox             A checkbox-like form input element is used (the form class will assign false for
 *                      boolean fields or array() for lists, if the value is not passed to setDefinedValues()
 *                      or setDefinedWritableValues()). This is a work around, because browsers do not submit
 *                      any data for unchecked checkboxes or multi-select fields without a selected element.
 * regex                Regular expression to match against
 * matches              Field value must match another form field (e.g. for password or email validation).
 *                      Property can be prefixed with "!" to state that the fields must be different.
 * depends              Field is required, if the given form field is not empty
 * depends_value        Field is required, if the field defined in "depends" has this value
 * depends_value_empty  Field is required, if the field defined in "depends" is empty
 * depends_first_option Field is required, if the field defined in "depends" has the first value (see "options")
 * depends_last_option  Field is required, if the field defined in "depends" has the last value (see "options")
 * page                 Page number for multi-page forms
 * =====================================================================================================================
 *
 * @author <NAME> <<EMAIL>>
 * @package Sympathy
 * @license MIT
 */
class Form
{
    private $_definition = array(); // The form definition array (see documentation above)
    private $_values = array(); // The form values
    private $_errors = array(); // The form errors (after calling validate())
    private $_groups = array(); // Optional form element groups
    private $_options = null;

    protected $_factoryPrefix = '';
    protected $_factoryPostfix = '';
    protected $_validationDone = false; // Set to true, if the validation was executed

    protected $_translator = null;
    protected $_validator = null;

    protected $_params = array(); // Optional params for init()

    public function __construct(Translator $translator, Validator $validator, array $params = array())
    {
        $this->setTranslator($translator);
        $this->setValidator($validator);
        $this->init($params);
    }

    /**
     * Returns optional config parameter or throws Exception, if parameter does not exist
     *
     * @param string $name
     * @throws Exception
     * @return string
     */
    protected function getParam($name)
    {
        if (isset($this->_params[$name])) {
            throw new Exception ('Required parameter "' . $name . '" was not set');
        }

        return $this->_params[$name];
    }

    /**
     * Creates a new form instance
     *
     * @param $className string Form class name
     * @param $params array Optional config parameters for init()
     * @return Form
     */
    public function factory($className, array $params = array())
    {
        $className = $this->_factoryPrefix . '\\' .$className . $this->_factoryPostfix;

        $result = new $className ($this->getTranslator(), $this->getValidator(), $params);

        return $result;
    }

    public function setFactoryPrefix($prefix)
    {
        $this->_factoryPrefix = $prefix;
    }

    public function setFactoryPostfix($postfix)
    {
        $this->_factoryPostfix = $postfix;
    }

    /**
     * Init function (don't overwrite the constructor)
     *
     * @param array $params Optional parameters
     */
    protected function init(array $params = array())
    {
        // must be implemented by inherited form classes
    }

    /**
     * @param OptionsInterface $options
     * @return $this
     */
    public function setOptions(OptionsInterface $options)
    {
        $this->_options = $options;

        return $this;
    }

    /**
     * Returns the options list or instance (if parameter is empty)
     *
     * @param string $listName Optional name of options list (shortcut)
     * @throws Exception
     * @return OptionsInterface
     */
    public function getOptions($listName = '')
    {
        if (empty($this->_options)) {
            throw new Exception ('No options instance set');
        }

        if ($listName == '') {
            $result = $this->_options;
        } else {
            $result = $this->_options->get($listName);
        }

        return $result;
    }

    /**
     * Helper function to return default option string like "Please select"
     *
     * @param string $label The field label token
     * @return array
     */
    protected function defaultOption($label = 'please_select')
    {
        return array('' => $this->translate($label));
    }

    /**
     * Set form field group definition array
     *
     * Example:
     *
     * $this->setGroups(
     *      array(
     *          'first_group' => array('field1', 'field2'),
     *          'second_group' => array('field3')
     *      )
     *  );
     *
     * @param $groups array The group definition array
     * @return $this
     */
    protected function setGroups(array $groups)
    {
        $this->_groups = $groups;

        return $this;
    }

    /**
     * @return Translator
     * @throws Exception
     */
    public function getTranslator()
    {
        if (!$this->_translator) {
            throw new Exception('Translator was not set');
        }

        return $this->_translator;
    }

    /**
     * @param Translator $translator
     * @return $this
     */
    public function setTranslator(Translator $translator)
    {
        $this->_translator = $translator;

        return $this;
    }

    /**
     * @return Validator
     * @throws Exception
     */
    public function getValidator()
    {
        if (!$this->_validator) {
            throw new Exception('Validator was not set');
        }

        return $this->_validator;
    }

    /**
     * @param Validator $validator
     * @return $this
     */
    public function setValidator(Validator $validator)
    {
        $validator->setForm($this);

        $this->_validator = $validator;

        return $this;
    }

    /**
     * Sets the current locale e.g. en, de or fr
     */
    public function setLocale($locale)
    {
        $this->getTranslator()->setLocale($locale);

        return $this;
    }

    /**
     * Returns the current locale string
     *
     * @return string
     */
    public function getLocale()
    {
        return $this->getTranslator()->getLocale();
    }

    /**
     * Sets the form field definition array (see documentation)
     *
     * @param $definition array The form field definition array
     * @return $this
     */
    public function setDefinition(array $definition)
    {
        $this->_definition = $definition;

        return $this;
    }

    /**
     * Returns the form field definition(s)
     *
     * @param $key string Optional field name (only the single field definition is returned)
     * @param $propertyName string Optional field property name (only the property value is returned)
     * @throws Exception
     * @return mixed
     */
    public function getDefinition($key = null, $propertyName = null)
    {
        if (!is_array($this->_definition)) {
            throw new Exception('Form definition is not an array. Something went totally wrong.');
        } elseif (count($this->_definition) == 0) {
            throw new Exception('Form definition is empty.');
        } elseif ($key == null) {
            return $this->_definition;
        } elseif (isset($this->_definition[$key])) {
            if ($propertyName != null) {
                if (isset($this->_definition[$key][$propertyName])) {
                    return $this->_definition[$key][$propertyName];
                }

                return null;
            }

            return $this->_definition[$key];
        }

        throw new Exception('No form field definition found for "' . $key . '".');
    }

    /**
     * Adds a single form field definition
     *
     * @param $key string Field name
     * @param $definition array Field definition array
     * @throws Exception
     * @return $this
     */
    public function addDefinition($key, array $definition)
    {
        if (isset($this->_definition[$key])) {
            throw new Exception('Definition for ' . $key . ' already exists');
        }

        $this->_definition[$key] = $definition;

        return $this;
    }

    /**
     * Changes a single form field definition
     *
     * @param $key string Field name
     * @param $changes array Field definition array
     * @throws Exception
     * @return $this
     */
    public function changeDefinition($key, array $changes)
    {
        if (!isset($this->_definition[$key])) {
            throw new Exception('Definition for ' . $key . ' does not exist');
        }

        foreach ($changes as $prop => $val) {
            if ($val === null) {
                unset($this->_definition[$key][$prop]);
            } else {
                $this->_definition[$key][$prop] = $val;
            }
        }

        return $this;
    }

    /**
     * Returns the complete form (definition + values) as array, which can
     * be used in the view templates to render the form or be converted to
     * JSON/XML for Web services
     *
     * @return array
     */
    public function getAsArray()
    {
        $result = array();

        foreach ($this->_definition as $key => $def) {
            $result[$key] = $def;
            $value = $this->$key;
            $type = $def['type'];

            if (($type == 'date' || $type == 'datetime' || $type == 'time') && is_object($value)) {
                $value = $value->format($this->translate('form.' . $type));
            }

            $result[$key]['value'] = $value;
            $result[$key]['uid'] = 'id' . uniqid();
        }

        return $result;
    }


    /**
     * Returns form fields structured in groups (you must use setGroups() first)
     *
     * @return array
     */
    public function getAsGroupedArray()
    {
        $form = $this->getAsArray();
        $result = array();

        foreach ($this->_groups as $groupName => $memberKeys) {
            $members = array();

            foreach ($memberKeys as $key) {
                $members[$key] = $form[$key];
            }

            $result[$this->_('group_' . $groupName)] = $members;
        }

        return $result;
    }

    /**
     * Returns true, if the field is writable by the user (readonly property)
     *
     * @param $key string
     * @return bool
     */
    protected function isWritable($key)
    {
        return $this->getDefinition($key, 'readonly') != true;
    }

    /**
     * Returns true, if the field is a checkbox
     *
     * This is a workaround for Web form elements, that do not get submitted, if empty
     *
     * @param $key string
     * @return bool
     */
    protected function isCheckbox($key)
    {
        return $this->getDefinition($key, 'checkbox') == true;
    }

    /**
     * Sets the default types for values of form fields marked as checkboxes
     *
     * @param $key string The field name
     * @param $values array Reference to the array containing all form values
     * @return $this
     */
    protected function setCheckboxValueInArray($key, &$values)
    {
        if ($this->isCheckbox($key) && !array_key_exists($key, $values)) {
            $type = $this->getDefinition($key, 'type');
            switch ($type) {
                case 'list':
                    $values[$key] = array();
                    break;
                case 'bool':
                    $values[$key] = false;
                    break;
                default:
                    $values[$key] = null;
            }
        }

        return $this;
    }

    /**
     * Sets all form values (does not check, if the fields exist or if the fields are writable by the user)
     * Note: Throws an exception, if you try to set values for undefined fields
     *
     * @param $values array The values (key must be the field name)
     * @return $this
     */
    public function setAllValues(Array $values)
    {
        foreach ($values as $key => $value) {
            $this->$key = $value;
        }

        return $this;
    }

    /**
     * Interates through the form definition and sets the values for fields, that are present in the form definition
     *
     * @param $values array The values (key must be the field name)
     * @throws Exception
     * @return $this
     */
    public function setDefinedValues(array $values)
    {
        foreach ($this->_definition as $key => $value) {
            $this->setCheckboxValueInArray($key, $values);

            if (!array_key_exists($key, $values)) {
                throw new Exception ('Array provided to setDefinedValues() was not complete: ' . $key);
            }

            $this->$key = $values[$key];
        }

        return $this;
    }

    /**
     * Interates through the passed value array and sets the values for fields, that are writable by the user
     *
     * @param $values array The values (key must be the field name)
     * @return $this
     */
    public function setWritableValues(array $values)
    {
        foreach ($values as $key => $value) {
            if ($this->isWritable($key)) {
                $this->$key = $value;
            }
        }

        return $this;
    }

    /**
     * Sets the values for fields, that are present in the form definition
     * and that are writable by the user (recommended method for most use cases)
     *
     * @param $values array The values (key must be the field name)
     * @throws Exception
     * @return $this
     */
    public function setDefinedWritableValues(array $values)
    {
        foreach ($this->_definition as $key => $value) {
            if ($this->isWritable($key)) {
                $this->setCheckboxValueInArray($key, $values);

                if (!array_key_exists($key, $values)) {
                    throw new Exception ('Array provided to setDefinedWritableValues() was not complete: ' . $key);
                }


                $this->$key = $values[$key];
            }
        }

        return $this;
    }

    /**
     * Sets the values for fields on the given page, that are present in the form definition
     * and that are writable by the user (recommended method for most use cases)
     *
     * @param $values array The values (key must be the field name)
     * @param $page scalar
     * @throws Exception
     * @return $this
     */
    public function setWritableValuesOnPage(array $values, $page)
    {
        foreach ($this->_definition as $key => $value) {
            if (isset($value['page']) && $value['page'] == $page && $this->isWritable($key)) {
                $this->setCheckboxValueInArray($key, $values);<commit_msg>Renamed "checkbox" form field property to "optional"<commit_after><?php

namespace Sympathy\Form;

use Symfony\Component\Translation\TranslatorInterface as Translator;
use DateTime;

/**
 * \Sympathy\Form\Form can be used to validate user input of any origin (POST data, CLI or SOAP/REST)
 *
 * The form classes return localized validation messages and pass on the form definition
 * to controllers that render the forms to HTML using view templates and interact with
 * models (see \Sympathy\Model).
 *
 * A major advantage of this modular approach is that developers can use unit testing to instantly
 * find bugs and tune the validation rules without the need for an HTML front-end and manual user
 * input.
 *
 * Form classes can inherit their definitions from each other. If needed, the validation behavior
 * can be changed using standard object-oriented methodologies (e.g. overwriting or extending
 * the parent methods).
 *
 * =============================| Description of form definition parameters |===========================================
 * caption              Field title (used for form rendering and in validation messages)
 * type                 Data type: int, numeric, scalar, list, bool, string, email, ip, url, date, datetime, time and switch
 * type_params          Optional parameters for data type validation
 * options              Array of possible values for the field (for select lists or radio button groups)
 * min                  Minimum value for numbers/dates, length for strings or number of elements for lists
 * max                  Maximum value for numbers/dates, length for strings or number of elements for lists
 * required             Field cannot be empty
 * readonly             User is not allowed to change the field
 * hidden               User can not see the field
 * default              Default value
 * optional             A checkbox-like form input element is used (the form class will assign false for
 *                      boolean fields or array() for lists, if the value is not passed to setDefinedValues()
 *                      or setDefinedWritableValues()). This is a work around, because browsers do not submit
 *                      any data for unchecked checkboxes or multi-select fields without a selected element.
 * regex                Regular expression to match against
 * matches              Field value must match another form field (e.g. for password or email validation).
 *                      Property can be prefixed with "!" to state that the fields must be different.
 * depends              Field is required, if the given form field is not empty
 * depends_value        Field is required, if the field defined in "depends" has this value
 * depends_value_empty  Field is required, if the field defined in "depends" is empty
 * depends_first_option Field is required, if the field defined in "depends" has the first value (see "options")
 * depends_last_option  Field is required, if the field defined in "depends" has the last value (see "options")
 * page                 Page number for multi-page forms
 * =====================================================================================================================
 *
 * @author <NAME> <<EMAIL>>
 * @package Sympathy
 * @license MIT
 */
class Form
{
    private $_definition = array(); // The form definition array (see documentation above)
    private $_values = array(); // The form values
    private $_errors = array(); // The form errors (after calling validate())
    private $_groups = array(); // Optional form element groups
    private $_options = null;

    protected $_factoryPrefix = '';
    protected $_factoryPostfix = '';
    protected $_validationDone = false; // Set to true, if the validation was executed

    protected $_translator = null;
    protected $_validator = null;

    protected $_params = array(); // Optional params for init()

    public function __construct(Translator $translator, Validator $validator, array $params = array())
    {
        $this->setTranslator($translator);
        $this->setValidator($validator);
        $this->init($params);
    }

    /**
     * Returns optional config parameter or throws Exception, if parameter does not exist
     *
     * @param string $name
     * @throws Exception
     * @return string
     */
    protected function getParam($name)
    {
        if (isset($this->_params[$name])) {
            throw new Exception ('Required parameter "' . $name . '" was not set');
        }

        return $this->_params[$name];
    }

    /**
     * Creates a new form instance
     *
     * @param $className string Form class name
     * @param $params array Optional config parameters for init()
     * @return Form
     */
    public function factory($className, array $params = array())
    {
        $className = $this->_factoryPrefix . '\\' .$className . $this->_factoryPostfix;

        $result = new $className ($this->getTranslator(), $this->getValidator(), $params);

        return $result;
    }

    public function setFactoryPrefix($prefix)
    {
        $this->_factoryPrefix = $prefix;
    }

    public function setFactoryPostfix($postfix)
    {
        $this->_factoryPostfix = $postfix;
    }

    /**
     * Init function (don't overwrite the constructor)
     *
     * @param array $params Optional parameters
     */
    protected function init(array $params = array())
    {
        // must be implemented by inherited form classes
    }

    /**
     * @param OptionsInterface $options
     * @return $this
     */
    public function setOptions(OptionsInterface $options)
    {
        $this->_options = $options;

        return $this;
    }

    /**
     * Returns the options list or instance (if parameter is empty)
     *
     * @param string $listName Optional name of options list (shortcut)
     * @throws Exception
     * @return OptionsInterface
     */
    public function getOptions($listName = '')
    {
        if (empty($this->_options)) {
            throw new Exception ('No options instance set');
        }

        if ($listName == '') {
            $result = $this->_options;
        } else {
            $result = $this->_options->get($listName);
        }

        return $result;
    }

    /**
     * Helper function to return default option string like "Please select"
     *
     * @param string $label The field label token
     * @return array
     */
    protected function defaultOption($label = 'please_select')
    {
        return array('' => $this->translate($label));
    }

    /**
     * Set form field group definition array
     *
     * Example:
     *
     * $this->setGroups(
     *      array(
     *          'first_group' => array('field1', 'field2'),
     *          'second_group' => array('field3')
     *      )
     *  );
     *
     * @param $groups array The group definition array
     * @return $this
     */
    protected function setGroups(array $groups)
    {
        $this->_groups = $groups;

        return $this;
    }

    /**
     * @return Translator
     * @throws Exception
     */
    public function getTranslator()
    {
        if (!$this->_translator) {
            throw new Exception('Translator was not set');
        }

        return $this->_translator;
    }

    /**
     * @param Translator $translator
     * @return $this
     */
    public function setTranslator(Translator $translator)
    {
        $this->_translator = $translator;

        return $this;
    }

    /**
     * @return Validator
     * @throws Exception
     */
    public function getValidator()
    {
        if (!$this->_validator) {
            throw new Exception('Validator was not set');
        }

        return $this->_validator;
    }

    /**
     * @param Validator $validator
     * @return $this
     */
    public function setValidator(Validator $validator)
    {
        $validator->setForm($this);

        $this->_validator = $validator;

        return $this;
    }

    /**
     * Sets the current locale e.g. en, de or fr
     */
    public function setLocale($locale)
    {
        $this->getTranslator()->setLocale($locale);

        return $this;
    }

    /**
     * Returns the current locale string
     *
     * @return string
     */
    public function getLocale()
    {
        return $this->getTranslator()->getLocale();
    }

    /**
     * Sets the form field definition array (see documentation)
     *
     * @param $definition array The form field definition array
     * @return $this
     */
    public function setDefinition(array $definition)
    {
        $this->_definition = $definition;

        return $this;
    }

    /**
     * Returns the form field definition(s)
     *
     * @param $key string Optional field name (only the single field definition is returned)
     * @param $propertyName string Optional field property name (only the property value is returned)
     * @throws Exception
     * @return mixed
     */
    public function getDefinition($key = null, $propertyName = null)
    {
        if (!is_array($this->_definition)) {
            throw new Exception('Form definition is not an array. Something went totally wrong.');
        } elseif (count($this->_definition) == 0) {
            throw new Exception('Form definition is empty.');
        } elseif ($key == null) {
            return $this->_definition;
        } elseif (isset($this->_definition[$key])) {
            if ($propertyName != null) {
                if (isset($this->_definition[$key][$propertyName])) {
                    return $this->_definition[$key][$propertyName];
                }

                return null;
            }

            return $this->_definition[$key];
        }

        throw new Exception('No form field definition found for "' . $key . '".');
    }

    /**
     * Adds a single form field definition
     *
     * @param $key string Field name
     * @param $definition array Field definition array
     * @throws Exception
     * @return $this
     */
    public function addDefinition($key, array $definition)
    {
        if (isset($this->_definition[$key])) {
            throw new Exception('Definition for ' . $key . ' already exists');
        }

        $this->_definition[$key] = $definition;

        return $this;
    }

    /**
     * Changes a single form field definition
     *
     * @param $key string Field name
     * @param $changes array Field definition array
     * @throws Exception
     * @return $this
     */
    public function changeDefinition($key, array $changes)
    {
        if (!isset($this->_definition[$key])) {
            throw new Exception('Definition for ' . $key . ' does not exist');
        }

        foreach ($changes as $prop => $val) {
            if ($val === null) {
                unset($this->_definition[$key][$prop]);
            } else {
                $this->_definition[$key][$prop] = $val;
            }
        }

        return $this;
    }

    /**
     * Returns the complete form (definition + values) as array, which can
     * be used in the view templates to render the form or be converted to
     * JSON/XML for Web services
     *
     * @return array
     */
    public function getAsArray()
    {
        $result = array();

        foreach ($this->_definition as $key => $def) {
            $result[$key] = $def;
            $value = $this->$key;
            $type = $def['type'];

            if (($type == 'date' || $type == 'datetime' || $type == 'time') && is_object($value)) {
                $value = $value->format($this->translate('form.' . $type));
            }

            $result[$key]['value'] = $value;
            $result[$key]['uid'] = 'id' . uniqid();
        }

        return $result;
    }


    /**
     * Returns form fields structured in groups (you must use setGroups() first)
     *
     * @return array
     */
    public function getAsGroupedArray()
    {
        $form = $this->getAsArray();
        $result = array();

        foreach ($this->_groups as $groupName => $memberKeys) {
            $members = array();

            foreach ($memberKeys as $key) {
                $members[$key] = $form[$key];
            }

            $result[$this->_('group_' . $groupName)] = $members;
        }

        return $result;
    }

    /**
     * Returns true, if the field is writable by the user (readonly property)
     *
     * @param $key string
     * @return bool
     */
    protected function isWritable($key)
    {
        return $this->getDefinition($key, 'readonly') != true;
    }

    /**
     * Returns true, if the field is optional
     *
     * This is a workaround for Web form elements, that do not get submitted, if empty.
     * In general, this applies to non-checked checkboxes or to empty form elements
     * submitted by certain JavaScript frameworks (e.g. AngularJS).
     *
     * @param $key string
     * @return bool
     */
    protected function isOptional($key)
    {
        return ($this->getDefinition($key, 'checkbox') == true) || ($this->getDefinition($key, 'optional') == true);
    }

    /**
     * Sets the default types for values of form fields marked as checkboxes
     *
     * @param $key string The field name
     * @param $values array Reference to the array containing all form values
     * @return $this
     */
    protected function setOptionalValueInArray($key, &$values)
    {
        if ($this->isOptional($key) && !array_key_exists($key, $values)) {
            $type = $this->getDefinition($key, 'type');
            switch ($type) {
                case 'list':
                    $values[$key] = array();
                    break;
                case 'bool':
                    $values[$key] = false;
                    break;
                default:
                    $values[$key] = null;
            }
        }

        return $this;
    }

    /**
     * Sets all form values (does not check, if the fields exist or if the fields are writable by the user)
     * Note: Throws an exception, if you try to set values for undefined fields
     *
     * @param $values array The values (key must be the field name)
     * @return $this
     */
    public function setAllValues(Array $values)
    {
        foreach ($values as $key => $value) {
            $this->$key = $value;
        }

        return $this;
    }

    /**
     * Interates through the form definition and sets the values for fields, that are present in the form definition
     *
     * @param $values array The values (key must be the field name)
     * @throws Exception
     * @return $this
     */
    public function setDefinedValues(array $values)
    {
        foreach ($this->_definition as $key => $value) {
            $this->setOptionalValueInArray($key, $values);

            if (!array_key_exists($key, $values)) {
                throw new Exception ('Array provided to setDefinedValues() was not complete: ' . $key);
            }

            $this->$key = $values[$key];
        }

        return $this;
    }

    /**
     * Interates through the passed value array and sets the values for fields, that are writable by the user
     *
     * @param $values array The values (key must be the field name)
     * @return $this
     */
    public function setWritableValues(array $values)
    {
        foreach ($values as $key => $value) {
            if ($this->isWritable($key)) {
                $this->$key = $value;
            }
        }

        return $this;
    }

    /**
     * Sets the values for fields, that are present in the form definition
     * and that are writable by the user (recommended method for most use cases)
     *
     * @param $values array The values (key must be the field name)
     * @throws Exception
     * @return $this
     */
    public function setDefinedWritableValues(array $values)
    {
        foreach ($this->_definition as $key => $value) {
            if ($this->isWritable($key)) {
                $this->setOptionalValueInArray($key, $values);

                if (!array_key_exists($key, $values)) {
                    throw new Exception ('Array provided to setDefinedWritableValues() was not complete: ' . $key);
                }


                $this->$key = $values[$key];
            }
        }

        return $this;
    }

    /**
     * Sets the values for fields on the given page, that are present in the form definition
     * and that are writable by the user (recommended method for most use cases)
     *
     * @param $values array The values (key must be the field name)
     * @param $page scalar
     * @throws Exception
     * @return $this
     */
    public function setWritableValuesOnPage(array $values, $page)
    {
        foreach ($this->_definition as $key => $value) {
            if (isset($value['page']) && $value['page'] == $page && $this->isWritable($key)) {
                $this->setOptionalValueInArray($key, $values);
{'fraction_non_alphanumeric': 0.11922871234179096, 'fraction_numerical': 0.00023768494860062987, 'mean_word_length': 2.0965041398344066, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 166, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2236787', 'n_tokens_mistral': 8737, 'n_tokens_neox': 8497, 'n_words': 3040}