From 44ec2b4b298eb0e6be3d1a95ba55da211eb8633f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Mon, 4 Nov 2013 16:27:38 +0000 Subject: [PATCH] Stole the TypeGuesser (and its tests) from symfony's propel bridge --- Form/PropelTypeGuesser.php | 182 +++++++++++++++++++++++++++ Resources/config/propel.xml | 5 + Tests/Fixtures/Column.php | 61 +++++++++ Tests/Fixtures/Item.php | 109 ++++++++++++++++ Tests/Fixtures/ItemQuery.php | 100 +++++++++++++++ Tests/Form/PropelTypeGuesserTest.php | 129 +++++++++++++++++++ 6 files changed, 586 insertions(+) create mode 100644 Form/PropelTypeGuesser.php create mode 100644 Tests/Fixtures/Column.php create mode 100644 Tests/Fixtures/Item.php create mode 100644 Tests/Fixtures/ItemQuery.php create mode 100644 Tests/Form/PropelTypeGuesserTest.php diff --git a/Form/PropelTypeGuesser.php b/Form/PropelTypeGuesser.php new file mode 100644 index 0000000..a6a7001 --- /dev/null +++ b/Form/PropelTypeGuesser.php @@ -0,0 +1,182 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Form; + +use Propel\Runtime\Map\RelationMap; +use Propel\Runtime\Util\PropelColumnTypes; +use Symfony\Component\Form\FormTypeGuesserInterface; +use Symfony\Component\Form\Guess\Guess; +use Symfony\Component\Form\Guess\TypeGuess; +use Symfony\Component\Form\Guess\ValueGuess; + +/** + * Propel Type guesser. + * + * @author Fabien Potencier + */ +class PropelTypeGuesser implements FormTypeGuesserInterface +{ + private $cache = array(); + + /** + * {@inheritDoc} + */ + public function guessType($class, $property) + { + if (!$table = $this->getTable($class)) { + return new TypeGuess('text', array(), Guess::LOW_CONFIDENCE); + } + + foreach ($table->getRelations() as $relation) { + if ($relation->getType() === RelationMap::MANY_TO_ONE) { + if (strtolower($property) === strtolower($relation->getName())) { + return new TypeGuess('model', array( + 'class' => $relation->getForeignTable()->getClassName(), + 'multiple' => false, + ), Guess::HIGH_CONFIDENCE); + } + } elseif ($relation->getType() === RelationMap::ONE_TO_MANY) { + if (strtolower($property) === strtolower($relation->getPluralName())) { + return new TypeGuess('model', array( + 'class' => $relation->getForeignTable()->getClassName(), + 'multiple' => true, + ), Guess::HIGH_CONFIDENCE); + } + } elseif ($relation->getType() === RelationMap::MANY_TO_MANY) { + if (strtolower($property) == strtolower($relation->getPluralName())) { + return new TypeGuess('model', array( + 'class' => $relation->getLocalTable()->getClassName(), + 'multiple' => true, + ), Guess::HIGH_CONFIDENCE); + } + } + } + + if (!$column = $this->getColumn($class, $property)) { + return new TypeGuess('text', array(), Guess::LOW_CONFIDENCE); + } + + switch ($column->getType()) { + case PropelColumnTypes::BOOLEAN: + case PropelColumnTypes::BOOLEAN_EMU: + return new TypeGuess('checkbox', array(), Guess::HIGH_CONFIDENCE); + case PropelColumnTypes::TIMESTAMP: + case PropelColumnTypes::BU_TIMESTAMP: + return new TypeGuess('datetime', array(), Guess::HIGH_CONFIDENCE); + case PropelColumnTypes::DATE: + case PropelColumnTypes::BU_DATE: + return new TypeGuess('date', array(), Guess::HIGH_CONFIDENCE); + case PropelColumnTypes::TIME: + return new TypeGuess('time', array(), Guess::HIGH_CONFIDENCE); + case PropelColumnTypes::FLOAT: + case PropelColumnTypes::REAL: + case PropelColumnTypes::DOUBLE: + case PropelColumnTypes::DECIMAL: + return new TypeGuess('number', array(), Guess::MEDIUM_CONFIDENCE); + case PropelColumnTypes::TINYINT: + case PropelColumnTypes::SMALLINT: + case PropelColumnTypes::INTEGER: + case PropelColumnTypes::BIGINT: + case PropelColumnTypes::NUMERIC: + return new TypeGuess('integer', array(), Guess::MEDIUM_CONFIDENCE); + case PropelColumnTypes::ENUM: + case PropelColumnTypes::CHAR: + if ($column->getValueSet()) { + //check if this is mysql enum + $choices = $column->getValueSet(); + $labels = array_map('ucfirst', $choices); + + return new TypeGuess('choice', array('choices' => array_combine($choices, $labels)), Guess::MEDIUM_CONFIDENCE); + } + case PropelColumnTypes::VARCHAR: + return new TypeGuess('text', array(), Guess::MEDIUM_CONFIDENCE); + case PropelColumnTypes::LONGVARCHAR: + case PropelColumnTypes::BLOB: + case PropelColumnTypes::CLOB: + case PropelColumnTypes::CLOB_EMU: + return new TypeGuess('textarea', array(), Guess::MEDIUM_CONFIDENCE); + default: + return new TypeGuess('text', array(), Guess::LOW_CONFIDENCE); + } + } + + /** + * {@inheritDoc} + */ + public function guessRequired($class, $property) + { + if ($column = $this->getColumn($class, $property)) { + return new ValueGuess($column->isNotNull(), Guess::HIGH_CONFIDENCE); + } + } + + /** + * {@inheritDoc} + */ + public function guessMaxLength($class, $property) + { + if ($column = $this->getColumn($class, $property)) { + if ($column->isTextType()) { + return new ValueGuess($column->getSize(), Guess::HIGH_CONFIDENCE); + } + switch ($column->getType()) { + case PropelColumnTypes::FLOAT: + case PropelColumnTypes::REAL: + case PropelColumnTypes::DOUBLE: + case PropelColumnTypes::DECIMAL: + return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE); + } + } + } + + /** + * {@inheritDoc} + */ + public function guessPattern($class, $property) + { + if ($column = $this->getColumn($class, $property)) { + switch ($column->getType()) { + case PropelColumnTypes::FLOAT: + case PropelColumnTypes::REAL: + case PropelColumnTypes::DOUBLE: + case PropelColumnTypes::DECIMAL: + return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE); + } + } + } + + protected function getTable($class) + { + if (isset($this->cache[$class])) { + return $this->cache[$class]; + } + + if (class_exists($queryClass = $class.'Query')) { + $query = new $queryClass(); + + return $this->cache[$class] = $query->getTableMap(); + } + } + + protected function getColumn($class, $property) + { + if (isset($this->cache[$class.'::'.$property])) { + return $this->cache[$class.'::'.$property]; + } + + $table = $this->getTable($class); + + if ($table && $table->hasColumn($property)) { + return $this->cache[$class.'::'.$property] = $table->getColumn($property); + } + } +} diff --git a/Resources/config/propel.xml b/Resources/config/propel.xml index 93de36b..361825a 100644 --- a/Resources/config/propel.xml +++ b/Resources/config/propel.xml @@ -12,6 +12,7 @@ Propel\PropelBundle\DataCollector\PropelDataCollector Propel\PropelBundle\Logger\PropelLogger Propel\PropelBundle\Twig\Extension\SyntaxExtension + Propel\PropelBundle\Form\PropelTypeGuesser @@ -32,5 +33,9 @@ + + + + diff --git a/Tests/Fixtures/Column.php b/Tests/Fixtures/Column.php new file mode 100644 index 0000000..b708db2 --- /dev/null +++ b/Tests/Fixtures/Column.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Tests\Fixtures; + +use Propel\Runtime\Util\PropelColumnTypes; + +class Column +{ + private $name; + + private $type; + + public function __construct($name, $type) + { + $this->name = $name; + $this->type = $type; + } + + public function getType() + { + return $this->type; + } + + public function isTextType() + { + if (!$this->type) { + return false; + } + + switch ($this->type) { + case PropelColumnTypes::CHAR: + case PropelColumnTypes::VARCHAR: + case PropelColumnTypes::LONGVARCHAR: + case PropelColumnTypes::BLOB: + case PropelColumnTypes::CLOB: + case PropelColumnTypes::CLOB_EMU: + return true; + } + + return false; + } + + public function getSize() + { + return $this->isTextType() ? 255 : 0; + } + + public function isNotNull() + { + return ('id' === $this->name); + } +} diff --git a/Tests/Fixtures/Item.php b/Tests/Fixtures/Item.php new file mode 100644 index 0000000..b111510 --- /dev/null +++ b/Tests/Fixtures/Item.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Tests\Fixtures; + +use Propel\Runtime\ActiveRecord\ActiveRecordInterface; +use Propel\Runtime\Connection\ConnectionInterface; + +class Item implements ActiveRecordInterface +{ + private $id; + + private $value; + + private $groupName; + + private $price; + + public function __construct($id = null, $value = null, $groupName = null, $price = null) + { + $this->id = $id; + $this->value = $value; + $this->groupName = $groupName; + $this->price = $price; + } + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getValue() + { + return $this->value; + } + + public function getGroupName() + { + return $this->groupName; + } + + public function getPrice() + { + return $this->price; + } + + public function getPrimaryKey() + { + return $this->getId(); + } + + public function setPrimaryKey($primaryKey) + { + $this->setId($primaryKey); + } + + public function isModified() + { + return false; + } + + public function isColumnModified($col) + { + return false; + } + + public function isNew() + { + return false; + } + + public function setNew($b) + { + } + + public function resetModified() + { + } + + public function isDeleted() + { + return false; + } + + public function setDeleted($b) + { + } + + public function delete(ConnectionInterface $con = null) + { + } + + public function save(ConnectionInterface $con = null) + { + } +} diff --git a/Tests/Fixtures/ItemQuery.php b/Tests/Fixtures/ItemQuery.php new file mode 100644 index 0000000..5a6a683 --- /dev/null +++ b/Tests/Fixtures/ItemQuery.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Tests\Fixtures; + +use Propel\Runtime\Map\ColumnMap; +use Propel\Runtime\Map\RelationMap; +use Propel\Runtime\Map\TableMap; +use Propel\Runtime\Util\PropelColumnTypes; + +class ItemQuery +{ + private $map = array( + 'id' => PropelColumnTypes::INTEGER, + 'value' => PropelColumnTypes::VARCHAR, + 'price' => PropelColumnTypes::FLOAT, + 'is_active' => PropelColumnTypes::BOOLEAN, + 'enabled' => PropelColumnTypes::BOOLEAN_EMU, + 'updated_at' => PropelColumnTypes::TIMESTAMP, + + 'updated_at' => PropelColumnTypes::TIMESTAMP, + 'updated_at' => PropelColumnTypes::TIMESTAMP, + 'updated_at' => PropelColumnTypes::TIMESTAMP, + ); + + public function getTableMap() + { + // Allows to define methods in this class + // to avoid a lot of mock classes + return $this; + } + + public function getPrimaryKeys() + { + $cm = new ColumnMap('id', new TableMap()); + $cm->setType('INTEGER'); + + return array('id' => $cm); + } + + /** + * Method from the TableMap API + */ + public function hasColumn($column) + { + return in_array($column, array_keys($this->map)); + } + + /** + * Method from the TableMap API + */ + public function getColumn($column) + { + if ($this->hasColumn($column)) { + return new Column($column, $this->map[$column]); + } + + return null; + } + + /** + * Method from the TableMap API + */ + public function getRelations() + { + // table maps + $authorTable = new TableMap(); + $authorTable->setClassName('\Foo\Author'); + + $resellerTable = new TableMap(); + $resellerTable->setClassName('\Foo\Reseller'); + + // relations + $mainAuthorRelation = new RelationMap('MainAuthor'); + $mainAuthorRelation->setType(RelationMap::MANY_TO_ONE); + $mainAuthorRelation->setForeignTable($authorTable); + + $authorRelation = new RelationMap('Author'); + $authorRelation->setType(RelationMap::ONE_TO_MANY); + $authorRelation->setForeignTable($authorTable); + + $resellerRelation = new RelationMap('Reseller'); + $resellerRelation->setType(RelationMap::MANY_TO_MANY); + $resellerRelation->setLocalTable($resellerTable); + + return array( + $mainAuthorRelation, + $authorRelation, + $resellerRelation + ); + } +} diff --git a/Tests/Form/PropelTypeGuesserTest.php b/Tests/Form/PropelTypeGuesserTest.php new file mode 100644 index 0000000..a5c9137 --- /dev/null +++ b/Tests/Form/PropelTypeGuesserTest.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Tests\Form; + +use Propel\PropelBundle\Form\PropelTypeGuesser; +use Propel\PropelBundle\Tests\TestCase; +use Symfony\Component\Form\Guess\Guess; + +class PropelTypeGuesserTest extends TestCase +{ + const CLASS_NAME = 'Propel\PropelBundle\Tests\Fixtures\Item'; + + const UNKNOWN_CLASS_NAME = 'Propel\PropelBundle\Tests\Fixtures\UnknownItem'; + + private $guesser; + + public function setUp() + { + $this->guesser = new PropelTypeGuesser(); + } + + public function testGuessMaxLengthWithText() + { + $value = $this->guesser->guessMaxLength(self::CLASS_NAME, 'value'); + + $this->assertNotNull($value); + $this->assertEquals(255, $value->getValue()); + } + + public function testGuessMaxLengthWithFloat() + { + $value = $this->guesser->guessMaxLength(self::CLASS_NAME, 'price'); + + $this->assertNotNull($value); + $this->assertNull($value->getValue()); + } + + public function testGuessMinLengthWithText() + { + $value = $this->guesser->guessPattern(self::CLASS_NAME, 'value'); + + $this->assertNull($value); + } + + public function testGuessMinLengthWithFloat() + { + $value = $this->guesser->guessPattern(self::CLASS_NAME, 'price'); + + $this->assertNotNull($value); + $this->assertNull($value->getValue()); + } + + public function testGuessRequired() + { + $value = $this->guesser->guessRequired(self::CLASS_NAME, 'id'); + + $this->assertNotNull($value); + $this->assertTrue($value->getValue()); + } + + public function testGuessRequiredWithNullableColumn() + { + $value = $this->guesser->guessRequired(self::CLASS_NAME, 'value'); + + $this->assertNotNull($value); + $this->assertFalse($value->getValue()); + } + + public function testGuessTypeWithoutTable() + { + $value = $this->guesser->guessType(self::UNKNOWN_CLASS_NAME, 'property'); + + $this->assertNotNull($value); + $this->assertEquals('text', $value->getType()); + $this->assertEquals(Guess::LOW_CONFIDENCE, $value->getConfidence()); + } + + public function testGuessTypeWithoutColumn() + { + $value = $this->guesser->guessType(self::CLASS_NAME, 'property'); + + $this->assertNotNull($value); + $this->assertEquals('text', $value->getType()); + $this->assertEquals(Guess::LOW_CONFIDENCE, $value->getConfidence()); + } + + /** + * @dataProvider dataProviderForGuessType + */ + public function testGuessType($property, $type, $confidence, $multiple = null) + { + $value = $this->guesser->guessType(self::CLASS_NAME, $property); + + $this->assertNotNull($value); + $this->assertEquals($type, $value->getType()); + $this->assertEquals($confidence, $value->getConfidence()); + + if ($type === 'model') { + $options = $value->getOptions(); + + $this->assertSame($multiple, $options['multiple']); + } + } + + public static function dataProviderForGuessType() + { + return array( + array('is_active', 'checkbox', Guess::HIGH_CONFIDENCE), + array('enabled', 'checkbox', Guess::HIGH_CONFIDENCE), + array('id', 'integer', Guess::MEDIUM_CONFIDENCE), + array('value', 'text', Guess::MEDIUM_CONFIDENCE), + array('price', 'number', Guess::MEDIUM_CONFIDENCE), + array('updated_at', 'datetime', Guess::HIGH_CONFIDENCE), + + array('Authors', 'model', Guess::HIGH_CONFIDENCE, true), + array('Resellers', 'model', Guess::HIGH_CONFIDENCE, true), + array('MainAuthor', 'model', Guess::HIGH_CONFIDENCE, false), + ); + } +}