From 213579bf465aa6d6d63f45c3977464d387028b70 Mon Sep 17 00:00:00 2001 From: Andrew Moore Date: Tue, 26 Nov 2013 11:15:00 -0500 Subject: [PATCH] Implemented CoreTypeGuesser which guesses if an added form child is a specialized button type based on the field name and the underlying data_class --- .../FrameworkBundle/Resources/config/form.xml | 6 + .../Form/Extension/Core/CoreExtension.php | 5 + .../Form/Extension/Core/CoreTypeGuesser.php | 69 +++++++++++ .../Extension/Core/CoreTypeGuesserTest.php | 51 ++++++++ .../Fixtures/SubmitResetPropertyFixture.php | 34 ++++++ .../Form/Util/ClassReflectionUtil.php | 114 ++++++++++++++++++ 6 files changed, 279 insertions(+) create mode 100644 src/Symfony/Component/Form/Extension/Core/CoreTypeGuesser.php create mode 100644 src/Symfony/Component/Form/Tests/Extension/Core/CoreTypeGuesserTest.php create mode 100644 src/Symfony/Component/Form/Tests/Fixtures/SubmitResetPropertyFixture.php create mode 100644 src/Symfony/Component/Form/Util/ClassReflectionUtil.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml index 93fa5235871cb..d05e07064e6d7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml @@ -9,6 +9,7 @@ Symfony\Component\Form\FormRegistry Symfony\Component\Form\FormFactory Symfony\Component\Form\Extension\DependencyInjection\DependencyInjectionExtension + Symfony\Component\Form\Extension\Core\CoreTypeGuesser Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser @@ -47,6 +48,11 @@ + + + + + diff --git a/src/Symfony/Component/Form/Extension/Core/CoreExtension.php b/src/Symfony/Component/Form/Extension/Core/CoreExtension.php index bbcac4baea933..350e34e224936 100644 --- a/src/Symfony/Component/Form/Extension/Core/CoreExtension.php +++ b/src/Symfony/Component/Form/Extension/Core/CoreExtension.php @@ -56,4 +56,9 @@ protected function loadTypes() new Type\CurrencyType(), ); } + + public function loadTypeGuesser() + { + return new CoreTypeGuesser(); + } } diff --git a/src/Symfony/Component/Form/Extension/Core/CoreTypeGuesser.php b/src/Symfony/Component/Form/Extension/Core/CoreTypeGuesser.php new file mode 100644 index 0000000000000..f356dc3562eb5 --- /dev/null +++ b/src/Symfony/Component/Form/Extension/Core/CoreTypeGuesser.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Extension\Core; + +use Symfony\Component\Form\FormTypeGuesserInterface; +use Symfony\Component\Form\Guess\Guess; +use Symfony\Component\Form\Guess\TypeGuess; +use Symfony\Component\Form\Util\ClassReflectionUtil; + +/** + * @author Andrew Moore + */ +class CoreTypeGuesser implements FormTypeGuesserInterface +{ + private static $nameToTypeMapping = array( + 'submit' => 'submit', + 'reset' => 'reset', + ); + + /** + * {@inheritDoc} + */ + public function guessType($class, $property) + { + // Guess types for specialized button types + if (!array_key_exists($property, self::$nameToTypeMapping)) { + return null; + } + + if (!ClassReflectionUtil::hasPropertyAvailable($class, $property)) { + return new TypeGuess(self::$nameToTypeMapping[$property], array(), Guess::LOW_CONFIDENCE); + } + + return null; + } + + /** + * {@inheritDoc} + */ + public function guessRequired($class, $property) + { + return null; + } + + /** + * {@inheritDoc} + */ + public function guessMaxLength($class, $property) + { + return null; + } + + /** + * {@inheritDoc} + */ + public function guessPattern($class, $property) + { + return null; + } +} diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/CoreTypeGuesserTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/CoreTypeGuesserTest.php new file mode 100644 index 0000000000000..baf725b7a61ef --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Extension/Core/CoreTypeGuesserTest.php @@ -0,0 +1,51 @@ + + * + * 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\Core; + +use Symfony\Component\Form\Guess\Guess; +use Symfony\Component\Form\Extension\Core\CoreTypeGuesser; + +class CoreTypeGuesserTest extends \PHPUnit_Framework_TestCase +{ + /** @var CoreTypeGuesser */ + private $guesser; + + const AUTHOR_FIXTURE = 'Symfony\Component\Form\Tests\Fixtures\Author'; + const PROPERTIES_DEFINED_FIXTURE = 'Symfony\Component\Form\Tests\Fixtures\SubmitResetPropertyFixture'; + + protected function setUp() + { + $this->guesser = new CoreTypeGuesser(); + } + + public function testButtonTypeInferral() + { + $submitGuess = $this->guesser->guessType(self::AUTHOR_FIXTURE, 'submit'); + $resetGuess = $this->guesser->guessType(self::AUTHOR_FIXTURE, 'reset'); + + $this->assertInstanceOf('Symfony\Component\Form\Guess\TypeGuess', $submitGuess); + $this->assertEquals('submit', $submitGuess->getType()); + $this->assertEquals(Guess::LOW_CONFIDENCE, $submitGuess->getConfidence()); + + $this->assertInstanceOf('Symfony\Component\Form\Guess\TypeGuess', $resetGuess); + $this->assertEquals('reset', $resetGuess->getType()); + $this->assertEquals(Guess::LOW_CONFIDENCE, $resetGuess->getConfidence()); + } + + public function testButtonPropertyExists() + { + $submitGuess = $this->guesser->guessType(self::PROPERTIES_DEFINED_FIXTURE, 'submit'); + $resetGuess = $this->guesser->guessType(self::PROPERTIES_DEFINED_FIXTURE, 'reset'); + + $this->assertNull($submitGuess); + $this->assertNull($resetGuess); + } +} diff --git a/src/Symfony/Component/Form/Tests/Fixtures/SubmitResetPropertyFixture.php b/src/Symfony/Component/Form/Tests/Fixtures/SubmitResetPropertyFixture.php new file mode 100644 index 0000000000000..f87488c19c276 --- /dev/null +++ b/src/Symfony/Component/Form/Tests/Fixtures/SubmitResetPropertyFixture.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Tests\Fixtures; + +class SubmitResetPropertyFixture +{ + private $submit; + public $reset; + + /** + * @return mixed + */ + public function getSubmit() + { + return $this->submit; + } + + /** + * @param mixed $submit + */ + public function setSubmit($submit) + { + $this->submit = $submit; + } +} diff --git a/src/Symfony/Component/Form/Util/ClassReflectionUtil.php b/src/Symfony/Component/Form/Util/ClassReflectionUtil.php new file mode 100644 index 0000000000000..f25b42be73698 --- /dev/null +++ b/src/Symfony/Component/Form/Util/ClassReflectionUtil.php @@ -0,0 +1,114 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form\Util; + +use Symfony\Component\PropertyAccess\StringUtil; + +/** + * @author Andrew Moore + */ +class ClassReflectionUtil +{ + /** + * This class should not be instantiated + */ + private function __construct() + { + } + + /** + * Detects if a property or adder/setter is available for a given + * property. + * + * @param string $class The class to verify against + * @param string $property The property to verify + * + * @return Boolean + */ + public static function hasPropertyAvailable($class, $property) + { + if (!class_exists($class)) { + return false; + } + + $reflClass = new \ReflectionClass($class); + + $setter = 'set' . self::camelize($property); + $classHasProperty = $reflClass->hasProperty($property); + + if ($reflClass->hasMethod($setter) && $reflClass->getMethod($setter)->isPublic()) { + return true; + } + if ($reflClass->hasMethod('__set') && $reflClass->getMethod('__set')->isPublic()) { + return true; + } + if ($classHasProperty && $reflClass->getProperty($property)->isPublic()) { + return true; + } + + $plural = self::camelize($property); + $singular = StringUtil::singularify($plural); + + $adder = 'add' . $singular; + $remover = 'remove' . $singular; + + $adderFound = self::isMethodAccessible($reflClass, $adder, 1); + $removerFound = self::isMethodAccessible($reflClass, $remover, 1); + + if ($adderFound && $removerFound) { + return true; + } + + return false; + } + + /** + * Camelizes a given string. + * + * @param string $string Some string + * + * @return string The camelized version of the string + */ + private static function camelize($string) + { + return preg_replace_callback( + '/(^|_|\.)+(.)/', + function ($match) { + return ('.' === $match[1] ? '_' : '') . strtoupper($match[2]); + }, + $string + ); + } + + /** + * Returns whether a method is public and has a specific number of required parameters. + * + * @param \ReflectionClass $class The class of the method + * @param string $methodName The method name + * @param integer $parameters The number of parameters + * + * @return Boolean Whether the method is public and has $parameters + * required parameters + */ + private static function isMethodAccessible(\ReflectionClass $class, $methodName, $parameters) + { + if ($class->hasMethod($methodName)) { + $method = $class->getMethod($methodName); + + if ($method->isPublic() && $method->getNumberOfRequiredParameters() === $parameters) { + return true; + } + } + + return false; + } +} \ No newline at end of file