浏览代码

[Form] Added FieldFactory mechanism to automatically create fields by introspecting metadata of a class

Bernhard Schussek 14 年之前
父节点
当前提交
4b78c4376f

+ 0 - 44
src/Symfony/Component/Form/Configurator/ValidatorConfigurator.php

@@ -1,44 +0,0 @@
-<?php
-
-namespace Symfony\Component\Form\Configurator;
-
-/*
- * This file is part of the Symfony framework.
- *
- * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
- *
- * This source file is subject to the MIT license that is bundled
- * with this source code in the file LICENSE.
- */
-
-class ValidatorConfigurator implements ConfiguratorInterface
-{
-    protected $metaData = null;
-    protected $classMetaData = null;
-
-    public function __construct(MetaDataInterface $metaData)
-    {
-        $this->metaData = $metaData;
-    }
-
-    public function initialize($object)
-    {
-        $this->classMetaData = $this->metaData->getClassMetaData(get_class($object));
-    }
-
-    public function getClass($fieldName)
-    {
-
-    }
-
-    public function getOptions($fieldName)
-    {
-
-    }
-
-    public function isRequired($fieldName)
-    {
-        return $this->classMetaData->getPropertyMetaData($fieldName)->hasConstraint('NotNull')
-                || $this->classMetaData->getPropertyMetaData($fieldName)->hasConstraint('NotEmpty');
-    }
-}

+ 1 - 1
src/Symfony/Component/Form/DateTimeField.php

@@ -57,7 +57,7 @@ class DateTimeField extends FieldGroup
         $this->addOption('seconds', range(0, 59));
         $this->addOption('data_timezone', 'UTC');
         $this->addOption('user_timezone', 'UTC');
-        $this->addOption('date_widget', DateField::INPUT, self::$dateWidgets);
+        $this->addOption('date_widget', DateField::CHOICE, self::$dateWidgets);
         $this->addOption('time_widget', TimeField::CHOICE, self::$timeWidgets);
         $this->addOption('type', self::DATETIME, self::$types);
         $this->addOption('with_seconds', false);

+ 8 - 10
src/Symfony/Component/Form/Configurator/ConfiguratorInterface.php

@@ -1,6 +1,6 @@
 <?php
 
-namespace Symfony\Component\Form\Configurator;
+namespace Symfony\Component\Form\Exception;
 
 /*
  * This file is part of the Symfony framework.
@@ -11,13 +11,11 @@ namespace Symfony\Component\Form\Configurator;
  * with this source code in the file LICENSE.
  */
 
-interface ConfiguratorInterface
+/**
+ * Thrown when a field is expected to be added to a form but is not
+ *
+ * @author Bernhard Schussek <bernhard.schussek@symfony-project.com>
+ */
+class DanglingFieldException extends FormException
 {
-    function initialize($object);
-
-    function getClass($fieldName);
-
-    function getOptions($fieldName);
-
-    function isRequired($fieldName);
-}
+}

+ 1 - 1
src/Symfony/Component/Form/Exception/UnexpectedTypeException.php

@@ -15,6 +15,6 @@ class UnexpectedTypeException extends FormException
 {
     public function __construct($value, $expectedType)
     {
-        parent::__construct(sprintf('Expected argument of type %s, %s given', $expectedType, gettype($value)));
+        parent::__construct(sprintf('Expected argument of type %s, %s given', $expectedType, is_object($value) ? get_class($value) : gettype($value)));
     }
 }

+ 10 - 0
src/Symfony/Component/Form/Field.php

@@ -237,6 +237,16 @@ class Field extends Configurable implements FieldInterface
         return $this->parent;
     }
 
