From b6285562b40df35ad9cfa42aaf6091f4f8112ef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Thu, 12 Dec 2013 15:01:41 +0000 Subject: [PATCH] Added missing form features --- .../TranslationCollectionFormListener.php | 102 +++++++++++ .../EventListener/TranslationFormListener.php | 81 +++++++++ Form/PropelExtension.php | 37 ++++ Form/Type/TranslationCollectionType.php | 75 +++++++++ Form/Type/TranslationType.php | 54 ++++++ Tests/Fixtures/TranslatableItem.php | 131 +++++++++++++++ Tests/Fixtures/TranslatableItemI18n.php | 138 +++++++++++++++ .../Type/TranslationCollectionTypeTest.php | 158 ++++++++++++++++++ 8 files changed, 776 insertions(+) create mode 100644 Form/EventListener/TranslationCollectionFormListener.php create mode 100644 Form/EventListener/TranslationFormListener.php create mode 100644 Form/PropelExtension.php create mode 100644 Form/Type/TranslationCollectionType.php create mode 100644 Form/Type/TranslationType.php create mode 100644 Tests/Fixtures/TranslatableItem.php create mode 100644 Tests/Fixtures/TranslatableItemI18n.php create mode 100644 Tests/Form/Type/TranslationCollectionTypeTest.php diff --git a/Form/EventListener/TranslationCollectionFormListener.php b/Form/EventListener/TranslationCollectionFormListener.php new file mode 100644 index 0000000..1878469 --- /dev/null +++ b/Form/EventListener/TranslationCollectionFormListener.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 Propel\PropelBundle\Form\EventListener; + +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\FormEvent; + +/** + * listener class for propel_translatable_collection + * + * @author Patrick Kaufmann + */ +class TranslationCollectionFormListener implements EventSubscriberInterface +{ + private $i18nClass; + private $languages; + + public function __construct($languages, $i18nClass) + { + $this->i18nClass = $i18nClass; + $this->languages = $languages; + } + + public static function getSubscribedEvents() + { + return array( + FormEvents::PRE_SET_DATA => array('preSetData', 1), + ); + } + + public function preSetData(FormEvent $event) + { + $form = $event->getForm(); + $data = $event->getData(); + + if (null === $data) { + return; + } + + if (!is_array($data) && !($data instanceof \Traversable && $data instanceof \ArrayAccess)) { + throw new UnexpectedTypeException($data, 'array or (\Traversable and \ArrayAccess)'); + } + + //get the class name of the i18nClass + $temp = explode('\\', $this->i18nClass); + $dataClass = end($temp); + + $rootData = $form->getRoot()->getData(); + $foundData = false; + + $addFunction = 'add'.$dataClass; + + //add a database row for every needed language + foreach ($this->languages as $lang) { + $found = false; + + foreach ($data as $i18n) { + if (!method_exists($i18n, 'getLocale')) { + throw new UnexpectedTypeException($i18n, 'Propel i18n object'); + } + + if ($i18n->getLocale() == $lang) { + $found = true; + break; + } + } + + if (!$found) { + $currentForm = $form; + while (!$foundData) { + if (method_exists($rootData, $addFunction)) { + $foundData = true; + break; + } elseif ($currentForm->hasParent()) { + $currentForm = $currentForm->getParent(); + $rootData = $currentForm->getData(); + } else { + break; + } + } + if (!$foundData) { + throw new UnexpectedTypeException($rootData, 'Propel i18n object'); + } + + $newTranslation = new $this->i18nClass(); + $newTranslation->setLocale($lang); + + $rootData->$addFunction($newTranslation); + } + } + } +} diff --git a/Form/EventListener/TranslationFormListener.php b/Form/EventListener/TranslationFormListener.php new file mode 100644 index 0000000..785682c --- /dev/null +++ b/Form/EventListener/TranslationFormListener.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Form\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; + +/** + * Event Listener class for propel_translation + * + * @author Patrick Kaufmann + */ +class TranslationFormListener implements EventSubscriberInterface +{ + private $columns; + private $dataClass; + + public function __construct($columns, $dataClass) + { + $this->columns = $columns; + $this->dataClass = $dataClass; + } + + public static function getSubscribedEvents() + { + return array( + FormEvents::PRE_SET_DATA => array('preSetData', 1), + ); + } + + public function preSetData(FormEvent $event) + { + $form = $event->getForm(); + $data = $event->getData(); + + if (!$data instanceof $this->dataClass) { + return; + } + + //loop over all columns and add the input + foreach ($this->columns as $column => $options) { + if (is_string($options)) { + $column = $options; + $options = array(); + } + if (null === $options) { + $options = array(); + } + + $type = 'text'; + if (array_key_exists('type', $options)) { + $type = $options['type']; + } + $label = $column; + if (array_key_exists('label', $options)) { + $label = $options['label']; + } + + $customOptions = array(); + if (array_key_exists('options', $options)) { + $customOptions = $options['options']; + } + $options = array( + 'label' => $label.' '.strtoupper($data->getLocale()) + ); + + $options = array_merge($options, $customOptions); + + $form->add($column, $type, $options); + } + } +} diff --git a/Form/PropelExtension.php b/Form/PropelExtension.php new file mode 100644 index 0000000..413ff4b --- /dev/null +++ b/Form/PropelExtension.php @@ -0,0 +1,37 @@ + +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ + +namespace Propel\PropelBundle\Form; + +use Symfony\Component\Form\AbstractExtension; +use Symfony\Component\PropertyAccess\PropertyAccess; + +/** +* Represents the Propel form extension, which loads the Propel functionality. +* +* @author Joseph Rouff +*/ +class PropelExtension extends AbstractExtension +{ + protected function loadTypes() + { + return array( + new Type\ModelType(PropertyAccess::getPropertyAccessor()), + new Type\TranslationCollectionType(), + new Type\TranslationType() + ); + } + + protected function loadTypeGuesser() + { + return new TypeGuesser(); + } +} diff --git a/Form/Type/TranslationCollectionType.php b/Form/Type/TranslationCollectionType.php new file mode 100644 index 0000000..79010d7 --- /dev/null +++ b/Form/Type/TranslationCollectionType.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Form\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; +use Propel\PropelBundle\Form\EventListener\TranslationCollectionFormListener; + +/** + * form type for i18n-columns in propel + * + * @author Patrick Kaufmann + */ +class TranslationCollectionType extends AbstractType +{ + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + if (!isset($options['options']['data_class']) || null === $options['options']['data_class']) { + throw new MissingOptionsException('data_class must be set'); + } + if (!isset($options['options']['columns']) || null === $options['options']['columns']) { + throw new MissingOptionsException('columns must be set'); + } + + $listener = new TranslationCollectionFormListener($options['languages'], $options['options']['data_class']); + $builder->addEventSubscriber($listener); + } + + public function getParent() + { + return 'collection'; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'propel_translation_collection'; + } + + /** + * {@inheritdoc} + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setRequired(array( + 'languages' + )); + + $resolver->setDefaults(array( + 'type' => 'propel_translation', + 'allow_add' => false, + 'allow_delete' => false, + 'options' => array( + 'data_class' => null, + 'columns' => null + ) + )); + } +} diff --git a/Form/Type/TranslationType.php b/Form/Type/TranslationType.php new file mode 100644 index 0000000..68d53f1 --- /dev/null +++ b/Form/Type/TranslationType.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Form\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; +use Propel\PropelBundle\Form\EventListener\TranslationFormListener; + +/** + * Translation type class + * + * @author Patrick Kaufmann + */ +class TranslationType extends AbstractType +{ + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->addEventSubscriber( + new TranslationFormListener($options['columns'], $options['data_class']) + ); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'propel_translation'; + } + + /** + * {@inheritdoc} + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setRequired(array( + 'data_class', + 'columns' + )); + } +} diff --git a/Tests/Fixtures/TranslatableItem.php b/Tests/Fixtures/TranslatableItem.php new file mode 100644 index 0000000..e9f9a3f --- /dev/null +++ b/Tests/Fixtures/TranslatableItem.php @@ -0,0 +1,131 @@ + + * + * 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 TranslatableItem implements ActiveRecordInterface +{ + private $id; + + private $currentTranslations; + + private $groupName; + + private $price; + + public function __construct($id = null, $translations = array()) + { + $this->id = $id; + $this->currentTranslations = $translations; + } + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } + + 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) + { + } + + public function getTranslation($locale = 'de', ConnectionInterface $con = null) + { + if (!isset($this->currentTranslations[$locale])) { + $translation = new TranslatableItemI18n(); + $translation->setLocale($locale); + $this->currentTranslations[$locale] = $translation; + } + + return $this->currentTranslations[$locale]; + } + + public function addTranslatableItemI18n(TranslatableItemI18n $i) + { + if (!in_array($i, $this->currentTranslations)) { + $this->currentTranslations[$i->getLocale()] = $i; + $i->setItem($this); + } + } + + public function removeTranslatableItemI18n(TranslatableItemI18n $i) + { + unset($this->currentTranslations[$i->getLocale()]); + } + + public function getTranslatableItemI18ns() + { + return $this->currentTranslations; + } +} diff --git a/Tests/Fixtures/TranslatableItemI18n.php b/Tests/Fixtures/TranslatableItemI18n.php new file mode 100644 index 0000000..42432f6 --- /dev/null +++ b/Tests/Fixtures/TranslatableItemI18n.php @@ -0,0 +1,138 @@ + + * + * 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 TranslatableItemI18n implements ActiveRecordInterface +{ + private $id; + + private $locale; + + private $value; + + private $value2; + + private $item; + + public function __construct($id = null, $locale = null, $value = null) + { + $this->id = $id; + $this->locale = $locale; + $this->value = $value; + } + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } + + 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) + { + } + + public function setLocale($locale) + { + + $this->locale = $locale; + } + + public function getLocale() + { + return $this->locale; + } + + public function getItem() + { + return $this->item; + } + + public function setItem($item) + { + $this->item = $item; + } + + public function setValue($value) + { + + $this->value = $value; + } + + public function getValue() + { + return $this->value; + } + + public function setValue2($value2) + { + + $this->value2 = $value2; + } + + public function getValue2() + { + return $this->value2; + } +} diff --git a/Tests/Form/Type/TranslationCollectionTypeTest.php b/Tests/Form/Type/TranslationCollectionTypeTest.php new file mode 100644 index 0000000..feb30c8 --- /dev/null +++ b/Tests/Form/Type/TranslationCollectionTypeTest.php @@ -0,0 +1,158 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Tests\Form\Form\Type; + +use Propel\PropelBundle\Tests\Fixtures\Item; +use Propel\PropelBundle\Form\PropelExtension; +use Propel\PropelBundle\Tests\Fixtures\TranslatableItemI18n; +use Propel\PropelBundle\Tests\Fixtures\TranslatableItem; +use Symfony\Component\Form\Tests\Extension\Core\Type\TypeTestCase; + +class TranslationCollectionTypeTest extends TypeTestCase +{ + const TRANSLATION_CLASS = 'Propel\PropelBundle\Tests\Fixtures\TranslatableItem'; + const TRANSLATABLE_I18N_CLASS = 'Propel\PropelBundle\Tests\Fixtures\TranslatableItemI18n'; + const NON_TRANSLATION_CLASS = 'Propel\PropelBundle\Tests\Fixtures\Item'; + + protected function setUp() + { + parent::setUp(); + } + + protected function getExtensions() + { + return array_merge(parent::getExtensions(), array( + new PropelExtension(), + )); + } + + public function testTranslationsAdded() + { + $item = new TranslatableItem(); + $item->addTranslatableItemI18n(new TranslatableItemI18n(1, 'fr', 'val1')); + $item->addTranslatableItemI18n(new TranslatableItemI18n(2, 'en', 'val2')); + + $builder = $this->factory->createBuilder('form', null, array( + 'data_class' => self::TRANSLATION_CLASS + )); + + $builder->add('translatableItemI18ns', 'propel_translation_collection', array( + 'languages' => array('en', 'fr'), + 'options' => array( + 'data_class' => self::TRANSLATABLE_I18N_CLASS, + 'columns' => array('value', 'value2' => array('label' => 'Label', 'type' => 'textarea')) + ) + )); + $form = $builder->getForm(); + $form->setData($item); + $translations = $form->get('translatableItemI18ns'); + + $this->assertCount(2, $translations); + $this->assertInstanceOf('Symfony\Component\Form\Form', $translations['en']); + $this->assertInstanceOf('Symfony\Component\Form\Form', $translations['fr']); + + $this->assertInstanceOf(self::TRANSLATABLE_I18N_CLASS, $translations['en']->getData()); + $this->assertInstanceOf(self::TRANSLATABLE_I18N_CLASS, $translations['fr']->getData()); + + $this->assertEquals($item->getTranslation('en'), $translations['en']->getData()); + $this->assertEquals($item->getTranslation('fr'), $translations['fr']->getData()); + + $columnOptions = $translations['fr']->getConfig()->getOption('columns'); + $this->assertEquals('value', $columnOptions[0]); + $this->assertEquals('textarea', $columnOptions['value2']['type']); + $this->assertEquals('Label', $columnOptions['value2']['label']); + } + + public function testNotPresentTranslationsAdded() + { + $item = new TranslatableItem(); + + $this->assertCount(0, $item->getTranslatableItemI18ns()); + + $builder = $this->factory->createBuilder('form', null, array( + 'data_class' => self::TRANSLATION_CLASS + )); + $builder->add('translatableItemI18ns', 'propel_translation_collection', array( + 'languages' => array('en', 'fr'), + 'options' => array( + 'data_class' => self::TRANSLATABLE_I18N_CLASS, + 'columns' => array('value', 'value2' => array('label' => 'Label', 'type' => 'textarea')) + ) + )); + + $form = $builder->getForm(); + $form->setData($item); + + $this->assertCount(2, $item->getTranslatableItemI18ns()); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException + */ + public function testNoArrayGiven() + { + $item = new Item(null, 'val'); + + $builder = $this->factory->createBuilder('form', null, array( + 'data_class' => self::NON_TRANSLATION_CLASS + )); + $builder->add('value', 'propel_translation_collection', array( + 'languages' => array('en', 'fr'), + 'options' => array( + 'data_class' => self::TRANSLATABLE_I18N_CLASS, + 'columns' => array('value', 'value2' => array('label' => 'Label', 'type' => 'textarea')) + ) + )); + + $form = $builder->getForm(); + $form->setData($item); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\MissingOptionsException + */ + public function testNoDataClassAdded() + { + $this->factory->createNamed('itemI18ns', 'propel_translation_collection', null, array( + 'languages' => array('en', 'fr'), + 'options' => array( + 'columns' => array('value', 'value2') + ) + )); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\MissingOptionsException + */ + public function testNoLanguagesAdded() + { + $this->factory->createNamed('itemI18ns', 'propel_translation_collection', null, array( + 'options' => array( + 'data_class' => self::TRANSLATABLE_I18N_CLASS, + 'columns' => array('value', 'value2') + ) + )); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\MissingOptionsException + */ + public function testNoColumnsAdded() + { + $this->factory->createNamed('itemI18ns', 'propel_translation_collection', null, array( + 'languages' => array('en', 'fr'), + 'options' => array( + 'data_class' => self::TRANSLATABLE_I18N_CLASS + ) + )); + } +}