diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml index ee956f0fa0..7eb472fd97 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/collectors.xml @@ -13,7 +13,8 @@ Symfony\Component\HttpKernel\DataCollector\TimeDataCollector Symfony\Component\HttpKernel\DataCollector\MemoryDataCollector Symfony\Bundle\FrameworkBundle\DataCollector\RouterDataCollector - Symfony\Component\Form\Extension\DataCollector\Collector\FormCollector + Symfony\Component\Form\Extension\DataCollector\FormDataCollector + Symfony\Component\Form\Extension\DataCollector\FormDataExtractor @@ -55,8 +56,11 @@ - + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml index b0c0e3ff65..5d4faac4ac 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form_debug.xml @@ -5,11 +5,19 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + Symfony\Component\Form\Extension\DataCollector\Proxy\ResolvedTypeFactoryDataCollectorProxy Symfony\Component\Form\Extension\DataCollector\Type\DataCollectorTypeExtension - + + + + + + + + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig index ab9db1f28e..ecc0a42633 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig @@ -1,10 +1,12 @@ {% extends '@WebProfiler/Profiler/layout.html.twig' %} +{% from _self import form_tree_entry, form_tree_details %} + {% block toolbar %} {% if collector.data|length %} {% set icon %} Forms - {% if collector.errorCount %}{{ collector.errorCount }}{% else %}{{ collector.data|length }}{% endif %} + {% if collector.data.nb_errors %}{{ collector.data.nb_errors }}{% else %}{{ collector.data.forms|length }}{% endif %} {% endset %} {% include '@WebProfiler/Profiler/toolbar_item.html.twig' with { 'link': profiler_url } %} @@ -13,48 +15,331 @@ {% block menu %} - + Forms - {% if collector.data|length %} - {{ collector.data|length }} + {% if collector.data.forms|length %} + {{ collector.data.forms|length }} {% endif %} {% endblock %} {% block panel %} -

Form{% if collector.data|length > 1 %}s{% endif %}

+ + + {% if collector.data.forms|length %} +
+
+

Forms

+ +
    + {% for formName, formData in collector.data.forms %} + {{ form_tree_entry(formName, formData) }} + {% endfor %} +
+
+ + {% for formName, formData in collector.data.forms %} + {{ form_tree_details(formName, formData) }} + {% endfor %} +
+ {% else %} +

No forms were submitted for this request.