+    /**
+     * Returns the root of the form tree
+     *
+     * @return FieldInterface  The root of the tree
+     */
+    public function getRoot()
+    {
+        return $this->parent ? $this->parent->getRoot() : $this;
+    }
+
     /**
      * Updates the field with default data
      *

+ 107 - 0
src/Symfony/Component/Form/FieldFactory/FieldFactory.php

@@ -0,0 +1,107 @@
+<?php
+
+namespace Symfony\Component\Form\FieldFactory;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+use Symfony\Component\Form\Exception\UnexpectedTypeException;
+
+/**
+ * Default implementation of FieldFactoryInterface
+ *
+ * @author Bernhard Schussek <bernhard.schussek@symfony-project.com>
+ * @see FieldFactoryInterface
+ */
+class FieldFactory implements FieldFactoryInterface
+{
+    /**
+     * A list of guessers for guessing field classes and options
+     * @var array
+     */
+    protected $guessers;
+
+    /**
+     * Constructor
+     *
+     * @param array $guessers  A list of instances implementing
+     *                         FieldFactoryGuesserInterface
+     */
+    public function __construct(array $guessers)
+    {
+        foreach ($guessers as $guesser) {
+            if (!$guesser instanceof FieldFactoryGuesserInterface) {
+                throw new UnexpectedTypeException($guesser, 'FieldFactoryGuesserInterface');
+            }
+        }
+        $this->guessers = $guessers;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function getInstance($object, $property, array $options = array())
+    {
+        $guess = $this->guess(function ($guesser) use ($object, $property) {
+            return $guesser->guessMaxLength($object, $property);
+        });
+
+        $maxLength = $guess ? $guess->getValue() : null;
+
+        $guess = $this->guess(function ($guesser) use ($object, $property) {
+            return $guesser->guessClass($object, $property);
+        });
+
+        if (!$guess) {
+            throw new \RuntimeException(sprintf('No field could be guessed for property "%s" of class %s', $property, get_class($object)));
+        }
+
+        $class = $guess->getClass();
+        $textField = 'Symfony\Component\Form\TextField';
+
+        if (null !== $maxLength && ($class == $textField || is_subclass_of($class, $textField))) {
+            $options = array_merge(array('max_length' => $maxLength), $options);
+        }
+
+        $options = array_merge($guess->getOptions(), $options);
+        $field = new $class($property, $options);
+
+        $guess = $this->guess(function ($guesser) use ($object, $property) {
+            return $guesser->guessRequired($object, $property);
+        });
+
+        if ($guess) {
+            $field->setRequired($guess->getValue());
+        }
+
+        return $field;
+    }
+
+    /**
+     * Executes a closure for each guesser and returns the best guess from the
+     * return values
+     *
+     * @param  \Closure $closure  The closure to execute. Accepts a guesser as
+     *                            argument and should return a FieldFactoryGuess
+     *                            instance
+     * @return FieldFactoryGuess  The guess with the highest confidence
+     */
+    protected function guess(\Closure $closure)
+    {
+        $guesses = array();
+
+        foreach ($this->guessers as $guesser) {
+            if ($guess = $closure($guesser)) {
+                $guesses[] = $guess;
+            }
+        }
+
+        return FieldFactoryGuess::getBestGuess($guesses);
+    }
+}

+ 63 - 0
src/Symfony/Component/Form/FieldFactory/FieldFactoryClassGuess.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace Symfony\Component\Form\FieldFactory;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+/**
+ * Contains a guessed class name and a list of options for creating an instance
+ * of that class
+ *
+ * @author Bernhard Schussek <bernhard.schussek@symfony-project.com>
+ */
+class FieldFactoryClassGuess extends FieldFactoryGuess
+{
+    /**
+     * The guessed options for creating an instance of the guessed class
+     * @var array
+     */
+    protected $options;
+
+    /**
+     * Constructor
+     *
+     * @param string $class         The guessed class name
+     * @param array  $options       The options for creating instances of the
+     *                              guessed class
+     * @param integer $confidence   The confidence that the guessed class name
+     *                              is correct
+     */
+    public function __construct($class, array $options, $confidence)
+    {
+        parent::__construct($class, $confidence);
+
+        $this->options = $options;
+    }
+
+    /**
+     * Returns the guessed class name
+     *
+     * @return string
+     */
+    public function getClass()
+    {
+        return $this->getValue();
+    }
+
+    /**
+     * Returns the guessed options for creating instances of the guessed class
+     *
+     * @return array
+     */
+    public function getOptions()
+    {
+        return $this->options;
+    }
+}

+ 123 - 0
src/Symfony/Component/Form/FieldFactory/FieldFactoryGuess.php

@@ -0,0 +1,123 @@
+<?php
+
+namespace Symfony\Component\Form\FieldFactory;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+/**
+ * Contains a value guessed by a FieldFactoryGuesserInterface instance
+ *
+ * Each instance also contains a confidence value about the correctness of
+ * the guessed value. Thus an instance with confidence HIGH_CONFIDENCE is
+ * more likely to contain a correct value than an instance with confidence
+ * LOW_CONFIDENCE.
+ *
+ * @author Bernhard Schussek <bernhard.schussek@symfony-project.com>
+ */
+class FieldFactoryGuess
+{
+    /**
+     * Marks an instance with a value that is very likely to be correct
+     * @var integer
+     */
+    const HIGH_CONFIDENCE = 2;
+
+    /**
+     * Marks an instance with a value that is likely to be correct
+     * @var integer
+     */
+    const MEDIUM_CONFIDENCE = 1;
+
+    /**
+     * Marks an instance with a value that may be correct
+     * @var integer
+     */
+    const LOW_CONFIDENCE = 0;
+
+    /**
+     * The list of allowed confidence values
+     * @var array
+     */
+    protected static $confidences = array(
+        self::HIGH_CONFIDENCE,
+        self::MEDIUM_CONFIDENCE,
+        self::LOW_CONFIDENCE,
+    );
+
+    /**
+     * The guessed value
+     * @var mixed
+     */
+    protected $value;
+
+    /**
+     * The confidence about the correctness of the value
+     *
+     * One of HIGH_CONFIDENCE, MEDIUM_CONFIDENCE and LOW_CONFIDENCE.
+     *
+     * @var integer
+     */
+    protected $confidence;
+
+    /**
+     * Returns the guess most likely to be correct from a list of guesses
+     *
+     * If there are multiple guesses with the same, highest confidence, the
+     * returned guess is any of them.
+     *
+     * @param  array $guesses     A list of guesses
+     * @return FieldFactoryGuess  The guess with the highest confidence
+     */
+    static public function getBestGuess(array $guesses)
+    {
+        usort($guesses, function ($a, $b) {
+            return $b->getConfidence() - $a->getConfidence();
+        });
+
+        return count($guesses) > 0 ? $guesses[0] : null;
+    }
+
+    /**
+     * Constructor
+     *
+     * @param mixed $value          The guessed value
+     * @param integer $confidence   The confidence
+     */
+    public function __construct($value, $confidence)
+    {
+        if (!in_array($confidence, self::$confidences)) {
+            throw new \UnexpectedValueException(sprintf('The confidence should be one of "%s"', implode('", "', self::$confidences)));
+        }
+
+        $this->value = $value;
+        $this->confidence = $confidence;
+    }
+
+    /**
+     * Returns the guessed value
+     *
+     * @return mixed
+     */
+    public function getValue()
+    {
+        return $this->value;
+    }
+
+    /**
+     * Returns the confidence that the guessed value is correct
+     *
+     * @return integer  One of the constants HIGH_CONFIDENCE, MEDIUM_CONFIDENCE
+     *                  and LOW_CONFIDENCE
+     */
+    public function getConfidence()
+    {
+        return $this->confidence;
+    }
+}

+ 47 - 0
src/Symfony/Component/Form/FieldFactory/FieldFactoryGuesserInterface.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace Symfony\Component\Form\FieldFactory;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+/**
+ * Guesses field classes and options for the properties of an object
+ *
+ * @author Bernhard Schussek <bernhard.schussek@symfony-project.com>
+ */
+interface FieldFactoryGuesserInterface
+{
+    /**
+     * Returns a field guess for a given property name
+     *
+     * @param  object $object          The object to guess for
+     * @param  string $property        The name of the property to guess for
+     * @return FieldFactoryClassGuess  A guess for the field's class and options
+     */
+    function guessClass($object, $property);
+
+    /**
+     * Returns a guess whether the given property is required
+     *
+     * @param  object $object     The object to guess for
+     * @param  string $property   The name of the property to guess for
+     * @return FieldFactoryGuess  A guess for the field's required setting
+     */
+    function guessRequired($object, $property);
+
+    /**
+     * Returns a guess about the field's maximum length
+     *
+     * @param  object $object     The object to guess for
+     * @param  string $property   The name of the property to guess for
+     * @return FieldFactoryGuess  A guess for the field's maximum length
+     */
+    function guessMaxLength($object, $property);
+}

+ 30 - 0
src/Symfony/Component/Form/FieldFactory/FieldFactoryInterface.php

@@ -0,0 +1,30 @@
+<?php
+
+namespace Symfony\Component\Form\FieldFactory;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+/**
+ * Automatically creates form fields for properties of an object
+ *
+ * @author Bernhard Schussek <bernhard.schussek@symfony-project.com>
+ */
+interface FieldFactoryInterface
+{
+    /**
+     * Returns a field for a given property name
+     *
+     * @param  object $object     The object to create a field for
+     * @param  string $property   The name of the property
+     * @param  array $options     Custom options for creating the field
+     * @return FieldInterface     A field instance
+     */
+    function getInstance($object, $property, array $options = array());
+}

+ 301 - 0
src/Symfony/Component/Form/FieldFactory/ValidatorFieldFactoryGuesser.php

@@ -0,0 +1,301 @@
+<?php
+
+namespace Symfony\Component\Form\FieldFactory;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\Mapping\ClassMetadataFactoryInterface;
+
+/**
+ * Guesses form fields from the metadata of the a Validator class
+ *
+ * @author Bernhard Schussek <bernhard.schussek@symfony-project.com>
+ */
+class ValidatorFieldFactoryGuesser implements FieldFactoryGuesserInterface
+{
+    /**
+     * Constructor
+     *
+     * @param ClassMetadataFactoryInterface $metadataFactory
+     */
+    public function __construct(ClassMetadataFactoryInterface $metadataFactory)
+    {
+        $this->metadataFactory = $metadataFactory;
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function guessClass($object, $property)
+    {
+        $guesser = $this;
+
+        return $this->guess($object, $property, function (Constraint $constraint) use ($guesser) {
+            return $guesser->guessClassForConstraint($constraint);
+        });
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function guessRequired($object, $property)
+    {
+        $guesser = $this;
+
+        return $this->guess($object, $property, function (Constraint $constraint) use ($guesser) {
+            return $guesser->guessRequiredForConstraint($constraint);
+        });
+    }
+
+    /**
+     * @inheritDoc
+     */
+    public function guessMaxLength($object, $property)
+    {
+        $guesser = $this;
+
+        return $this->guess($object, $property, function (Constraint $constraint) use ($guesser) {
+            return $guesser->guessMaxLengthForConstraint($constraint);
+        });
+    }
+
+    /**
+     * Iterates over the constraints of a property, executes a constraints on
+     * them and returns the best guess
+     *
+     * @param object $object      The object to read the constraints from
+     * @param string $property    The property for which to find constraints
+     * @param \Closure $guessForConstraint   The closure that returns a guess
+     *                            for a given constraint
+     * @return FieldFactoryGuess  The guessed value with the highest confidence
+     */
+    protected function guess($object, $property, \Closure $guessForConstraint)
+    {
+        $guesses = array();
+        $classMetadata = $this->metadataFactory->getClassMetadata(get_class($object));
+        $memberMetadatas = $classMetadata->getMemberMetadatas($property);
+
+        foreach ($memberMetadatas as $memberMetadata) {
+            $constraints = $memberMetadata->getConstraints();
+
+            foreach ($constraints as $constraint) {
+                if ($guess = $guessForConstraint($constraint)) {
+                    $guesses[] = $guess;
+                }
+            }
+        }
+
+        return FieldFactoryGuess::getBestGuess($guesses);
+    }
+
+    /**
+     * Guesses a field class name for a given constraint
+     *
+     * @param  Constraint $constraint  The constraint to guess for
+     * @return FieldFactoryClassGuess  The guessed field class and options
+     */
+    public function guessClassForConstraint(Constraint $constraint)
+    {
+        switch (get_class($constraint)) {
+            case 'Symfony\Component\Validator\Constraints\AssertType':
+                switch ($constraint->type) {
+                    case 'boolean':
+                    case 'bool':
+                        return new FieldFactoryClassGuess(
+                        	'Symfony\Component\Form\CheckboxField',
+                            array(),
+                            FieldFactoryGuess::MEDIUM_CONFIDENCE
+                        );
+                    case 'double':
+                    case 'float':
+                    case 'numeric':
+                    case 'real':
+                        return new FieldFactoryClassGuess(
+                        	'Symfony\Component\Form\NumberField',
+                            array(),
+                            FieldFactoryGuess::MEDIUM_CONFIDENCE
+                        );
+                    case 'integer':
+                    case 'int':
+                    case 'long':
+                        return new FieldFactoryClassGuess(
+                        	'Symfony\Component\Form\IntegerField',
+                            array(),
+                            FieldFactoryGuess::MEDIUM_CONFIDENCE
+                        );
+                    case 'string':
+                        return new FieldFactoryClassGuess(
+                        	'Symfony\Component\Form\TextField',
+                            array(),
+                            FieldFactoryGuess::LOW_CONFIDENCE
+                        );
+                }
+                break;
+            case 'Symfony\Component\Validator\Constraints\Choice':
+                return new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\ChoiceField',
+                    array('choices' => $constraint->choices),
+                    FieldFactoryGuess::HIGH_CONFIDENCE
+                );
+            case 'Symfony\Component\Validator\Constraints\Country':
+                return new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\CountryField',
+                    array(),
+                    FieldFactoryGuess::HIGH_CONFIDENCE
+                );
+            case 'Symfony\Component\Validator\Constraints\Date':
+                return new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\DateField',
+                    array('type' => 'string'),
+                    FieldFactoryGuess::HIGH_CONFIDENCE
+                );
+            case 'Symfony\Component\Validator\Constraints\DateTime':
+                return new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\DateTimeField',
+                    array('type' => 'string'),
+                    FieldFactoryGuess::HIGH_CONFIDENCE
+                );
+            case 'Symfony\Component\Validator\Constraints\Email':
+                return new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\TextField',
+                    array(),
+                    FieldFactoryGuess::HIGH_CONFIDENCE
+                );
+            case 'Symfony\Component\Validator\Constraints\File':
+                return new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\FileField',
+                    array(),
+                    FieldFactoryGuess::HIGH_CONFIDENCE
+                );
+            case 'Symfony\Component\Validator\Constraints\Image':
+                return new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\FileField',
+                    array(),
+                    FieldFactoryGuess::HIGH_CONFIDENCE
+                );
+            case 'Symfony\Component\Validator\Constraints\Ip':
+                return new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\TextField',
+                    array(),
+                    FieldFactoryGuess::MEDIUM_CONFIDENCE
+                );
+            case 'Symfony\Component\Validator\Constraints\Language':
+                return new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\LanguageField',
+                    array(),
+                    FieldFactoryGuess::HIGH_CONFIDENCE
+                );
+            case 'Symfony\Component\Validator\Constraints\Locale':
+                return new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\LocaleField',
+                    array(),
+                    FieldFactoryGuess::HIGH_CONFIDENCE
+                );
+            case 'Symfony\Component\Validator\Constraints\Max':
+                return new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\NumberField',
+                    array(),
+                    FieldFactoryGuess::LOW_CONFIDENCE
+                );
+            case 'Symfony\Component\Validator\Constraints\MaxLength':
+                return new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\TextField',
+                    array(),
+                    FieldFactoryGuess::LOW_CONFIDENCE
+                );
+            case 'Symfony\Component\Validator\Constraints\Min':
+                return new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\NumberField',
+                    array(),
+                    FieldFactoryGuess::LOW_CONFIDENCE
+                );
+            case 'Symfony\Component\Validator\Constraints\MinLength':
+                return new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\TextField',
+                    array(),
+                    FieldFactoryGuess::LOW_CONFIDENCE
+                );
+            case 'Symfony\Component\Validator\Constraints\Regex':
+                return new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\TextField',
+                    array(),
+                    FieldFactoryGuess::LOW_CONFIDENCE
+                );
+            case 'Symfony\Component\Validator\Constraints\Time':
+                return new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\TimeField',
+                    array('type' => 'string'),
+                    FieldFactoryGuess::HIGH_CONFIDENCE
+                );
+            case 'Symfony\Component\Validator\Constraints\Url':
+                return new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\UrlField',
+                    array(),
+                    FieldFactoryGuess::HIGH_CONFIDENCE
+                );
+            default:
+                return new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\TextField',
+                    array(),
+                    FieldFactoryGuess::LOW_CONFIDENCE
+                );
+        }
+    }
+
+    /**
+     * Guesses whether a field is required based on the given constraint
+     *
+     * @param  Constraint $constraint  The constraint to guess for
+     * @return FieldFactoryGuess       The guess whether the field is required
+     */
+    public function guessRequiredForConstraint(Constraint $constraint)
+    {
+        switch (get_class($constraint)) {
+            case 'Symfony\Component\Validator\Constraints\NotNull':
+                return new FieldFactoryGuess(
+                	true,
+                    FieldFactoryGuess::HIGH_CONFIDENCE
+                );
+            case 'Symfony\Component\Validator\Constraints\NotBlank':
+                return new FieldFactoryGuess(
+                	true,
+                    FieldFactoryGuess::HIGH_CONFIDENCE
+                );
+            default:
+                return new FieldFactoryGuess(
+                	false,
+                    FieldFactoryGuess::LOW_CONFIDENCE
+                );
+        }
+    }
+
+    /**
+     * Guesses a field's maximum length based on the given constraint
+     *
+     * @param  Constraint $constraint  The constraint to guess for
+     * @return FieldFactoryGuess       The guess for the maximum length
+     */
+    public function guessMaxLengthForConstraint(Constraint $constraint)
+    {
+        switch (get_class($constraint)) {
+            case 'Symfony\Component\Validator\Constraints\MaxLength':
+                return new FieldFactoryGuess(
+                	$constraint->limit,
+                    FieldFactoryGuess::HIGH_CONFIDENCE
+                );
+            case 'Symfony\Component\Validator\Constraints\Max':
+                return new FieldFactoryGuess(
+                	strlen((string)$constraint->limit),
+                    FieldFactoryGuess::HIGH_CONFIDENCE
+                );
+        }
+    }
+}

+ 24 - 2
src/Symfony/Component/Form/FieldGroup.php

@@ -13,6 +13,7 @@ namespace Symfony\Component\Form;
 
 use Symfony\Component\Form\Exception\AlreadyBoundException;
 use Symfony\Component\Form\Exception\UnexpectedTypeException;
+use Symfony\Component\Form\Exception\DanglingFieldException;
 
 /**
  * FieldGroup represents an array of widgets bind to names and values.
@@ -90,14 +91,35 @@ class FieldGroup extends Field implements \IteratorAggregate, FieldGroupInterfac
      * $form->add($locationGroup);
      * </code>
      *
-     * @param FieldInterface $field
+     * @param FieldInterface|string $field
      */
-    public function add(FieldInterface $field)
+    public function add($field)
     {
         if ($this->isBound()) {
             throw new AlreadyBoundException('You cannot add fields after binding a form');
         }
 
+        // if the field is given as string, ask the field factory of the form
+        // to create a field
+        if (!$field instanceof FieldInterface) {
+            if (!is_string($field)) {
+                throw new UnexpectedTypeException($field, 'FieldInterface or string');
+            }
+
+            if (!$this->getRoot() instanceof Form) {
+                throw new DanglingFieldException('Field groups must be added to a form before fields can be created automatically');
+            }
+
+            $factory = $this->getRoot()->getFieldFactory();
+
+            if (!$factory) {
+                throw new \LogicException('A field factory must be available for automatically creating fields');
+            }
+
+            $options = func_num_args() > 1 ? func_get_arg(1) : array();
+            $field = $factory->getInstance($this->getData(), $field, $options);
+        }
+
         $this->fields[$field->getKey()] = $field;
 
         $field->setParent($this);

+ 13 - 0
src/Symfony/Component/Form/Form.php

@@ -57,6 +57,8 @@ class Form extends FieldGroup
             $this->enableCsrfProtection();
         }
 