+ {% endif %} + + +{% endblock %} + +{% macro form_tree_entry(name, data) %} +
  • + {{ name }} + + {% if data.children|length > 1 %} +
      + {% for childName, childData in data.children %} + {{ _self.form_tree_entry(childName, childData) }} + {% endfor %} +
    + {% endif %} +
  • +{% endmacro %} + +{% macro form_tree_details(name, data) %} +
    +

    + {{ name }} + {% if data.type_class is defined %} + [{{ data.type }}] + {% endif %} +

    + + {% if data.errors is defined and data.errors|length > 0 %} +

    Errors

    - {% for formName, fields in collector.data %} -

    {{ formName }}

    - {% if fields %} - - - - + + - {% for fieldName, field in fields %} + {% for error in data.errors %} + + + + + {% endfor %} +
    FieldTypeValueMessagesMessageCause
    {{ error.message }}Unknown.
    + {% endif %} + + {% if data.default_data is defined %} +

    Default Data

    + + + + + + + + + + + + + + +
    Model Format + {% if data.default_data.model is defined %} +
    {{ data.default_data.model }}
    + {% else %} + same as normalized format + {% endif %} +
    Normalized Format
    {{ data.default_data.norm }}
    View Format + {% if data.default_data.view is defined %} +
    {{ data.default_data.view }}
    + {% else %} + same as normalized format + {% endif %} +
    + {% endif %} + + {% if data.submitted_data is defined %} +

    Submitted Data

    + + {% if data.submitted_data.norm is defined %} + - - - + + + + + + + + + +
    {{ fieldName }}{{ field.type }}{{ field.value }}View Format -
      - {% for errorMessage in field.errors %} -
    • - {{ errorMessage.message }} -
    • - {% endfor %} -
    + {% if data.submitted_data.view is defined %} +
    {{ data.submitted_data.view }}
    + {% else %} + same as normalized format + {% endif %}
    Normalized Format
    {{ data.submitted_data.norm }}
    Model Format + {% if data.submitted_data.model is defined %} +
    {{ data.submitted_data.model }}
    + {% else %} + same as normalized format + {% endif %} +
    + {% else %} +

    This form was not submitted.

    + {% endif %} + {% endif %} + + {% if data.passed_options is defined %} +

    Passed Options

    + + {% if data.passed_options|length %} + + + + + + + {% for option, value in data.passed_options %} + + + + + {% endfor %}
    OptionPassed ValueResolved Value
    {{ option }}
    {{ value }}
    + {% if data.resolved_options[option] is sameas(value) %} + same as passed value + {% else %} +
    {{ data.resolved_options[option] }}
    + {% endif %} +
    {% else %} - This form is valid. +

    No options where passed when constructing this form.

    {% endif %} - {% else %} - No forms were submitted for this request. + {% endif %} + + {% if data.resolved_options is defined %} +

    Resolved Options

    + + + + + + + {% for option, value in data.resolved_options %} + + + + + {% endfor %} +
    OptionValue
    {{ option }}
    {{ value }}
    + {% endif %} + +

    View Variables

    + + + + + + + {% for variable, value in data.view_vars %} + + + + + {% endfor %} +
    VariableValue
    {{ variable }}
    {{ value }}
    +
    + + {% for childName, childData in data.children %} + {{ _self.form_tree_details(childName, childData) }} {% endfor %} -{% endblock %} +{% endmacro %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig index 7f10150c72..3b45ca1116 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig @@ -63,6 +63,9 @@ table th, table td { font-size: 12px; padding: 8px 10px; } +table td em { + color: #aaa; +} fieldset { border: none; } @@ -70,6 +73,9 @@ abbr { border-bottom: 1px dotted #000; cursor: help; } +pre, code { + font-size: 0.9em; +} .clear { clear: both; height: 0; diff --git a/src/Symfony/Component/Form/Extension/DataCollector/Collector/FormCollector.php b/src/Symfony/Component/Form/Extension/DataCollector/Collector/FormCollector.php deleted file mode 100644 index 82564e4068..0000000000 --- a/src/Symfony/Component/Form/Extension/DataCollector/Collector/FormCollector.php +++ /dev/null @@ -1,130 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Form\Extension\DataCollector\Collector; - -use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\Form\FormEvent; -use Symfony\Component\Form\FormEvents; -use Symfony\Component\Form\FormInterface; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\DataCollector\DataCollector as BaseCollector; - -/** - * DataCollector for Form Validation. - * - * @author Robert Schönthal - */ -class FormCollector extends BaseCollector implements EventSubscriberInterface -{ - /** - * {@inheritDoc} - */ - public static function getSubscribedEvents() - { - return array(FormEvents::POST_SUBMIT => array('collectForm', -255)); - } - - /** - * {@inheritDoc} - */ - public function collect(Request $request, Response $response, \Exception $exception = null) - { - //nothing to do, everything is added with addError() - } - - /** - * Collects Form-Validation-Data and adds them to the Collector. - * - * @param FormEvent $event The event object - */ - public function collectForm(FormEvent $event) - { - $form = $event->getForm(); - - if ($form->isRoot()) { - $this->data[$form->getName()] = array(); - $this->addForm($form); - } - } - - /** - * Adds an Form-Element to the Collector. - * - * @param FormInterface $form - */ - private function addForm(FormInterface $form) - { - if ($form->getErrors()) { - $this->addError($form); - } - - // recursively add all child-errors - foreach ($form->all() as $field) { - $this->addForm($field); - } - } - - /** - * Adds a Form-Error to the Collector. - * - * @param FormInterface $form - */ - private function addError(FormInterface $form) - { - $storeData = array( - 'root' => $form->getRoot()->getName(), - 'name' => (string) $form->getPropertyPath(), - 'type' => $form->getConfig()->getType()->getName(), - 'errors' => $form->getErrors(), - 'value' => $this->varToString($form->getViewData()) - ); - - $this->data[$storeData['root']][$storeData['name']] = $storeData; - } - - /** - * {@inheritDoc} - */ - public function getName() - { - return 'form'; - } - - /** - * Returns all collected Data. - * - * @return array - */ - public function getData() - { - return $this->data; - } - - /** - * Returns the number of Forms with Errors. - * - * @return integer - */ - public function getErrorCount() - { - $errorCount = 0; - - foreach ($this->data as $form) { - if (count($form)) { - $errorCount++; - } - } - - return $errorCount; - } -} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/DataCollectorExtension.php b/src/Symfony/Component/Form/Extension/DataCollector/DataCollectorExtension.php index f9f8209188..941bd2102e 100644 --- a/src/Symfony/Component/Form/Extension/DataCollector/DataCollectorExtension.php +++ b/src/Symfony/Component/Form/Extension/DataCollector/DataCollectorExtension.php @@ -15,20 +15,22 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Form\AbstractExtension; /** - * DataCollectorExtension for collecting Form Validation Failures. + * Extension for collecting data of the forms on a page. * + * @since 2.4 * @author Robert Schönthal + * @author Bernhard Schussek */ class DataCollectorExtension extends AbstractExtension { /** * @var EventSubscriberInterface */ - private $eventSubscriber; + private $dataCollector; - public function __construct(EventSubscriberInterface $eventSubscriber) + public function __construct(FormDataCollectorInterface $dataCollector) { - $this->eventSubscriber = $eventSubscriber; + $this->dataCollector = $dataCollector; } /** @@ -37,7 +39,7 @@ class DataCollectorExtension extends AbstractExtension protected function loadTypeExtensions() { return array( - new Type\DataCollectorTypeExtension($this->eventSubscriber) + new Type\DataCollectorTypeExtension($this->dataCollector) ); } } diff --git a/src/Symfony/Component/Form/Extension/DataCollector/EventListener/DataCollectorListener.php b/src/Symfony/Component/Form/Extension/DataCollector/EventListener/DataCollectorListener.php new file mode 100644 index 0000000000..4822989b58 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/EventListener/DataCollectorListener.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; + +/** + * Listener that invokes a data collector for the {@link FormEvents::POST_SET_DATA} + * and {@link FormEvents::POST_SUBMIT} events. + * + * @since 2.4 + * @author Bernhard Schussek + */ +class DataCollectorListener implements EventSubscriberInterface +{ + /** + * @var FormDataCollectorInterface + */ + private $dataCollector; + + public function __construct(FormDataCollectorInterface $dataCollector) + { + $this->dataCollector = $dataCollector; + } + + /** + * {@inheritDoc} + */ + public static function getSubscribedEvents() + { + return array( + // High priority in order to be called as soon as possible + FormEvents::POST_SET_DATA => array('postSetData', 255), + // Low priority in order to be called as late as possible + FormEvents::POST_SUBMIT => array('postSubmit', -255), + ); + } + + /** + * Listener for the {@link FormEvents::POST_SET_DATA} event. + * + * @param FormEvent $event The event object + */ + public function postSetData(FormEvent $event) + { + if ($event->getForm()->isRoot()) { + // Collect basic information about each form + $this->dataCollector->collectConfiguration($event->getForm()); + + // Collect the default data + $this->dataCollector->collectDefaultData($event->getForm()); + } + } + + /** + * Listener for the {@link FormEvents::POST_SUBMIT} event. + * + * @param FormEvent $event The event object + */ + public function postSubmit(FormEvent $event) + { + if ($event->getForm()->isRoot()) { + // Collect the submitted data of each form + $this->dataCollector->collectSubmittedData($event->getForm()); + + // Assemble a form tree + // This is done again in collectViewVariables(), but that method + // is not guaranteed to be called (i.e. when no view is created) + $this->dataCollector->buildPreliminaryFormTree($event->getForm()); + } + } + +} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/FormDataCollector.php b/src/Symfony/Component/Form/Extension/DataCollector/FormDataCollector.php new file mode 100644 index 0000000000..543d6c5b04 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/FormDataCollector.php @@ -0,0 +1,274 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\DataCollector\DataCollector; + +/** + * Data collector for {@link \Symfony\Component\Form\FormInterface} instances. + * + * @since 2.4 + * @author Robert Schönthal + * @author Bernhard Schussek + */ +class FormDataCollector extends DataCollector implements FormDataCollectorInterface +{ + /** + * @var FormDataExtractor + */ + private $dataExtractor; + + /** + * Stores the collected data per {@link FormInterface} instance. + * + * Uses the hashes of the forms as keys. This is preferrable over using + * {@link \SplObjectStorage}, because in this way no references are kept + * to the {@link FormInterface} instances. + * + * @var array + */ + private $dataByForm; + + /** + * Stores the collected data per {@link FormView} instance. + * + * Uses the hashes of the views as keys. This is preferrable over using + * {@link \SplObjectStorage}, because in this way no references are kept + * to the {@link FormView} instances. + * + * @var array + */ + private $dataByView; + + /** + * Connects {@link FormView} with {@link FormInterface} instances. + * + * Uses the hashes of the views as keys and the hashes of the forms as + * values. This is preferrable over storing the objects directly, because + * this way they can safely be discarded by the GC. + * + * @var array + */ + private $formsByView; + + public function __construct(FormDataExtractorInterface $dataExtractor) + { + $this->dataExtractor = $dataExtractor; + $this->data = array( + 'forms' => array(), + 'nb_errors' => 0, + ); + } + + /** + * Does nothing. The data is collected during the form event listeners. + */ + public function collect(Request $request, Response $response, \Exception $exception = null) + { + } + + /** + * {@inheritdoc} + */ + public function associateFormWithView(FormInterface $form, FormView $view) + { + $this->formsByView[spl_object_hash($view)] = spl_object_hash($form); + } + + /** + * {@inheritdoc} + */ + public function collectConfiguration(FormInterface $form) + { + $hash = spl_object_hash($form); + + if (!isset($this->dataByForm[$hash])) { + $this->dataByForm[$hash] = array(); + } + + $this->dataByForm[$hash] = array_replace( + $this->dataByForm[$hash], + $this->dataExtractor->extractConfiguration($form) + ); + + foreach ($form as $child) { + $this->collectConfiguration($child); + } + } + + /** + * {@inheritdoc} + */ + public function collectDefaultData(FormInterface $form) + { + $hash = spl_object_hash($form); + + if (!isset($this->dataByForm[$hash])) { + $this->dataByForm[$hash] = array(); + } + + $this->dataByForm[$hash] = array_replace( + $this->dataByForm[$hash], + $this->dataExtractor->extractDefaultData($form) + ); + + foreach ($form as $child) { + $this->collectDefaultData($child); + } + } + + /** + * {@inheritdoc} + */ + public function collectSubmittedData(FormInterface $form) + { + $hash = spl_object_hash($form); + + if (!isset($this->dataByForm[$hash])) { + $this->dataByForm[$hash] = array(); + } + + $this->dataByForm[$hash] = array_replace( + $this->dataByForm[$hash], + $this->dataExtractor->extractSubmittedData($form) + ); + + // Count errors + if (isset($this->dataByForm[$hash]['errors'])) { + $this->data['nb_errors'] += count($this->dataByForm[$hash]['errors']); + } + + foreach ($form as $child) { + $this->collectSubmittedData($child); + } + } + + /** + * {@inheritdoc} + */ + public function collectViewVariables(FormView $view) + { + $hash = spl_object_hash($view); + + if (!isset($this->dataByView[$hash])) { + $this->dataByView[$hash] = array(); + } + + $this->dataByView[$hash] = array_replace( + $this->dataByView[$hash], + $this->dataExtractor->extractViewVariables($view) + ); + + foreach ($view->children as $child) { + $this->collectViewVariables($child); + } + } + + /** + * {@inheritdoc} + */ + public function buildPreliminaryFormTree(FormInterface $form) + { + $this->data['forms'][$form->getName()] = array(); + + $this->recursiveBuildPreliminaryFormTree($form, $this->data['forms'][$form->getName()]); + } + + /** + * {@inheritdoc} + */ + public function buildFinalFormTree(FormInterface $form, FormView $view) + { + $this->data['forms'][$form->getName()] = array(); + + $this->recursiveBuildFinalFormTree($form, $view, $this->data['forms'][$form->getName()]); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'form'; + } + + /** + * {@inheritdoc} + */ + public function getData() + { + return $this->data; + } + + private function recursiveBuildPreliminaryFormTree(FormInterface $form, &$output = null) + { + $hash = spl_object_hash($form); + + $output = isset($this->dataByForm[$hash]) + ? $this->dataByForm[$hash] + : array(); + + $output['children'] = array(); + + foreach ($form as $name => $child) { + $output['children'][$name] = array(); + + $this->recursiveBuildPreliminaryFormTree($child, $output['children'][$name]); + } + } + + private function recursiveBuildFinalFormTree(FormInterface $form = null, FormView $view, &$output = null) + { + $viewHash = spl_object_hash($view); + $formHash = null; + + if (null !== $form) { + $formHash = spl_object_hash($form); + } elseif (isset($this->formsByView[$viewHash])) { + // The FormInterface instance of the CSRF token is never contained in + // the FormInterface tree of the form, so we need to get the + // corresponding FormInterface instance for its view in a different way + $formHash = $this->formsByView[$viewHash]; + } + + $output = isset($this->dataByView[$viewHash]) + ? $this->dataByView[$viewHash] + : array(); + + if (null !== $formHash) { + $output = array_replace( + $output, + isset($this->dataByForm[$formHash]) + ? $this->dataByForm[$formHash] + : array() + ); + } + + $output['children'] = array(); + + foreach ($view->children as $name => $childView) { + // The CSRF token, for example, is never added to the form tree. + // It is only present in the view. + $childForm = null !== $form && $form->has($name) + ? $form->get($name) + : null; + + $output['children'][$name] = array(); + + $this->recursiveBuildFinalFormTree($childForm, $childView, $output['children'][$name]); + } + } +} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/FormDataCollectorInterface.php b/src/Symfony/Component/Form/Extension/DataCollector/FormDataCollectorInterface.php new file mode 100644 index 0000000000..9a805029f8 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/FormDataCollectorInterface.php @@ -0,0 +1,102 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; + +/** + * Collects and structures information about forms. + * + * @since 2.4 + * @author Bernhard Schussek + */ +interface FormDataCollectorInterface extends DataCollectorInterface +{ + /** + * Stores configuration data of the given form and its children. + * + * @param FormInterface $form A root form + */ + public function collectConfiguration(FormInterface $form); + + /** + * Stores the default data of the given form and its children. + * + * @param FormInterface $form A root form + */ + public function collectDefaultData(FormInterface $form); + + /** + * Stores the submitted data of the given form and its children. + * + * @param FormInterface $form A root form + */ + public function collectSubmittedData(FormInterface $form); + + /** + * Stores the view variables of the given form view and its children. + * + * @param FormView $view A root form view + */ + public function collectViewVariables(FormView $view); + + /** + * Specifies that the given objects represent the same conceptual form. + * + * @param FormInterface $form A form object + * @param FormView $view A view object + */ + public function associateFormWithView(FormInterface $form, FormView $view); + + /** + * Assembles the data collected about the given form and its children as + * a tree-like data structure. + * + * The result can be queried using {@link getData()}. + * + * @param FormInterface $form A root form + */ + public function buildPreliminaryFormTree(FormInterface $form); + + /** + * Assembles the data collected about the given form and its children as + * a tree-like data structure. + * + * The result can be queried using {@link getData()}. + * + * Contrary to {@link buildPreliminaryFormTree()}, a {@link FormView} + * object has to be passed. The tree structure of this view object will be + * used for structuring the resulting data. That means, if a child is + * present in the view, but not in the form, it will be present in the final + * data array anyway. + * + * When {@link FormView} instances are present in the view tree, for which + * no corresponding {@link FormInterface} objects can be found in the form + * tree, only the view data will be included in the result. If a + * corresponding {@link FormInterface} exists otherwise, call + * {@link associateFormWithView()} before calling this method. + * + * @param FormInterface $form A root form + * @param FormView $view A root view + */ + public function buildFinalFormTree(FormInterface $form, FormView $view); + + /** + * Returns all collected data. + * + * @return array + */ + public function getData(); + +} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractor.php b/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractor.php new file mode 100644 index 0000000000..ad401796bd --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractor.php @@ -0,0 +1,135 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\HttpKernel\DataCollector\Util\ValueExporter; + +/** + * Default implementation of {@link FormDataExtractorInterface}. + * + * @since 2.4 + * @author Bernhard Schussek + */ +class FormDataExtractor implements FormDataExtractorInterface +{ + /** + * @var ValueExporter + */ + private $valueExporter; + + /** + * Constructs a new data extractor. + */ + public function __construct(ValueExporter $valueExporter = null) + { + $this->valueExporter = $valueExporter ?: new ValueExporter(); + } + + /** + * {@inheritdoc} + */ + public function extractConfiguration(FormInterface $form) + { + $data = array( + 'type' => $form->getConfig()->getType()->getName(), + 'type_class' => get_class($form->getConfig()->getType()->getInnerType()), + 'synchronized' => $this->valueExporter->exportValue($form->isSynchronized()), + 'passed_options' => array(), + 'resolved_options' => array(), + ); + + foreach ($form->getConfig()->getAttribute('data_collector/passed_options', array()) as $option => $value) { + $data['passed_options'][$option] = $this->valueExporter->exportValue($value); + } + + foreach ($form->getConfig()->getOptions() as $option => $value) { + $data['resolved_options'][$option] = $this->valueExporter->exportValue($value); + } + + ksort($data['passed_options']); + ksort($data['resolved_options']); + + return $data; + } + + /** + * {@inheritdoc} + */ + public function extractDefaultData(FormInterface $form) + { + $data = array( + 'default_data' => array( + 'norm' => $this->valueExporter->exportValue($form->getNormData()), + ), + 'submitted_data' => array(), + ); + + if ($form->getData() !== $form->getNormData()) { + $data['default_data']['model'] = $this->valueExporter->exportValue($form->getData()); + } + + if ($form->getViewData() !== $form->getNormData()) { + $data['default_data']['view'] = $this->valueExporter->exportValue($form->getViewData()); + } + + return $data; + } + + /** + * {@inheritdoc} + */ + public function extractSubmittedData(FormInterface $form) + { + $data = array( + 'submitted_data' => array( + 'norm' => $this->valueExporter->exportValue($form->getNormData()), + ), + 'errors' => array(), + ); + + if ($form->getViewData() !== $form->getNormData()) { + $data['submitted_data']['view'] = $this->valueExporter->exportValue($form->getViewData()); + } + + if ($form->getData() !== $form->getNormData()) { + $data['submitted_data']['model'] = $this->valueExporter->exportValue($form->getData()); + } + + foreach ($form->getErrors() as $error) { + $data['errors'][] = array( + 'message' => $error->getMessage(), + ); + } + + $data['synchronized'] = $this->valueExporter->exportValue($form->isSynchronized()); + + return $data; + } + + /** + * {@inheritdoc} + */ + public function extractViewVariables(FormView $view) + { + $data = array(); + + foreach ($view->vars as $varName => $value) { + $data['view_vars'][$varName] = $this->valueExporter->exportValue($value); + } + + ksort($data['view_vars']); + + return $data; + } +} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractorInterface.php b/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractorInterface.php new file mode 100644 index 0000000000..d47496552d --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/FormDataExtractorInterface.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector; + +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; + +/** + * Extracts arrays of information out of forms. + * + * @since 2.4 + * @author Bernhard Schussek + */ +interface FormDataExtractorInterface +{ + /** + * Extracts the configuration data of a form. + * + * @param FormInterface $form The form + * + * @return array Information about the form's configuration + */ + public function extractConfiguration(FormInterface $form); + + /** + * Extracts the default data of a form. + * + * @param FormInterface $form The form + * + * @return array Information about the form's default data + */ + public function extractDefaultData(FormInterface $form); + + /** + * Extracts the submitted data of a form. + * + * @param FormInterface $form The form + * + * @return array Information about the form's submitted data + */ + public function extractSubmittedData(FormInterface $form); + + /** + * Extracts the view variables of a form. + * + * @param FormView $view The form view + * + * @return array Information about the view's variables + */ + public function extractViewVariables(FormView $view); +} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php b/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php new file mode 100644 index 0000000000..960048a0c2 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php @@ -0,0 +1,148 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector\Proxy; + +use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\Form\ResolvedFormTypeInterface; + +/** + * Proxy that invokes a data collector when creating a form and its view. + * + * @since 2.4 + * @author Bernhard Schussek + */ +class ResolvedTypeDataCollectorProxy implements ResolvedFormTypeInterface +{ + /** + * @var ResolvedFormTypeInterface + */ + private $proxiedType; + + /** + * @var FormDataCollectorInterface + */ + private $dataCollector; + + public function __construct(ResolvedFormTypeInterface $proxiedType, FormDataCollectorInterface $dataCollector) + { + $this->proxiedType = $proxiedType; + $this->dataCollector = $dataCollector; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->proxiedType->getName(); + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + return $this->proxiedType->getParent(); + } + + /** + * {@inheritdoc} + */ + public function getInnerType() + { + return $this->proxiedType->getInnerType(); + } + + /** + * {@inheritdoc} + */ + public function getTypeExtensions() + { + return $this->proxiedType->getTypeExtensions(); + } + + /** + * {@inheritdoc} + */ + public function createBuilder(FormFactoryInterface $factory, $name, array $options = array()) + { + $builder = $this->proxiedType->createBuilder($factory, $name, $options); + + $builder->setAttribute('data_collector/passed_options', $options); + $builder->setType($this); + + return $builder; + } + + /** + * {@inheritdoc} + */ + public function createView(FormInterface $form, FormView $parent = null) + { + return $this->proxiedType->createView($form, $parent); + } + + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $this->proxiedType->buildForm($builder, $options); + } + + /** + * {@inheritdoc} + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + $this->proxiedType->buildView($view, $form, $options); + } + + /** + * {@inheritdoc} + */ + public function finishView(FormView $view, FormInterface $form, array $options) + { + $this->proxiedType->finishView($view, $form, $options); + + // Remember which view belongs to which form instance, so that we can + // get the collected data for a view when its form instance is not + // available (e.g. CSRF token) + $this->dataCollector->associateFormWithView($form, $view); + + // Since the CSRF token is only present in the FormView tree, we also + // need to check the FormView tree instead of calling isRoot() on the + // FormInterface tree + if (null === $view->parent) { + $this->dataCollector->collectViewVariables($view); + + // Re-assemble data, in case FormView instances were added, for + // which no FormInterface instances were present (e.g. CSRF token). + // Since finishView() is called after finishing the views of all + // children, we can safely assume that information has been + // collected about the complete form tree. + $this->dataCollector->buildFinalFormTree($form, $view); + } + } + + /** + * {@inheritdoc} + */ + public function getOptionsResolver() + { + return $this->proxiedType->getOptionsResolver(); + } +} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php b/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php new file mode 100644 index 0000000000..f15b720585 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeFactoryDataCollectorProxy.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\DataCollector\Proxy; + +use Symfony\Component\Form\Exception; +use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface; +use Symfony\Component\Form\FormTypeInterface; +use Symfony\Component\Form\ResolvedFormTypeFactoryInterface; +use Symfony\Component\Form\ResolvedFormTypeInterface; + +/** + * Proxy that wraps resolved types into {@link ResolvedTypeDataCollectorProxy} + * instances. + * + * @since 2.4 + * @author Bernhard Schussek + */ +class ResolvedTypeFactoryDataCollectorProxy implements ResolvedFormTypeFactoryInterface +{ + /** + * @var ResolvedFormTypeFactoryInterface + */ + private $proxiedFactory; + + /** + * @var FormDataCollectorInterface + */ + private $dataCollector; + + public function __construct(ResolvedFormTypeFactoryInterface $proxiedFactory, FormDataCollectorInterface $dataCollector) + { + $this->proxiedFactory = $proxiedFactory; + $this->dataCollector = $dataCollector; + } + + /** + * {@inheritdoc} + */ + public function createResolvedType(FormTypeInterface $type, array $typeExtensions, ResolvedFormTypeInterface $parent = null) + { + return new ResolvedTypeDataCollectorProxy( + $this->proxiedFactory->createResolvedType($type, $typeExtensions, $parent), + $this->dataCollector + ); + } +} diff --git a/src/Symfony/Component/Form/Extension/DataCollector/Type/DataCollectorTypeExtension.php b/src/Symfony/Component/Form/Extension/DataCollector/Type/DataCollectorTypeExtension.php index 8bc849b74b..1b5cfb695d 100644 --- a/src/Symfony/Component/Form/Extension/DataCollector/Type/DataCollectorTypeExtension.php +++ b/src/Symfony/Component/Form/Extension/DataCollector/Type/DataCollectorTypeExtension.php @@ -11,25 +11,28 @@ namespace Symfony\Component\Form\Extension\DataCollector\Type; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\DataCollector\EventListener\DataCollectorListener; +use Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface; use Symfony\Component\Form\FormBuilderInterface; /** - * DataCollector Type Extension for collecting invalid Forms. + * Type extension for collecting data of a form with this type. * + * @since 2.4 * @author Robert Schönthal + * @author Bernhard Schussek */ class DataCollectorTypeExtension extends AbstractTypeExtension { /** - * @var EventSubscriberInterface + * @var \Symfony\Component\EventDispatcher\EventSubscriberInterface */ - private $eventSubscriber; + private $listener; - public function __construct(EventSubscriberInterface $eventSubscriber) + public function __construct(FormDataCollectorInterface $dataCollector) { - $this->eventSubscriber = $eventSubscriber; + $this->listener = new DataCollectorListener($dataCollector); } /** @@ -37,7 +40,7 @@ class DataCollectorTypeExtension extends AbstractTypeExtension */ public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->addEventSubscriber($this->eventSubscriber); + $builder->addEventSubscriber($this->listener); } /** diff --git a/src/Symfony/Component/Form/Tests/Extension/DataCollector/Collector/FormCollectorTest.php b/src/Symfony/Component/Form/Tests/Extension/DataCollector/Collector/FormCollectorTest.php deleted file mode 100644 index 20bf9878df..0000000000 --- a/src/Symfony/Component/Form/Tests/Extension/DataCollector/Collector/FormCollectorTest.php +++ /dev/null @@ -1,58 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Form\Tests\Extension\DataCollector\Collector; - -use Symfony\Component\Form\Extension\DataCollector\Collector\FormCollector; -use Symfony\Component\Form\FormEvent; -use Symfony\Component\Form\FormEvents; - -class FormCollectorTest extends \PHPUnit_Framework_TestCase -{ - public function testSubscribedEvents() - { - $events = FormCollector::getSubscribedEvents(); - - $this->assertInternalType('array', $events); - $this->assertEquals(array(FormEvents::POST_SUBMIT => array('collectForm', -255)), $events); - } - - public function testCollect() - { - $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); - $subForm = $this->getMock('Symfony\Component\Form\Test\FormInterface'); - - $type = $this->getMock('Symfony\Component\Form\FormTypeInterface'); - $type->expects($this->atLeastOnce())->method('getName')->will($this->returnValue('fizz')); - - $config = $this->getMock('Symfony\Component\Form\FormConfigInterface'); - $config->expects($this->atLeastOnce())->method('getType')->will($this->returnValue($type)); - - $form->expects($this->atLeastOnce())->method('all')->will($this->returnValue(array($subForm))); - $form->expects($this->atLeastOnce())->method('isRoot')->will($this->returnValue(true)); - $form->expects($this->atLeastOnce())->method('getName')->will($this->returnValue('foo')); - - $subForm->expects($this->atLeastOnce())->method('all')->will($this->returnValue(array())); - $subForm->expects($this->atLeastOnce())->method('getErrors')->will($this->returnValue(array('foo'))); - $subForm->expects($this->atLeastOnce())->method('getRoot')->will($this->returnValue($form)); - $subForm->expects($this->atLeastOnce())->method('getConfig')->will($this->returnValue($config)); - $subForm->expects($this->atLeastOnce())->method('getPropertyPath')->will($this->returnValue('bar')); - $subForm->expects($this->atLeastOnce())->method('getViewData')->will($this->returnValue('bazz')); - - $event = new FormEvent($form, array()); - $c = new FormCollector(); - $c->collectForm($event); - - $this->assertInternalType('array', $c->getData()); - $this->assertEquals(1, $c->getErrorCount()); - $this->assertEquals(array('foo' => array('bar' => array('value' => 'bazz', 'root' => 'foo', 'type' => 'fizz', 'name' => 'bar', 'errors' => array('foo')))), $c->getData()); - } -} - \ No newline at end of file diff --git a/src/Symfony/Component/Form/Tests/Extension/DataCollector/DataCollectorExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/DataCollector/DataCollectorExtensionTest.php index 14ab0eac1b..9f5991c105 100644 --- a/src/Symfony/Component/Form/Tests/Extension/DataCollector/DataCollectorExtensionTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/DataCollector/DataCollectorExtensionTest.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Form\Tests\Extension\DataCollector; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Form\Extension\DataCollector\DataCollectorExtension; /** @@ -25,14 +24,14 @@ class DataCollectorExtensionTest extends \PHPUnit_Framework_TestCase private $extension; /** - * @var EventSubscriberInterface + * @var \PHPUnit_Framework_MockObject_MockObject */ - private $eventSubscriber; + private $dataCollector; public function setUp() { - $this->eventSubscriber = $this->getMock('Symfony\Component\EventDispatcher\EventSubscriberInterface'); - $this->extension = new DataCollectorExtension($this->eventSubscriber); + $this->dataCollector = $this->getMock('Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface'); + $this->extension = new DataCollectorExtension($this->dataCollector); } public function testLoadTypeExtensions() @@ -44,4 +43,3 @@ class DataCollectorExtensionTest extends \PHPUnit_Framework_TestCase $this->assertInstanceOf('Symfony\Component\Form\Extension\DataCollector\Type\DataCollectorTypeExtension', array_shift($typeExtensions)); } } - \ No newline at end of file diff --git a/src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataCollectorTest.php b/src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataCollectorTest.php new file mode 100644 index 0000000000..c31f62c7e0 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataCollectorTest.php @@ -0,0 +1,465 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\DataCollector; + +use Symfony\Component\Form\Extension\DataCollector\FormDataCollector; +use Symfony\Component\Form\Form; +use Symfony\Component\Form\FormBuilder; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormView; + +class FormDataCollectorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $dataExtractor; + + /** + * @var FormDataCollector + */ + private $dataCollector; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $dispatcher; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $factory; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $dataMapper; + + /** + * @var Form + */ + private $form; + + /** + * @var Form + */ + private $childForm; + + /** + * @var FormView + */ + private $view; + + /** + * @var FormView + */ + private $childView; + + + protected function setUp() + { + $this->dataExtractor = $this->getMock('Symfony\Component\Form\Extension\DataCollector\FormDataExtractorInterface'); + $this->dataCollector = new FormDataCollector($this->dataExtractor); + $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); + $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface'); + $this->dataMapper = $this->getMock('Symfony\Component\Form\DataMapperInterface'); + $this->form = $this->createForm('name'); + $this->childForm = $this->createForm('child'); + $this->view = new FormView(); + $this->childView = new FormView(); + } + + public function testBuildPreliminaryFormTree() + { + $this->form->add($this->childForm); + + $this->dataExtractor->expects($this->at(0)) + ->method('extractConfiguration') + ->with($this->form) + ->will($this->returnValue(array('config' => 'foo'))); + $this->dataExtractor->expects($this->at(1)) + ->method('extractConfiguration') + ->with($this->childForm) + ->will($this->returnValue(array('config' => 'bar'))); + + $this->dataExtractor->expects($this->at(2)) + ->method('extractDefaultData') + ->with($this->form) + ->will($this->returnValue(array('default_data' => 'foo'))); + $this->dataExtractor->expects($this->at(3)) + ->method('extractDefaultData') + ->with($this->childForm) + ->will($this->returnValue(array('default_data' => 'bar'))); + + $this->dataExtractor->expects($this->at(4)) + ->method('extractSubmittedData') + ->with($this->form) + ->will($this->returnValue(array('submitted_data' => 'foo'))); + $this->dataExtractor->expects($this->at(5)) + ->method('extractSubmittedData') + ->with($this->childForm) + ->will($this->returnValue(array('submitted_data' => 'bar'))); + + $this->dataCollector->collectConfiguration($this->form); + $this->dataCollector->collectDefaultData($this->form); + $this->dataCollector->collectSubmittedData($this->form); + $this->dataCollector->buildPreliminaryFormTree($this->form); + + $this->assertSame(array( + 'forms' => array( + 'name' => array( + 'config' => 'foo', + 'default_data' => 'foo', + 'submitted_data' => 'foo', + 'children' => array( + 'child' => array( + 'config' => 'bar', + 'default_data' => 'bar', + 'submitted_data' => 'bar', + 'children' => array(), + ), + ), + ), + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + public function testBuildMultiplePreliminaryFormTrees() + { + $form1 = $this->createForm('form1'); + $form2 = $this->createForm('form2'); + + $this->dataExtractor->expects($this->at(0)) + ->method('extractConfiguration') + ->with($form1) + ->will($this->returnValue(array('config' => 'foo'))); + $this->dataExtractor->expects($this->at(1)) + ->method('extractConfiguration') + ->with($form2) + ->will($this->returnValue(array('config' => 'bar'))); + + $this->dataCollector->collectConfiguration($form1); + $this->dataCollector->collectConfiguration($form2); + $this->dataCollector->buildPreliminaryFormTree($form1); + + $this->assertSame(array( + 'forms' => array( + 'form1' => array( + 'config' => 'foo', + 'children' => array(), + ), + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + + $this->dataCollector->buildPreliminaryFormTree($form2); + + $this->assertSame(array( + 'forms' => array( + 'form1' => array( + 'config' => 'foo', + 'children' => array(), + ), + 'form2' => array( + 'config' => 'bar', + 'children' => array(), + ), + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + public function testBuildSamePreliminaryFormTreeMultipleTimes() + { + $this->dataExtractor->expects($this->at(0)) + ->method('extractConfiguration') + ->with($this->form) + ->will($this->returnValue(array('config' => 'foo'))); + + $this->dataExtractor->expects($this->at(1)) + ->method('extractDefaultData') + ->with($this->form) + ->will($this->returnValue(array('default_data' => 'foo'))); + + $this->dataCollector->collectConfiguration($this->form); + $this->dataCollector->buildPreliminaryFormTree($this->form); + + $this->assertSame(array( + 'forms' => array( + 'name' => array( + 'config' => 'foo', + 'children' => array(), + ), + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + + $this->dataCollector->collectDefaultData($this->form); + $this->dataCollector->buildPreliminaryFormTree($this->form); + + $this->assertSame(array( + 'forms' => array( + 'name' => array( + 'config' => 'foo', + 'default_data' => 'foo', + 'children' => array(), + ), + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + public function testBuildPreliminaryFormTreeWithoutCollectingAnyData() + { + $this->dataCollector->buildPreliminaryFormTree($this->form); + + $this->assertSame(array( + 'forms' => array( + 'name' => array( + 'children' => array(), + ), + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + public function testBuildFinalFormTree() + { + $this->form->add($this->childForm); + $this->view->children['child'] = $this->childView; + + $this->dataExtractor->expects($this->at(0)) + ->method('extractConfiguration') + ->with($this->form) + ->will($this->returnValue(array('config' => 'foo'))); + $this->dataExtractor->expects($this->at(1)) + ->method('extractConfiguration') + ->with($this->childForm) + ->will($this->returnValue(array('config' => 'bar'))); + + $this->dataExtractor->expects($this->at(2)) + ->method('extractDefaultData') + ->with($this->form) + ->will($this->returnValue(array('default_data' => 'foo'))); + $this->dataExtractor->expects($this->at(3)) + ->method('extractDefaultData') + ->with($this->childForm) + ->will($this->returnValue(array('default_data' => 'bar'))); + + $this->dataExtractor->expects($this->at(4)) + ->method('extractSubmittedData') + ->with($this->form) + ->will($this->returnValue(array('submitted_data' => 'foo'))); + $this->dataExtractor->expects($this->at(5)) + ->method('extractSubmittedData') + ->with($this->childForm) + ->will($this->returnValue(array('submitted_data' => 'bar'))); + + $this->dataExtractor->expects($this->at(6)) + ->method('extractViewVariables') + ->with($this->view) + ->will($this->returnValue(array('view_vars' => 'foo'))); + + $this->dataExtractor->expects($this->at(7)) + ->method('extractViewVariables') + ->with($this->childView) + ->will($this->returnValue(array('view_vars' => 'bar'))); + + $this->dataCollector->collectConfiguration($this->form); + $this->dataCollector->collectDefaultData($this->form); + $this->dataCollector->collectSubmittedData($this->form); + $this->dataCollector->collectViewVariables($this->view); + $this->dataCollector->buildFinalFormTree($this->form, $this->view); + + $this->assertSame(array( + 'forms' => array( + 'name' => array( + 'view_vars' => 'foo', + 'config' => 'foo', + 'default_data' => 'foo', + 'submitted_data' => 'foo', + 'children' => array( + 'child' => array( + 'view_vars' => 'bar', + 'config' => 'bar', + 'default_data' => 'bar', + 'submitted_data' => 'bar', + 'children' => array(), + ), + ), + ), + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + public function testFinalFormReliesOnFormViewStructure() + { + $this->form->add($this->createForm('first')); + $this->form->add($this->createForm('second')); + + $this->view->children['second'] = $this->childView; + + $this->dataCollector->buildPreliminaryFormTree($this->form); + + $this->assertSame(array( + 'forms' => array( + 'name' => array( + 'children' => array( + 'first' => array( + 'children' => array(), + ), + 'second' => array( + 'children' => array(), + ), + ), + ), + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + + $this->dataCollector->buildFinalFormTree($this->form, $this->view); + + $this->assertSame(array( + 'forms' => array( + 'name' => array( + 'children' => array( + // "first" not present in FormView + 'second' => array( + 'children' => array(), + ), + ), + ), + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + public function testChildViewsCanBeWithoutCorrespondingChildForms() + { + // don't add $this->childForm to $this->form! + + $this->view->children['child'] = $this->childView; + + $this->dataExtractor->expects($this->at(0)) + ->method('extractConfiguration') + ->with($this->form) + ->will($this->returnValue(array('config' => 'foo'))); + $this->dataExtractor->expects($this->at(1)) + ->method('extractConfiguration') + ->with($this->childForm) + ->will($this->returnValue(array('config' => 'bar'))); + + // explicitly call collectConfiguration(), since $this->childForm is not + // contained in the form tree + $this->dataCollector->collectConfiguration($this->form); + $this->dataCollector->collectConfiguration($this->childForm); + $this->dataCollector->buildFinalFormTree($this->form, $this->view); + + $this->assertSame(array( + 'forms' => array( + 'name' => array( + 'config' => 'foo', + 'children' => array( + 'child' => array( + // no "config" key + 'children' => array(), + ), + ), + ), + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + public function testChildViewsWithoutCorrespondingChildFormsMayBeExplicitlyAssociated() + { + // don't add $this->childForm to $this->form! + + $this->view->children['child'] = $this->childView; + + // but associate the two + $this->dataCollector->associateFormWithView($this->childForm, $this->childView); + + $this->dataExtractor->expects($this->at(0)) + ->method('extractConfiguration') + ->with($this->form) + ->will($this->returnValue(array('config' => 'foo'))); + $this->dataExtractor->expects($this->at(1)) + ->method('extractConfiguration') + ->with($this->childForm) + ->will($this->returnValue(array('config' => 'bar'))); + + // explicitly call collectConfiguration(), since $this->childForm is not + // contained in the form tree + $this->dataCollector->collectConfiguration($this->form); + $this->dataCollector->collectConfiguration($this->childForm); + $this->dataCollector->buildFinalFormTree($this->form, $this->view); + + $this->assertSame(array( + 'forms' => array( + 'name' => array( + 'config' => 'foo', + 'children' => array( + 'child' => array( + 'config' => 'bar', + 'children' => array(), + ), + ), + ), + ), + 'nb_errors' => 0, + ), $this->dataCollector->getData()); + } + + public function testCollectSubmittedDataCountsErrors() + { + $form1 = $this->createForm('form1'); + $childForm1 = $this->createForm('child1'); + $form2 = $this->createForm('form2'); + + $form1->add($childForm1); + + $this->dataExtractor->expects($this->at(0)) + ->method('extractSubmittedData') + ->with($form1) + ->will($this->returnValue(array('errors' => array('foo')))); + $this->dataExtractor->expects($this->at(1)) + ->method('extractSubmittedData') + ->with($childForm1) + ->will($this->returnValue(array('errors' => array('bar', 'bam')))); + $this->dataExtractor->expects($this->at(2)) + ->method('extractSubmittedData') + ->with($form2) + ->will($this->returnValue(array('errors' => array('baz')))); + + $this->dataCollector->collectSubmittedData($form1); + + $data = $this->dataCollector->getData(); + $this->assertSame(3, $data['nb_errors']); + + $this->dataCollector->collectSubmittedData($form2); + + $data = $this->dataCollector->getData(); + $this->assertSame(4, $data['nb_errors']); + + } + + private function createForm($name) + { + $builder = new FormBuilder($name, null, $this->dispatcher, $this->factory); + $builder->setCompound(true); + $builder->setDataMapper($this->dataMapper); + + return $builder->getForm(); + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataExtractorTest.php b/src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataExtractorTest.php new file mode 100644 index 0000000000..d9c1bb3180 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/DataCollector/FormDataExtractorTest.php @@ -0,0 +1,338 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Extension\DataCollector; + +use Symfony\Component\Form\CallbackTransformer; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Extension\DataCollector\FormDataExtractor; +use Symfony\Component\Form\FormBuilder; +use Symfony\Component\Form\FormError; +use Symfony\Component\Form\FormView; +use Symfony\Component\Form\Tests\Fixtures\FixedDataTransformer; +use Symfony\Component\HttpKernel\DataCollector\Util\ValueExporter; + +class FormDataExtractorTest_SimpleValueExporter extends ValueExporter +{ + /** + * {@inheritdoc} + */ + public function exportValue($value) + { + return var_export($value, true); + } +} + +/** + * @author Bernhard Schussek + */ +class FormDataExtractorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var FormDataExtractorTest_SimpleValueExporter + */ + private $valueExporter; + + /** + * @var FormDataExtractor + */ + private $dataExtractor; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $dispatcher; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $factory; + + protected function setUp() + { + $this->valueExporter = new FormDataExtractorTest_SimpleValueExporter(); + $this->dataExtractor = new FormDataExtractor($this->valueExporter); + $this->dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); + $this->factory = $this->getMock('Symfony\Component\Form\FormFactoryInterface'); + } + + public function testExtractConfiguration() + { + $type = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface'); + $type->expects($this->any()) + ->method('getName') + ->will($this->returnValue('type_name')); + $type->expects($this->any()) + ->method('getInnerType') + ->will($this->returnValue(new \stdClass())); + + $form = $this->createBuilder('name') + ->setType($type) + ->getForm(); + + $this->assertSame(array( + 'type' => 'type_name', + 'type_class' => 'stdClass', + 'synchronized' => 'true', + 'passed_options' => array(), + 'resolved_options' => array(), + ), $this->dataExtractor->extractConfiguration($form)); + } + + public function testExtractConfigurationSortsPassedOptions() + { + $type = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface'); + $type->expects($this->any()) + ->method('getName') + ->will($this->returnValue('type_name')); + $type->expects($this->any()) + ->method('getInnerType') + ->will($this->returnValue(new \stdClass())); + + $options = array( + 'b' => 'foo', + 'a' => 'bar', + 'c' => 'baz', + ); + + $form = $this->createBuilder('name') + ->setType($type) + // passed options are stored in an attribute by + // ResolvedTypeDataCollectorProxy + ->setAttribute('data_collector/passed_options', $options) + ->getForm(); + + $this->assertSame(array( + 'type' => 'type_name', + 'type_class' => 'stdClass', + 'synchronized' => 'true', + 'passed_options' => array( + 'a' => "'bar'", + 'b' => "'foo'", + 'c' => "'baz'", + ), + 'resolved_options' => array(), + ), $this->dataExtractor->extractConfiguration($form)); + } + + public function testExtractConfigurationSortsResolvedOptions() + { + $type = $this->getMock('Symfony\Component\Form\ResolvedFormTypeInterface'); + $type->expects($this->any()) + ->method('getName') + ->will($this->returnValue('type_name')); + $type->expects($this->any()) + ->method('getInnerType') + ->will($this->returnValue(new \stdClass())); + + $options = array( + 'b' => 'foo', + 'a' => 'bar', + 'c' => 'baz', + ); + + $form = $this->createBuilder('name', $options) + ->setType($type) + ->getForm(); + + $this->assertSame(array( + 'type' => 'type_name', + 'type_class' => 'stdClass', + 'synchronized' => 'true', + 'passed_options' => array(), + 'resolved_options' => array( + 'a' => "'bar'", + 'b' => "'foo'", + 'c' => "'baz'", + ), + ), $this->dataExtractor->extractConfiguration($form)); + } + + public function testExtractDefaultData() + { + $form = $this->createBuilder('name')->getForm(); + + $form->setData('Foobar'); + + $this->assertSame(array( + 'default_data' => array( + 'norm' => "'Foobar'", + ), + 'submitted_data' => array(), + ), $this->dataExtractor->extractDefaultData($form)); + } + + public function testExtractDefaultDataStoresModelDataIfDifferent() + { + $form = $this->createBuilder('name') + ->addModelTransformer(new FixedDataTransformer(array( + 'Foo' => 'Bar' + ))) + ->getForm(); + + $form->setData('Foo'); + + $this->assertSame(array( + 'default_data' => array( + 'norm' => "'Bar'", + 'model' => "'Foo'", + ), + 'submitted_data' => array(), + ), $this->dataExtractor->extractDefaultData($form)); + } + + public function testExtractDefaultDataStoresViewDataIfDifferent() + { + $form = $this->createBuilder('name') + ->addViewTransformer(new FixedDataTransformer(array( + 'Foo' => 'Bar' + ))) + ->getForm(); + + $form->setData('Foo'); + + $this->assertSame(array( + 'default_data' => array( + 'norm' => "'Foo'", + 'view' => "'Bar'", + ), + 'submitted_data' => array(), + ), $this->dataExtractor->extractDefaultData($form)); + } + + public function testExtractSubmittedData() + { + $form = $this->createBuilder('name')->getForm(); + + $form->submit('Foobar'); + + $this->assertSame(array( + 'submitted_data' => array( + 'norm' => "'Foobar'", + ), + 'errors' => array(), + 'synchronized' => 'true', + ), $this->dataExtractor->extractSubmittedData($form)); + } + + public function testExtractSubmittedDataStoresModelDataIfDifferent() + { + $form = $this->createBuilder('name') + ->addModelTransformer(new FixedDataTransformer(array( + 'Foo' => 'Bar', + '' => '', + ))) + ->getForm(); + + $form->submit('Bar'); + + $this->assertSame(array( + 'submitted_data' => array( + 'norm' => "'Bar'", + 'model' => "'Foo'", + ), + 'errors' => array(), + 'synchronized' => 'true', + ), $this->dataExtractor->extractSubmittedData($form)); + } + + public function testExtractSubmittedDataStoresViewDataIfDifferent() + { + $form = $this->createBuilder('name') + ->addViewTransformer(new FixedDataTransformer(array( + 'Foo' => 'Bar', + '' => '', + ))) + ->getForm(); + + $form->submit('Bar'); + + $this->assertSame(array( + 'submitted_data' => array( + 'norm' => "'Foo'", + 'view' => "'Bar'", + ), + 'errors' => array(), + 'synchronized' => 'true', + ), $this->dataExtractor->extractSubmittedData($form)); + } + + public function testExtractSubmittedDataStoresErrors() + { + $form = $this->createBuilder('name')->getForm(); + + $form->submit('Foobar'); + $form->addError(new FormError('Invalid!')); + + $this->assertSame(array( + 'submitted_data' => array( + 'norm' => "'Foobar'", + ), + 'errors' => array( + array('message' => 'Invalid!'), + ), + 'synchronized' => 'true', + ), $this->dataExtractor->extractSubmittedData($form)); + } + + public function testExtractSubmittedDataRemembersIfNonSynchronized() + { + $form = $this->createBuilder('name') + ->addModelTransformer(new CallbackTransformer( + function () {}, + function () { + throw new TransformationFailedException('Fail!'); + } + )) + ->getForm(); + + $form->submit('Foobar'); + + $this->assertSame(array( + 'submitted_data' => array( + 'norm' => "'Foobar'", + 'model' => 'NULL', + ), + 'errors' => array(), + 'synchronized' => 'false', + ), $this->dataExtractor->extractSubmittedData($form)); + } + + public function testExtractViewVariables() + { + $view = new FormView(); + + $view->vars = array( + 'b' => 'foo', + 'a' => 'bar', + 'c' => 'baz', + ); + + $this->assertSame(array( + 'view_vars' => array( + 'a' => "'bar'", + 'b' => "'foo'", + 'c' => "'baz'", + ), + ), $this->dataExtractor->extractViewVariables($view)); + } + + /** + * @param string $name + * @param array $options + * + * @return FormBuilder + */ + private function createBuilder($name, array $options = array()) + { + return new FormBuilder($name, null, $this->dispatcher, $this->factory, $options); + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/DataCollector/Type/DataCollectorTypeExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/DataCollector/Type/DataCollectorTypeExtensionTest.php index 4039bf6ee6..cb77456984 100644 --- a/src/Symfony/Component/Form/Tests/Extension/DataCollector/Type/DataCollectorTypeExtensionTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/DataCollector/Type/DataCollectorTypeExtensionTest.php @@ -10,7 +10,6 @@ namespace Symfony\Component\Form\Tests\Extension\DataCollector\Type; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Form\Extension\DataCollector\Type\DataCollectorTypeExtension; class DataCollectorTypeExtensionTest extends \PHPUnit_Framework_TestCase @@ -21,14 +20,14 @@ class DataCollectorTypeExtensionTest extends \PHPUnit_Framework_TestCase private $extension; /** - * @var EventSubscriberInterface + * @var \PHPUnit_Framework_MockObject_MockObject */ - private $eventSubscriber; + private $dataCollector; public function setUp() { - $this->eventSubscriber = $this->getMock('Symfony\Component\EventDispatcher\EventSubscriberInterface'); - $this->extension = new DataCollectorTypeExtension($this->eventSubscriber); + $this->dataCollector = $this->getMock('Symfony\Component\Form\Extension\DataCollector\FormDataCollectorInterface'); + $this->extension = new DataCollectorTypeExtension($this->dataCollector); } public function testGetExtendedType() @@ -39,7 +38,9 @@ class DataCollectorTypeExtensionTest extends \PHPUnit_Framework_TestCase public function testBuildForm() { $builder = $this->getMock('Symfony\Component\Form\Test\FormBuilderInterface'); - $builder->expects($this->atLeastOnce())->method('addEventSubscriber')->with($this->eventSubscriber); + $builder->expects($this->atLeastOnce()) + ->method('addEventSubscriber') + ->with($this->isInstanceOf('Symfony\Component\Form\Extension\DataCollector\EventListener\DataCollectorListener')); $this->extension->buildForm($builder, array()); }