+        $this->addOption('field_factory');
+
         parent::__construct($name, $options);
 
         // If data is passed to this constructor, objects from parent forms
@@ -86,6 +88,17 @@ class Form extends FieldGroup
         return $this->validationGroups;
     }
 
+    /**
+     * Returns a factory for automatically creating fields based on metadata
+     * available for a form's object
+     *
+     * @return FieldFactoryInterface  The factory
+     */
+    public function getFieldFactory()
+    {
+        return $this->getOption('field_factory');
+    }
+
     /**
      * Binds the form with values and files.
      *

+ 1 - 1
src/Symfony/Component/Form/HybridField.php

@@ -69,7 +69,7 @@ class HybridField extends FieldGroup
      * @throws FormException  When the field is in mode HybridField::FIELD adding
      *                        subfields is not allowed
      */
-    public function add(FieldInterface $field)
+    public function add($field)
     {
         if ($this->mode === self::FIELD) {
             throw new FormException('You cannot add nested fields while in mode FIELD');

+ 34 - 0
tests/Symfony/Tests/Component/Form/FieldFactory/FieldFactoryGuessTest.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace Symfony\Tests\Component\Form\FieldFactory;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+use Symfony\Component\Form\FieldFactory\FieldFactoryGuess;
+
+class FieldFactoryGuessTest extends \PHPUnit_Framework_TestCase
+{
+    public function testGetBestGuessReturnsGuessWithHighestConfidence()
+    {
+        $guess1 = new FieldFactoryGuess('foo', FieldFactoryGuess::MEDIUM_CONFIDENCE);
+        $guess2 = new FieldFactoryGuess('bar', FieldFactoryGuess::LOW_CONFIDENCE);
+        $guess3 = new FieldFactoryGuess('baz', FieldFactoryGuess::HIGH_CONFIDENCE);
+
+        $this->assertEquals($guess3, FieldFactoryGuess::getBestGuess(array($guess1, $guess2, $guess3)));
+    }
+
+    /**
+     * @expectedException \UnexpectedValueException
+     */
+    public function testGuessExpectsValidConfidence()
+    {
+        new FieldFactoryGuess('foo', 5);
+    }
+}

+ 203 - 0
tests/Symfony/Tests/Component/Form/FieldFactory/FieldFactoryTest.php

@@ -0,0 +1,203 @@
+<?php
+
+namespace Symfony\Tests\Component\Form\FieldFactory;
+
+/*
+ * This file is part of the Symfony framework.
+ *
+ * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+use Symfony\Component\Form\FieldFactory\FieldFactory;
+use Symfony\Component\Form\FieldFactory\FieldFactoryGuess;
+use Symfony\Component\Form\FieldFactory\FieldFactoryClassGuess;
+
+class FieldFactoryTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
+     */
+    public function testConstructThrowsExceptionIfNoGuesser()
+    {
+        new FieldFactory(array(new \stdClass()));
+    }
+
+    public function testGetInstanceCreatesClassWithHighestConfidence()
+    {
+        $object = new \stdClass();
+        $object->firstName = 'Bernhard';
+
+        $guesser1 = $this->getMock('Symfony\Component\Form\FieldFactory\FieldFactoryGuesserInterface');
+        $guesser1->expects($this->once())
+                ->method('guessClass')
+                ->with($this->equalTo($object), $this->equalTo('firstName'))
+                ->will($this->returnValue(new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\TextField',
+                    array('max_length' => 10),
+                    FieldFactoryGuess::MEDIUM_CONFIDENCE
+                )));
+
+        $guesser2 = $this->getMock('Symfony\Component\Form\FieldFactory\FieldFactoryGuesserInterface');
+        $guesser2->expects($this->once())
+                ->method('guessClass')
+                ->with($this->equalTo($object), $this->equalTo('firstName'))
+                ->will($this->returnValue(new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\PasswordField',
+                    array('max_length' => 7),
+                    FieldFactoryGuess::HIGH_CONFIDENCE
+                )));
+
+        $factory = new FieldFactory(array($guesser1, $guesser2));
+        $field = $factory->getInstance($object, 'firstName');
+
+        $this->assertEquals('Symfony\Component\Form\PasswordField', get_class($field));
+        $this->assertEquals(7, $field->getMaxLength());
+    }
+
+    public function testGetInstanceThrowsExceptionIfNoClassIsFound()
+    {
+        $object = new \stdClass();
+        $object->firstName = 'Bernhard';
+
+        $guesser = $this->getMock('Symfony\Component\Form\FieldFactory\FieldFactoryGuesserInterface');
+        $guesser->expects($this->once())
+                ->method('guessClass')
+                ->with($this->equalTo($object), $this->equalTo('firstName'))
+                ->will($this->returnValue(null));
+
+        $factory = new FieldFactory(array($guesser));
+
+        $this->setExpectedException('\RuntimeException');
+
+        $field = $factory->getInstance($object, 'firstName');
+    }
+
+    public function testOptionsCanBeOverridden()
+    {
+        $object = new \stdClass();
+        $object->firstName = 'Bernhard';
+
+        $guesser = $this->getMock('Symfony\Component\Form\FieldFactory\FieldFactoryGuesserInterface');
+        $guesser->expects($this->once())
+                ->method('guessClass')
+                ->with($this->equalTo($object), $this->equalTo('firstName'))
+                ->will($this->returnValue(new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\TextField',
+                    array('max_length' => 10),
+                    FieldFactoryGuess::MEDIUM_CONFIDENCE
+                )));
+
+        $factory = new FieldFactory(array($guesser));
+        $field = $factory->getInstance($object, 'firstName', array('max_length' => 11));
+
+        $this->assertEquals('Symfony\Component\Form\TextField', get_class($field));
+        $this->assertEquals(11, $field->getMaxLength());
+    }
+
+    public function testGetInstanceUsesMaxLengthIfFoundAndTextField()
+    {
+        $object = new \stdClass();
+        $object->firstName = 'Bernhard';
+
+        $guesser1 = $this->getMock('Symfony\Component\Form\FieldFactory\FieldFactoryGuesserInterface');
+        $guesser1->expects($this->once())
+                ->method('guessClass')
+                ->with($this->equalTo($object), $this->equalTo('firstName'))
+                ->will($this->returnValue(new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\TextField',
+                    array('max_length' => 10),
+                    FieldFactoryGuess::MEDIUM_CONFIDENCE
+                )));
+        $guesser1->expects($this->once())
+                ->method('guessMaxLength')
+                ->with($this->equalTo($object), $this->equalTo('firstName'))
+                ->will($this->returnValue(new FieldFactoryGuess(
+                	15,
+                    FieldFactoryGuess::MEDIUM_CONFIDENCE
+                )));
+
+        $guesser2 = $this->getMock('Symfony\Component\Form\FieldFactory\FieldFactoryGuesserInterface');
+        $guesser2->expects($this->once())
+                ->method('guessMaxLength')
+                ->with($this->equalTo($object), $this->equalTo('firstName'))
+                ->will($this->returnValue(new FieldFactoryGuess(
+                	20,
+                    FieldFactoryGuess::HIGH_CONFIDENCE
+                )));
+
+        $factory = new FieldFactory(array($guesser1, $guesser2));
+        $field = $factory->getInstance($object, 'firstName');
+
+        $this->assertEquals('Symfony\Component\Form\TextField', get_class($field));
+        $this->assertEquals(20, $field->getMaxLength());
+    }
+
+    public function testGetInstanceUsesMaxLengthIfFoundAndSubclassOfTextField()
+    {
+        $object = new \stdClass();
+        $object->firstName = 'Bernhard';
+
+        $guesser = $this->getMock('Symfony\Component\Form\FieldFactory\FieldFactoryGuesserInterface');
+        $guesser->expects($this->once())
+                ->method('guessClass')
+                ->with($this->equalTo($object), $this->equalTo('firstName'))
+                ->will($this->returnValue(new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\PasswordField',
+                    array('max_length' => 10),
+                    FieldFactoryGuess::MEDIUM_CONFIDENCE
+                )));
+        $guesser->expects($this->once())
+                ->method('guessMaxLength')
+                ->with($this->equalTo($object), $this->equalTo('firstName'))
+                ->will($this->returnValue(new FieldFactoryGuess(
+                	15,
+                    FieldFactoryGuess::MEDIUM_CONFIDENCE
+                )));
+
+        $factory = new FieldFactory(array($guesser));
+        $field = $factory->getInstance($object, 'firstName');
+
+        $this->assertEquals('Symfony\Component\Form\PasswordField', get_class($field));
+        $this->assertEquals(15, $field->getMaxLength());
+    }
+
+    public function testGetInstanceUsesRequiredSettingWithHighestConfidence()
+    {
+        $object = new \stdClass();
+        $object->firstName = 'Bernhard';
+
+        $guesser1 = $this->getMock('Symfony\Component\Form\FieldFactory\FieldFactoryGuesserInterface');
+        $guesser1->expects($this->once())
+                ->method('guessClass')
+                ->with($this->equalTo($object), $this->equalTo('firstName'))
+                ->will($this->returnValue(new FieldFactoryClassGuess(
+                	'Symfony\Component\Form\TextField',
+                    array(),
+                    FieldFactoryGuess::MEDIUM_CONFIDENCE
+                )));
+        $guesser1->expects($this->once())
+                ->method('guessRequired')
+                ->with($this->equalTo($object), $this->equalTo('firstName'))
+                ->will($this->returnValue(new FieldFactoryGuess(
+                	true,
+                    FieldFactoryGuess::MEDIUM_CONFIDENCE
+                )));
+
+        $guesser2 = $this->getMock('Symfony\Component\Form\FieldFactory\FieldFactoryGuesserInterface');
+        $guesser2->expects($this->once())
+                ->method('guessRequired')
+                ->with($this->equalTo($object), $this->equalTo('firstName'))
+                ->will($this->returnValue(new FieldFactoryGuess(
+                	false,
+                    FieldFactoryGuess::HIGH_CONFIDENCE
+                )));
+
+        $factory = new FieldFactory(array($guesser1, $guesser2));
+        $field = $factory->getInstance($object, 'firstName');
+
+        $this->assertFalse($field->isRequired());
+    }
+}

+ 76 - 0
tests/Symfony/Tests/Component/Form/FieldGroupTest.php

@@ -435,6 +435,64 @@ class FieldGroupTest extends \PHPUnit_Framework_TestCase
         $group->add($field);
     }
 
+    /**
+     * @expectedException Symfony\Component\Form\Exception\UnexpectedTypeException
+     */
+    public function testAddThrowsExceptionIfNoFieldOrString()
+    {
+        $group = new TestFieldGroup('author');
+
+        $group->add(1234);
+    }
+
+    /**
+     * @expectedException Symfony\Component\Form\Exception\DanglingFieldException
+     */
+    public function testAddThrowsExceptionIfStringButNoRootForm()
+    {
+        $group = new TestFieldGroup('author');
+
+        $group->add('firstName');
+    }
+
+    public function testAddThrowsExceptionIfStringButNoFieldFactory()
+    {
+        $form = $this->createMockForm();
+        $form->expects($this->once())
+                ->method('getFieldFactory')
+                ->will($this->returnValue(null));
+
+        $group = new TestFieldGroup('author');
+        $group->setParent($form);
+
+        $this->setExpectedException('\LogicException');
+
+        $group->add('firstName');
+    }
+
+    public function testAddUsesFieldFromFactoryIfStringIsGiven()
+    {
+        $author = new \stdClass();
+        $field = $this->createMockField('firstName');
+
+        $factory = $this->getMock('Symfony\Component\Form\FieldFactory\FieldFactoryInterface');
+        $factory->expects($this->once())
+                ->method('getInstance')
+                ->with($this->equalTo($author), $this->equalTo('firstName'), $this->equalTo(array('foo' => 'bar')))
+                ->will($this->returnValue($field));
+        $form = $this->createMockForm();
+        $form->expects($this->once())
+                ->method('getFieldFactory')
+                ->will($this->returnValue($factory));
+
+        $group = new TestFieldGroup('author');
+        $group->setParent($form);
+        $group->setData($author);
+        $group->add('firstName', array('foo' => 'bar'));
+
+        $this->assertSame($field, $group['firstName']);
+    }
+
     public function testSetDataUpdatesAllFieldsFromTransformedData()
     {
         $originalAuthor = new Author();
@@ -693,6 +751,24 @@ class FieldGroupTest extends \PHPUnit_Framework_TestCase
         return $field;
     }
 
+    protected function createMockForm()
+    {
+        $form = $this->getMock(
+        	'Symfony\Component\Form\Form',
+            array(),
+            array(),
+            '',
+            false, // don't use constructor
+            false  // don't call parent::__clone)
+        );
+
+        $form->expects($this->any())
+                ->method('getRoot')
+                ->will($this->returnValue($form));
+
+        return $form;
+    }
+
     protected function createInvalidMockField($key)
     {
         $field = $this->createMockField($key);

+ 17 - 0
tests/Symfony/Tests/Component/Form/FieldTest.php

@@ -483,6 +483,23 @@ class FieldTest extends \PHPUnit_Framework_TestCase
         $this->assertFalse($field->isTransformationSuccessful());
     }
 
+    public function testGetRootReturnsRootOfParentIfSet()
+    {
+        $parent = $this->createMockGroup();
+        $parent->expects($this->any())
+                ->method('getRoot')
+                ->will($this->returnValue('ROOT'));
+
+        $this->field->setParent($parent);
+
+        $this->assertEquals('ROOT', $this->field->getRoot());
+    }
+
+    public function testGetRootReturnsFieldIfNoParent()
+    {
+        $this->assertEquals($this->field, $this->field->getRoot());
+    }
+
     protected function createMockTransformer()
     {
         return $this->getMock('Symfony\Component\Form\ValueTransformer\ValueTransformerInterface', array(), array(), '', false, false);