diff --git a/DataCollector/PropelDataCollector.php b/DataCollector/PropelDataCollector.php new file mode 100644 index 0000000..69d027a --- /dev/null +++ b/DataCollector/PropelDataCollector.php @@ -0,0 +1,147 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\DataCollector; + +use Propel\PropelBundle\Logger\PropelLogger; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\DataCollector\DataCollector; + +/** + * The PropelDataCollector collector class collects information. + * + * @author William Durand + */ +class PropelDataCollector extends DataCollector +{ + /** + * Propel logger. + * + * @var PropelLogger + */ + private $logger; + + /** + * Propel configuration. + * + * @var \PropelConfiguration + */ + protected $propelConfiguration; + + /** + * Constructor. + * + * @param PropelLogger $logger A Propel logger. + * @param \PropelConfiguration $propelConfiguration The Propel configuration object. + */ + public function __construct(PropelLogger $logger, \PropelConfiguration $propelConfiguration) + { + $this->logger = $logger; + $this->propelConfiguration = $propelConfiguration; + } + + /** + * {@inheritdoc} + */ + public function collect(Request $request, Response $response, \Exception $exception = null) + { + $this->data = array( + 'queries' => $this->buildQueries(), + 'querycount' => $this->countQueries(), + ); + } + + /** + * Returns the collector name. + * + * @return string + */ + public function getName() + { + return 'propel'; + } + + /** + * Returns the collected stats for all executed SQL queries. + * + * @return array + */ + public function getQueries() + { + return $this->data['queries']; + } + + /** + * Returns the query count. + * + * @return int + */ + public function getQueryCount() + { + return $this->data['querycount']; + } + + /** + * Returns the total time spent on running all queries. + * + * @return float + */ + public function getTime() + { + $time = 0; + foreach ($this->data['queries'] as $query) { + $time += (float) $query['time']; + } + + return $time; + } + + /** + * Computes the stats of all executed SQL queries. + * + * @return array + */ + private function buildQueries() + { + $queries = array(); + + $outerGlue = $this->propelConfiguration->getParameter('debugpdo.logging.outerglue', ' | '); + $innerGlue = $this->propelConfiguration->getParameter('debugpdo.logging.innerglue', ': '); + + foreach ($this->logger->getQueries() as $q) { + $parts = explode($outerGlue, $q, 4); + + $times = explode($innerGlue, $parts[0]); + $con = explode($innerGlue, $parts[2]); + $memories = explode($innerGlue, $parts[1]); + + $sql = trim($parts[3]); + $con = trim($con[1]); + $time = trim($times[1]); + $memory = trim($memories[1]); + + $queries[] = array('connection' => $con, 'sql' => $sql, 'time' => $time, 'memory' => $memory); + } + + return $queries; + } + + /** + * Returns the total count of SQL queries. + * + * @return int + */ + private function countQueries() + { + return count($this->logger->getQueries()); + } +} diff --git a/DependencyInjection/Security/UserProvider/PropelFactory.php b/DependencyInjection/Security/UserProvider/PropelFactory.php new file mode 100644 index 0000000..9797b46 --- /dev/null +++ b/DependencyInjection/Security/UserProvider/PropelFactory.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 Propel\PropelBundle\DependencyInjection\Security\UserProvider; + +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\DefinitionDecorator; + +/** + * PropelFactory creates services for Propel user provider. + * + * @author William Durand + */ +class PropelFactory implements UserProviderFactoryInterface +{ + private $key; + private $providerId; + + /** + * Constructor. + * + * @param string $key + * @param string $providerId + */ + public function __construct($key, $providerId) + { + $this->key = $key; + $this->providerId = $providerId; + } + + public function create(ContainerBuilder $container, $id, $config) + { + $container + ->setDefinition($id, new DefinitionDecorator($this->providerId)) + ->addArgument($config['class']) + ->addArgument($config['property']) + ; + } + + public function getKey() + { + return $this->key; + } + + public function addConfiguration(NodeDefinition $node) + { + $node + ->children() + ->scalarNode('class') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->scalarNode('property') + ->defaultNull() + ->end() + ->end() + ; + } +} diff --git a/Form/ChoiceList/ModelChoiceList.php b/Form/ChoiceList/ModelChoiceList.php new file mode 100644 index 0000000..f303e29 --- /dev/null +++ b/Form/ChoiceList/ModelChoiceList.php @@ -0,0 +1,512 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Form\ChoiceList; + +use Propel\PropelBundle\Form\Type\ModelType; +use Symfony\Component\Form\Exception\StringCastException; +use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList; +use Symfony\Component\Form\Extension\Core\DataTransformer\ChoicesToValuesTransformer; +use Symfony\Component\Form\Extension\Core\DataTransformer\ChoiceToValueTransformer; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; + +/** + * A choice list for object choices based on Propel model. + * + * @author William Durand + * @author Toni Uebernickel + */ +class ModelChoiceList extends ObjectChoiceList +{ + /** + * The fields of which the identifier of the underlying class consists. + * + * This property should only be accessed through identifier. + * + * @var array + */ + protected $identifier = array(); + + /** + * The query to retrieve the choices of this list. + * + * @var \ModelCriteria + */ + protected $query; + + /** + * The query to retrieve the preferred choices for this list. + * + * @var \ModelCriteria + */ + protected $preferredQuery; + + /** + * Whether the model objects have already been loaded. + * + * @var bool + */ + protected $loaded = false; + + /** + * Whether to use the identifier for index generation. + * + * @var bool + */ + private $identifierAsIndex = false; + + /** + * Constructor. + * + * @see ModelType How to use the preferred choices. + * + * @param string $class The FQCN of the model class to be loaded. + * @param string $labelPath A property path pointing to the property used for the choice labels. + * @param array $choices An optional array to use, rather than fetching the models. + * @param \ModelCriteria $queryObject The query to use retrieving model data from database. + * @param string $groupPath A property path pointing to the property used to group the choices. + * @param array|\ModelCriteria $preferred The preferred items of this choice. + * Either an array if $choices is given, + * or a \ModelCriteria to be merged with the $queryObject. + * @param PropertyAccessorInterface $propertyAccessor The reflection graph for reading property paths. + * @param string $useAsIdentifier a custom unique column (eg slug) to use instead of primary key. + * + * @throws MissingOptionsException In case the class parameter is empty. + * @throws InvalidOptionsException In case the query class is not found. + */ + public function __construct($class, $labelPath = null, $choices = null, $queryObject = null, $groupPath = null, $preferred = array(), PropertyAccessorInterface $propertyAccessor = null, $useAsIdentifier = null) + { + $this->class = $class; + + $queryClass = $this->class.'Query'; + if (!class_exists($queryClass)) { + if (empty($this->class)) { + throw new MissingOptionsException('The "class" parameter is empty, you should provide the model class'); + } + throw new InvalidOptionsException(sprintf('The query class "%s" is not found, you should provide the FQCN of the model class', $queryClass)); + } + + $query = new $queryClass(); + + $this->query = $queryObject ?: $query; + if ($useAsIdentifier) { + $this->identifier = array($this->query->getTableMap()->getColumn($useAsIdentifier)); + } else { + $this->identifier = $this->query->getTableMap()->getPrimaryKeys(); + } + + $this->loaded = is_array($choices) || $choices instanceof \Traversable; + + if ($preferred instanceof \ModelCriteria) { + $this->preferredQuery = $preferred->mergeWith($this->query); + } + + if (!$this->loaded) { + // Make sure the constraints of the parent constructor are + // fulfilled + $choices = array(); + $preferred = array(); + } + + if (1 === count($this->identifier) && $this->isScalar(current($this->identifier))) { + $this->identifierAsIndex = true; + } + + parent::__construct($choices, $labelPath, $preferred, $groupPath, null, $propertyAccessor); + } + + /** + * Returns the class name of the model. + * + * @return string + */ + public function getClass() + { + return $this->class; + } + + /** + * {@inheritdoc} + */ + public function getChoices() + { + $this->load(); + + return parent::getChoices(); + } + + /** + * {@inheritdoc} + */ + public function getValues() + { + $this->load(); + + return parent::getValues(); + } + + /** + * {@inheritdoc} + */ + public function getPreferredViews() + { + $this->load(); + + return parent::getPreferredViews(); + } + + /** + * {@inheritdoc} + */ + public function getRemainingViews() + { + $this->load(); + + return parent::getRemainingViews(); + } + + /** + * {@inheritdoc} + */ + public function getChoicesForValues(array $values) + { + if (empty($values)) { + return array(); + } + + /* + * This performance optimization reflects a common scenario: + * * A simple select of a model entry. + * * The choice option "expanded" is set to false. + * * The current request is the submission of the selected value. + * + * @see ChoicesToValuesTransformer::reverseTransform() + * @see ChoiceToValueTransformer::reverseTransform() + */ + if (!$this->loaded) { + if (1 === count($this->identifier)) { + $filterBy = 'filterBy'.current($this->identifier)->getPhpName(); + + // The initial query is cloned, so all additional filters are applied correctly. + $query = clone $this->query; + $result = (array) $query + ->$filterBy($values) + ->find(); + + // Preserve the keys as provided by the values. + $models = array(); + foreach ($values as $index => $value) { + foreach ($result as $eachModel) { + if ($value === $this->createValue($eachModel)) { + // Make sure to convert to the right format + $models[$index] = $this->fixChoice($eachModel); + + // If all values have been assigned, skip further loops. + unset($values[$index]); + if (0 === count($values)) { + break 2; + } + } + } + } + + return $models; + } + } + + $this->load(); + + return parent::getChoicesForValues($values); + } + + /** + * {@inheritdoc} + */ + public function getValuesForChoices(array $models) + { + if (empty($models)) { + return array(); + } + + if (!$this->loaded) { + /* + * This performance optimization assumes the validation of the respective values will be done by other means. + * + * It correlates with the performance optimization in {@link ModelChoiceList::getChoicesForValues()} + * as it won't load the actual entries from the database. + * + * @see ChoicesToValuesTransformer::transform() + * @see ChoiceToValueTransformer::transform() + */ + if (1 === count($this->identifier)) { + $values = array(); + foreach ($models as $index => $model) { + if ($model instanceof $this->class) { + // Make sure to convert to the right format + $values[$index] = $this->fixValue(current($this->getIdentifierValues($model))); + } + } + + return $values; + } + } + + $this->load(); + + $values = array(); + $availableValues = $this->getValues(); + + /* + * Overwriting default implementation. + * + * The two objects may represent the same entry in the database, + * but if they originated from different queries, there are not the same object within the code. + * + * This happens when using m:n relations with either sides model as data_class of the form. + * The choicelist will retrieve the list of available related models with a different query, resulting in different objects. + */ + $choices = $this->fixChoices($models); + foreach ($choices as $i => $givenChoice) { + if (null === $givenChoice) { + continue; + } + + foreach ($this->getChoices() as $j => $choice) { + if ($this->isEqual($choice, $givenChoice)) { + $values[$i] = $availableValues[$j]; + + // If all choices have been assigned, skip further loops. + unset($choices[$i]); + if (0 === count($choices)) { + break 2; + } + } + } + } + + return $values; + } + + /** + * {@inheritdoc} + * + * @deprecated since version 2.4, to be removed in 3.0. + */ + public function getIndicesForChoices(array $models) + { + trigger_error('The '.__METHOD__.' method is deprecated since version 2.4 and will be removed in 3.0.', E_USER_DEPRECATED); + + if (empty($models)) { + return array(); + } + + $this->load(); + + $indices = array(); + + /* + * Overwriting default implementation. + * + * The two objects may represent the same entry in the database, + * but if they originated from different queries, there are not the same object within the code. + * + * This happens when using m:n relations with either sides model as data_class of the form. + * The choice list will retrieve the list of available related models with a different query, resulting in different objects. + */ + $choices = $this->fixChoices($models); + foreach ($choices as $i => $givenChoice) { + if (null === $givenChoice) { + continue; + } + + foreach ($this->getChoices() as $j => $choice) { + if ($this->isEqual($choice, $givenChoice)) { + $indices[$i] = $j; + + // If all choices have been assigned, skip further loops. + unset($choices[$i]); + if (0 === count($choices)) { + break 2; + } + } + } + } + + return $indices; + } + + /** + * {@inheritdoc} + * + * @deprecated since version 2.4, to be removed in 3.0. + */ + public function getIndicesForValues(array $values) + { + trigger_error('The '.__METHOD__.' method is deprecated since version 2.4 and will be removed in 3.0.', E_USER_DEPRECATED); + + if (empty($values)) { + return array(); + } + + $this->load(); + + return parent::getIndicesForValues($values); + } + + /** + * Creates a new unique index for this model. + * + * If the model has a single-field identifier, this identifier is used. + * + * Otherwise a new integer is generated. + * + * @param mixed $model The choice to create an index for + * + * @return int|string A unique index containing only ASCII letters, + * digits and underscores. + */ + protected function createIndex($model) + { + if ($this->identifierAsIndex) { + return current($this->getIdentifierValues($model)); + } + + return parent::createIndex($model); + } + + /** + * Creates a new unique value for this model. + * + * If the model has a single-field identifier, this identifier is used. + * + * Otherwise a new integer is generated. + * + * @param mixed $model The choice to create a value for + * + * @return int|string A unique value without character limitations. + */ + protected function createValue($model) + { + if ($this->identifierAsIndex) { + return (string) current($this->getIdentifierValues($model)); + } + + return parent::createValue($model); + } + + /** + * Loads the complete choice list entries, once. + * + * If data has been loaded the choice list is initialized with the retrieved data. + */ + private function load() + { + if ($this->loaded) { + return; + } + + $models = (array) $this->query->find(); + + $preferred = array(); + if ($this->preferredQuery instanceof \ModelCriteria) { + $preferred = (array) $this->preferredQuery->find(); + } + + try { + // The second parameter $labels is ignored by ObjectChoiceList + parent::initialize($models, array(), $preferred); + + $this->loaded = true; + } catch (StringCastException $e) { + throw new StringCastException(str_replace('argument $labelPath', 'option "property"', $e->getMessage()), null, $e); + } + } + + /** + * Returns the values of the identifier fields of a model. + * + * Propel must know about this model, that is, the model must already + * be persisted or added to the idmodel map before. Otherwise an + * exception is thrown. + * + * @param object $model The model for which to get the identifier + * + * @return array + */ + private function getIdentifierValues($model) + { + if (!$model instanceof $this->class) { + return array(); + } + + if (1 === count($this->identifier) && current($this->identifier) instanceof \ColumnMap) { + $phpName = current($this->identifier)->getPhpName(); + + if (method_exists($model, 'get'.$phpName)) { + return array($model->{'get'.$phpName}()); + } + } + + if ($model instanceof \Persistent) { + return array($model->getPrimaryKey()); + } + + // readonly="true" models do not implement \Persistent. + if ($model instanceof \BaseObject && method_exists($model, 'getPrimaryKey')) { + return array($model->getPrimaryKey()); + } + + if (!method_exists($model, 'getPrimaryKeys')) { + return array(); + } + + return $model->getPrimaryKeys(); + } + + /** + * Whether this column contains scalar values (to be used as indices). + * + * @param \ColumnMap $column + * + * @return bool + */ + private function isScalar(\ColumnMap $column) + { + return in_array($column->getPdoType(), array( + \PDO::PARAM_BOOL, + \PDO::PARAM_INT, + \PDO::PARAM_STR, + )); + } + + /** + * Check the given choices for equality. + * + * @param mixed $choice + * @param mixed $givenChoice + * + * @return bool + */ + private function isEqual($choice, $givenChoice) + { + if ($choice === $givenChoice) { + return true; + } + + if ($this->getIdentifierValues($choice) === $this->getIdentifierValues($givenChoice)) { + return true; + } + + return false; + } +} diff --git a/Form/DataTransformer/CollectionToArrayTransformer.php b/Form/DataTransformer/CollectionToArrayTransformer.php new file mode 100644 index 0000000..fbb7d95 --- /dev/null +++ b/Form/DataTransformer/CollectionToArrayTransformer.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 Propel\PropelBundle\Form\DataTransformer; + +use PropelObjectCollection; +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; + +/** + * CollectionToArrayTransformer class. + * + * @author William Durand + * @author Pierre-Yves Lebecq + */ +class CollectionToArrayTransformer implements DataTransformerInterface +{ + public function transform($collection) + { + if (null === $collection) { + return array(); + } + + if (!$collection instanceof PropelObjectCollection) { + throw new TransformationFailedException('Expected a \PropelObjectCollection.'); + } + + return $collection->getData(); + } + + public function reverseTransform($array) + { + $collection = new PropelObjectCollection(); + + if ('' === $array || null === $array) { + return $collection; + } + + if (!is_array($array)) { + throw new TransformationFailedException('Expected an array.'); + } + + $collection->setData($array); + + return $collection; + } +} diff --git a/Form/EventListener/TranslationCollectionFormListener.php b/Form/EventListener/TranslationCollectionFormListener.php new file mode 100644 index 0000000..bdd5f1a --- /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\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\Exception\UnexpectedTypeException; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; + +/** + * listener class for propel1_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..d686923 --- /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 propel1_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..48ee3e0 --- /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::createPropertyAccessor()), + new Type\TranslationCollectionType(), + new Type\TranslationType(), + ); + } + + protected function loadTypeGuesser() + { + return new PropelTypeGuesser(); + } +} diff --git a/Form/PropelTypeGuesser.php b/Form/PropelTypeGuesser.php new file mode 100644 index 0000000..693c8b3 --- /dev/null +++ b/Form/PropelTypeGuesser.php @@ -0,0 +1,184 @@ + + * + * 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\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->isText()) { + 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); + } + + if ($table && $table->hasColumnByInsensitiveCase($property)) { + return $this->cache[$class.'::'.$property] = $table->getColumnByInsensitiveCase($property); + } + } +} diff --git a/Form/Type/ModelType.php b/Form/Type/ModelType.php new file mode 100644 index 0000000..1a3f248 --- /dev/null +++ b/Form/Type/ModelType.php @@ -0,0 +1,128 @@ + + * + * 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 Propel\PropelBundle\Form\ChoiceList\ModelChoiceList; +use Propel\PropelBundle\Form\DataTransformer\CollectionToArrayTransformer; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; + +/** + * ModelType class. + * + * @author William Durand + * @author Toni Uebernickel + * + * Example using the preferred_choices option. + * + * + * public function buildForm(FormBuilderInterface $builder, array $options) + * { + * $builder + * ->add('product', 'model', array( + * 'class' => 'Model\Product', + * 'query' => ProductQuery::create() + * ->filterIsActive(true) + * ->useI18nQuery($options['locale']) + * ->orderByName() + * ->endUse() + * , + * 'preferred_choices' => ProductQuery::create() + * ->filterByIsTopProduct(true) + * , + * )) + * ; + * } + * + */ +class ModelType extends AbstractType +{ + /** + * @var PropertyAccessorInterface + */ + private $propertyAccessor; + + /** + * Constructor. + * + * @param PropertyAccessorInterface|null $propertyAccessor + */ + public function __construct(PropertyAccessorInterface $propertyAccessor = null) + { + $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + } + + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + if ($options['multiple']) { + $builder->addViewTransformer(new CollectionToArrayTransformer(), true); + } + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $propertyAccessor = $this->propertyAccessor; + + $choiceList = function (Options $options) use ($propertyAccessor) { + return new ModelChoiceList( + $options['class'], + $options['property'], + $options['choices'], + $options['query'], + $options['group_by'], + $options['preferred_choices'], + $propertyAccessor, + $options['index_property'] + ); + }; + + $resolver->setDefaults(array( + 'template' => 'choice', + 'multiple' => false, + 'expanded' => false, + 'class' => null, + 'property' => null, + 'query' => null, + 'choices' => null, + 'choice_list' => $choiceList, + 'group_by' => null, + 'by_reference' => false, + 'index_property' => null, + )); + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + return 'choice'; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'model'; + } +} diff --git a/Form/Type/TranslationCollectionType.php b/Form/Type/TranslationCollectionType.php new file mode 100644 index 0000000..7863abf --- /dev/null +++ b/Form/Type/TranslationCollectionType.php @@ -0,0 +1,78 @@ + + * + * 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 Propel\PropelBundle\Form\EventListener\TranslationCollectionFormListener; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * 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); + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + return 'collection'; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'propel1_translation_collection'; + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setRequired(array( + 'languages', + )); + + $resolver->setDefaults(array( + 'type' => 'propel1_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..ef8178b --- /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 Propel\PropelBundle\Form\EventListener\TranslationFormListener; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * 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 'propel1_translation'; + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setRequired(array( + 'data_class', + 'columns', + )); + } +} diff --git a/Logger/PropelLogger.php b/Logger/PropelLogger.php new file mode 100644 index 0000000..939e5f3 --- /dev/null +++ b/Logger/PropelLogger.php @@ -0,0 +1,177 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Logger; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Stopwatch\Stopwatch; + +/** + * PropelLogger. + * + * @author Fabien Potencier + * @author William Durand + */ +class PropelLogger implements \BasicLogger +{ + /** + * @var LoggerInterface + */ + protected $logger; + + /** + * @var array + */ + protected $queries = array(); + + /** + * @var Stopwatch + */ + protected $stopwatch; + + /** + * @var bool + */ + private $isPrepared = false; + + /** + * Constructor. + * + * @param LoggerInterface $logger A LoggerInterface instance + * @param Stopwatch $stopwatch A Stopwatch instance + */ + public function __construct(LoggerInterface $logger = null, Stopwatch $stopwatch = null) + { + $this->logger = $logger; + $this->stopwatch = $stopwatch; + } + + /** + * {@inheritdoc} + */ + public function alert($message) + { + $this->log($message, 'alert'); + } + + /** + * {@inheritdoc} + */ + public function crit($message) + { + $this->log($message, 'crit'); + } + + /** + * {@inheritdoc} + */ + public function err($message) + { + $this->log($message, 'err'); + } + + /** + * {@inheritdoc} + */ + public function warning($message) + { + $this->log($message, 'warning'); + } + + /** + * {@inheritdoc} + */ + public function notice($message) + { + $this->log($message, 'notice'); + } + + /** + * {@inheritdoc} + */ + public function info($message) + { + $this->log($message, 'info'); + } + + /** + * {@inheritdoc} + */ + public function debug($message) + { + $add = true; + + if (null !== $this->stopwatch) { + $trace = debug_backtrace(); + $method = $trace[2]['args'][2]; + + $watch = 'Propel Query '.(count($this->queries) + 1); + if ('PropelPDO::prepare' === $method) { + $this->isPrepared = true; + $this->stopwatch->start($watch, 'propel'); + + $add = false; + } elseif ($this->isPrepared) { + $this->isPrepared = false; + $this->stopwatch->stop($watch); + } + } + + if ($add) { + $this->queries[] = $message; + $this->log($message, 'debug'); + } + } + + /** + * {@inheritdoc} + */ + public function log($message, $severity = null) + { + if (null !== $this->logger) { + $message = is_string($message) ? $message : var_export($message, true); + + switch ($severity) { + case 'alert': + $this->logger->alert($message); + break; + case 'crit': + $this->logger->critical($message); + break; + case 'err': + $this->logger->error($message); + break; + case 'warning': + $this->logger->warning($message); + break; + case 'notice': + $this->logger->notice($message); + break; + case 'info': + $this->logger->info($message); + break; + case 'debug': + default: + $this->logger->debug($message); + } + } + } + + /** + * Returns queries. + * + * @return array Queries + */ + public function getQueries() + { + return $this->queries; + } +} diff --git a/PropelBundle.php b/PropelBundle.php index b177e3e..e0b6f48 100644 --- a/PropelBundle.php +++ b/PropelBundle.php @@ -7,15 +7,14 @@ * * @license MIT License */ - namespace Propel\PropelBundle; -use Symfony\Bridge\Propel1\DependencyInjection\Security\UserProvider\PropelFactory; -use Symfony\Component\HttpKernel\Bundle\Bundle; +use Propel\PropelBundle\DependencyInjection\Security\UserProvider\PropelFactory; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Bundle\Bundle; /** - * PropelBundle + * PropelBundle. * * @author William DURAND */ @@ -51,7 +50,7 @@ class PropelBundle extends Bundle ), false); $config->setParameter('debugpdo.logging.details', array( 'time' => array('enabled' => true), - 'mem' => array('enabled' => true), + 'mem' => array('enabled' => true), 'connection' => array('enabled' => true), )); diff --git a/Resources/config/propel.xml b/Resources/config/propel.xml index f2e56a0..4ae8e93 100644 --- a/Resources/config/propel.xml +++ b/Resources/config/propel.xml @@ -7,14 +7,14 @@ default PropelConfiguration - Symfony\Bridge\Propel1\Logger\PropelLogger - Symfony\Bridge\Propel1\DataCollector\PropelDataCollector + Propel\PropelBundle\Logger\PropelLogger + Propel\PropelBundle\DataCollector\PropelDataCollector Propel\PropelBundle\DependencyInjection\Properties - Symfony\Bridge\Propel1\Form\Type\ModelType + Propel\PropelBundle\Form\Type\ModelType Propel\PropelBundle\Twig\Extension\SyntaxExtension - Symfony\Bridge\Propel1\Form\PropelTypeGuesser + Propel\PropelBundle\Form\PropelTypeGuesser Propel\PropelBundle\Security\Acl\AuditableAclProvider - Symfony\Bridge\Propel1\Security\User\PropelUserProvider + Propel\PropelBundle\Security\User\PropelUserProvider diff --git a/Security/User/PropelUserProvider.php b/Security/User/PropelUserProvider.php new file mode 100644 index 0000000..678cc10 --- /dev/null +++ b/Security/User/PropelUserProvider.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Security\User; + +use Symfony\Component\Security\Core\Exception\UnsupportedUserException; +use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\UserProviderInterface; + +/** + * Provides easy to use provisioning for Propel model users. + * + * @author William DURAND + */ +class PropelUserProvider implements UserProviderInterface +{ + /** + * A Model class name. + * + * @var string + */ + protected $class; + + /** + * A Query class name. + * + * @var string + */ + protected $queryClass; + + /** + * A property to use to retrieve the user. + * + * @var string + */ + protected $property; + + /** + * Default constructor. + * + * @param string $class The User model class. + * @param string|null $property The property to use to retrieve a user. + */ + public function __construct($class, $property = null) + { + $this->class = $class; + $this->queryClass = $class.'Query'; + $this->property = $property; + } + + /** + * {@inheritdoc} + */ + public function loadUserByUsername($username) + { + $queryClass = $this->queryClass; + $query = $queryClass::create(); + + if (null !== $this->property) { + $filter = 'filterBy'.ucfirst($this->property); + $query->$filter($username); + } else { + $query->filterByUsername($username); + } + + if (null === $user = $query->findOne()) { + throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username)); + } + + return $user; + } + + /** + * {@inheritdoc} + */ + public function refreshUser(UserInterface $user) + { + if (!$user instanceof $this->class) { + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user))); + } + + $queryClass = $this->queryClass; + + return $queryClass::create()->findPk($user->getPrimaryKey()); + } + + /** + * {@inheritdoc} + */ + public function supportsClass($class) + { + return $class === $this->class; + } +} diff --git a/Tests/DataCollector/PropelDataCollectorTest.php b/Tests/DataCollector/PropelDataCollectorTest.php new file mode 100644 index 0000000..fcf15af --- /dev/null +++ b/Tests/DataCollector/PropelDataCollectorTest.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Tests\DataCollector; + +use Propel\PropelBundle\DataCollector\PropelDataCollector; +use Propel\PropelBundle\Tests\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +class PropelDataCollectorTest extends TestCase +{ + public function testCollectWithoutData() + { + $c = $this->createCollector(array()); + $c->collect(new Request(), new Response()); + + $this->assertEquals(array(), $c->getQueries()); + $this->assertEquals(0, $c->getQueryCount()); + } + + public function testCollectWithData() + { + $queries = array( + "time: 0.000 sec | mem: 1.4 MB | connection: default | SET NAMES 'utf8'", + ); + + $c = $this->createCollector($queries); + $c->collect(new Request(), new Response()); + + $this->assertEquals(array( + array( + 'sql' => "SET NAMES 'utf8'", + 'time' => '0.000 sec', + 'connection' => 'default', + 'memory' => '1.4 MB', + ), + ), $c->getQueries()); + $this->assertEquals(1, $c->getQueryCount()); + } + + public function testCollectWithMultipleData() + { + $queries = array( + "time: 0.000 sec | mem: 1.4 MB | connection: default | SET NAMES 'utf8'", + 'time: 0.012 sec | mem: 2.4 MB | connection: default | SELECT tags.NAME, image.FILENAME FROM tags LEFT JOIN image ON tags.IMAGEID = image.ID WHERE image.ID = 12', + "time: 0.012 sec | mem: 2.4 MB | connection: default | INSERT INTO `table` (`some_array`) VALUES ('| 1 | 2 | 3 |')", + ); + + $c = $this->createCollector($queries); + $c->collect(new Request(), new Response()); + + $this->assertEquals(array( + array( + 'sql' => "SET NAMES 'utf8'", + 'time' => '0.000 sec', + 'connection' => 'default', + 'memory' => '1.4 MB', + ), + array( + 'sql' => 'SELECT tags.NAME, image.FILENAME FROM tags LEFT JOIN image ON tags.IMAGEID = image.ID WHERE image.ID = 12', + 'time' => '0.012 sec', + 'connection' => 'default', + 'memory' => '2.4 MB', + ), + array( + 'sql' => "INSERT INTO `table` (`some_array`) VALUES ('| 1 | 2 | 3 |')", + 'time' => '0.012 sec', + 'connection' => 'default', + 'memory' => '2.4 MB', + ), + ), $c->getQueries()); + $this->assertEquals(3, $c->getQueryCount()); + $this->assertEquals(0.024, $c->getTime()); + } + + private function createCollector($queries) + { + $config = $this->getMock('\PropelConfiguration'); + + $config + ->expects($this->any()) + ->method('getParameter') + ->will($this->returnArgument(1)) + ; + + $logger = $this->getMock('\Propel\PropelBundle\Logger\PropelLogger'); + $logger + ->expects($this->any()) + ->method('getQueries') + ->will($this->returnValue($queries)) + ; + + return new PropelDataCollector($logger, $config); + } +} diff --git a/Tests/Fixtures/Column.php b/Tests/Fixtures/Column.php new file mode 100644 index 0000000..bc89d73 --- /dev/null +++ b/Tests/Fixtures/Column.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\Tests\Fixtures; + +class Column extends \ColumnMap +{ + private $name; + protected $type; + + public function __construct($name, $type) + { + $this->name = $name; + $this->type = $type; + $this->phpName = ucfirst($name); + } + + public function isText() + { + 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->isText() ? 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..755dcf9 --- /dev/null +++ b/Tests/Fixtures/Item.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Tests\Fixtures; + +class Item implements \Persistent +{ + private $id; + private $value; + private $groupName; + private $price; + + private $slug; + + public function __construct($id = null, $value = null, $groupName = null, $price = null, $slug = null) + { + $this->id = $id; + $this->value = $value; + $this->groupName = $groupName; + $this->price = $price; + $this->slug = $slug; + } + + 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 getSlug() + { + return $this->slug; + } + + 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(\PropelPDO $con = null) + { + } + + public function save(\PropelPDO $con = null) + { + } +} diff --git a/Tests/Fixtures/ItemQuery.php b/Tests/Fixtures/ItemQuery.php new file mode 100644 index 0000000..5058be9 --- /dev/null +++ b/Tests/Fixtures/ItemQuery.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Tests\Fixtures; + +class ItemQuery +{ + private $map = array( + 'id' => \PropelColumnTypes::INTEGER, + 'value' => \PropelColumnTypes::VARCHAR, + 'price' => \PropelColumnTypes::FLOAT, + 'is_active' => \PropelColumnTypes::BOOLEAN, + 'slug' => \PropelColumnTypes::VARCHAR, + 'enabled' => \PropelColumnTypes::BOOLEAN_EMU, + 'updated_at' => \PropelColumnTypes::TIMESTAMP, + ); + + private $caseInsensitiveMap = array( + 'isactive' => 'is_active', + 'updatedat' => 'updated_at', + ); + + public static $result = array(); + + public function find() + { + return self::$result; + } + + public function filterById($id) + { + return $this; + } + + 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'); + $cm->setPhpName('Id'); + + 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]); + } + } + + /** + * Method from the TableMap API. + */ + public function hasColumnByInsensitiveCase($column) + { + $column = strtolower($column); + + return in_array($column, array_keys($this->caseInsensitiveMap)); + } + + /** + * Method from the TableMap API. + */ + public function getColumnByInsensitiveCase($column) + { + $column = strtolower($column); + + if (isset($this->caseInsensitiveMap[$column])) { + return $this->getColumn($this->caseInsensitiveMap[$column]); + } + } + + /** + * 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/Fixtures/ReadOnlyItem.php b/Tests/Fixtures/ReadOnlyItem.php new file mode 100644 index 0000000..ded3128 --- /dev/null +++ b/Tests/Fixtures/ReadOnlyItem.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Tests\Fixtures; + +class ReadOnlyItem extends \BaseObject +{ + public function getName() + { + return 'Marvin'; + } + + public function getPrimaryKey() + { + return 42; + } +} diff --git a/Tests/Fixtures/ReadOnlyItemQuery.php b/Tests/Fixtures/ReadOnlyItemQuery.php new file mode 100644 index 0000000..15028b5 --- /dev/null +++ b/Tests/Fixtures/ReadOnlyItemQuery.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Tests\Fixtures; + +class ReadOnlyItemQuery +{ + 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); + } +} diff --git a/Tests/Fixtures/TranslatableItem.php b/Tests/Fixtures/TranslatableItem.php new file mode 100644 index 0000000..d2f0d6f --- /dev/null +++ b/Tests/Fixtures/TranslatableItem.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Tests\Fixtures; + +class TranslatableItem implements \Persistent +{ + 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(\PropelPDO $con = null) + { + } + + public function save(\PropelPDO $con = null) + { + } + + public function getTranslation($locale = 'de', \PropelPDO $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..cbd0f95 --- /dev/null +++ b/Tests/Fixtures/TranslatableItemI18n.php @@ -0,0 +1,128 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Tests\Fixtures; + +class TranslatableItemI18n implements \Persistent +{ + 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(\PropelPDO $con = null) + { + } + + public function save(\PropelPDO $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/ChoiceList/CompatModelChoiceListTest.php b/Tests/Form/ChoiceList/CompatModelChoiceListTest.php new file mode 100644 index 0000000..d164831 --- /dev/null +++ b/Tests/Form/ChoiceList/CompatModelChoiceListTest.php @@ -0,0 +1,118 @@ +query + ->expects($this->once()) + ->method('filterById') + ->with(array(1, 2)) + ->will($this->returnSelf()) + ; + + ItemQuery::$result = array( + $this->item2, + $this->item1, + ); + + parent::testGetChoicesForValues(); + } + + protected function setUp() + { + $this->query = $this->getMock('Propel\PropelBundle\Tests\Fixtures\ItemQuery', array( + 'filterById', + ), array(), '', true, true, true, false, true); + + $this->query + ->expects($this->any()) + ->method('filterById') + ->with($this->anything()) + ->will($this->returnSelf()) + ; + + $this->createItems(); + + ItemQuery::$result = array( + $this->item1, + $this->item2, + $this->item3, + $this->item4, + ); + + parent::setUp(); + } + + protected function createItems() + { + $this->item1 = new Item(1, 'Foo'); + $this->item2 = new Item(2, 'Bar'); + $this->item3 = new Item(3, 'Baz'); + $this->item4 = new Item(4, 'Cuz'); + } + + protected function createChoiceList() + { + return new ModelChoiceList(self::ITEM_CLASS, 'value', null, $this->query); + } + + protected function getChoices() + { + return array( + 1 => $this->item1, + 2 => $this->item2, + 3 => $this->item3, + 4 => $this->item4, + ); + } + + protected function getLabels() + { + return array( + 1 => 'Foo', + 2 => 'Bar', + 3 => 'Baz', + 4 => 'Cuz', + ); + } + + protected function getValues() + { + return array( + 1 => '1', + 2 => '2', + 3 => '3', + 4 => '4', + ); + } + + protected function getIndices() + { + return array( + 1, + 2, + 3, + 4, + ); + } +} diff --git a/Tests/Form/ChoiceList/ModelChoiceListTest.php b/Tests/Form/ChoiceList/ModelChoiceListTest.php new file mode 100644 index 0000000..350b5c5 --- /dev/null +++ b/Tests/Form/ChoiceList/ModelChoiceListTest.php @@ -0,0 +1,316 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Tests\Form\ChoiceList; + +use Propel\PropelBundle\Form\ChoiceList\ModelChoiceList; +use Propel\PropelBundle\Tests\Fixtures\Item; +use Propel\PropelBundle\Tests\Fixtures\ItemQuery; +use Propel\PropelBundle\Tests\Fixtures\ReadOnlyItem; +use Propel\PropelBundle\Tests\TestCase; +use Symfony\Component\Form\Extension\Core\View\ChoiceView; + +class ModelChoiceListTest extends TestCase +{ + const ITEM_CLASS = '\Propel\PropelBundle\Tests\Fixtures\Item'; + + protected function setUp() + { + ItemQuery::$result = array(); + } + + public function testEmptyChoicesReturnsEmpty() + { + $choiceList = new ModelChoiceList( + self::ITEM_CLASS, + 'value', + array() + ); + + $this->assertSame(array(), $choiceList->getChoices()); + } + + public function testReadOnlyIsValidChoice() + { + $item = new ReadOnlyItem(); + $choiceList = new ModelChoiceList( + '\Propel\PropelBundle\Tests\Fixtures\ReadOnlyItem', + 'name', + array( + $item, + ) + ); + + $this->assertSame(array(42 => $item), $choiceList->getChoices()); + } + + public function testFlattenedChoices() + { + $item1 = new Item(1, 'Foo'); + $item2 = new Item(2, 'Bar'); + + $choiceList = new ModelChoiceList( + self::ITEM_CLASS, + 'value', + array( + $item1, + $item2, + ) + ); + + $this->assertSame(array(1 => $item1, 2 => $item2), $choiceList->getChoices()); + } + + public function testFlattenedPreferredChoices() + { + $item1 = new Item(1, 'Foo'); + $item2 = new Item(2, 'Bar'); + + $choiceList = new ModelChoiceList( + self::ITEM_CLASS, + 'value', + array( + $item1, + $item2, + ), + null, + null, + array( + $item1, + ) + ); + + $this->assertSame(array(1 => $item1, 2 => $item2), $choiceList->getChoices()); + $this->assertEquals(array(1 => new ChoiceView($item1, '1', 'Foo')), $choiceList->getPreferredViews()); + } + + public function testNestedChoices() + { + $item1 = new Item(1, 'Foo'); + $item2 = new Item(2, 'Bar'); + + $choiceList = new ModelChoiceList( + self::ITEM_CLASS, + 'value', + array( + 'group1' => array($item1), + 'group2' => array($item2), + ) + ); + + $this->assertSame(array(1 => $item1, 2 => $item2), $choiceList->getChoices()); + $this->assertEquals(array( + 'group1' => array(1 => new ChoiceView($item1, '1', 'Foo')), + 'group2' => array(2 => new ChoiceView($item2, '2', 'Bar')), + ), $choiceList->getRemainingViews()); + } + + public function testGroupBySupportsString() + { + $item1 = new Item(1, 'Foo', 'Group1'); + $item2 = new Item(2, 'Bar', 'Group1'); + $item3 = new Item(3, 'Baz', 'Group2'); + $item4 = new Item(4, 'Boo!', null); + + $choiceList = new ModelChoiceList( + self::ITEM_CLASS, + 'value', + array( + $item1, + $item2, + $item3, + $item4, + ), + null, + 'groupName' + ); + + $this->assertEquals(array(1 => $item1, 2 => $item2, 3 => $item3, 4 => $item4), $choiceList->getChoices()); + $this->assertEquals(array( + 'Group1' => array(1 => new ChoiceView($item1, '1', 'Foo'), 2 => new ChoiceView($item2, '2', 'Bar')), + 'Group2' => array(3 => new ChoiceView($item3, '3', 'Baz')), + 4 => new ChoiceView($item4, '4', 'Boo!'), + ), $choiceList->getRemainingViews()); + } + + public function testGroupByInvalidPropertyPathReturnsFlatChoices() + { + $item1 = new Item(1, 'Foo', 'Group1'); + $item2 = new Item(2, 'Bar', 'Group1'); + + $choiceList = new ModelChoiceList( + self::ITEM_CLASS, + 'value', + array( + $item1, + $item2, + ), + null, + 'child.that.does.not.exist' + ); + + $this->assertEquals(array( + 1 => $item1, + 2 => $item2, + ), $choiceList->getChoices()); + } + + public function testGetValuesForChoices() + { + $item1 = new Item(1, 'Foo'); + $item2 = new Item(2, 'Bar'); + + ItemQuery::$result = array( + $item1, + $item2, + ); + + $choiceList = new ModelChoiceList( + self::ITEM_CLASS, + 'value', + null, + null, + null, + null + ); + + $this->assertEquals(array(1, 2), $choiceList->getValuesForChoices(array($item1, $item2))); + } + + public function testDifferentEqualObjectsAreChoosen() + { + $item = new Item(1, 'Foo'); + + ItemQuery::$result = array( + $item, + ); + + $choiceList = new ModelChoiceList( + self::ITEM_CLASS, + 'value', + array($item) + ); + + $choosenItem = new Item(1, 'Foo'); + + $this->assertEquals(array('1'), $choiceList->getValuesForChoices(array($choosenItem))); + } + + public function testLegacygetIndicesForChoices() + { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + + $item1 = new Item(1, 'Foo'); + $item2 = new Item(2, 'Bar'); + + ItemQuery::$result = array( + $item1, + $item2, + ); + + $choiceList = new ModelChoiceList( + self::ITEM_CLASS, + 'value', + null, + null, + null, + null + ); + + $this->assertEquals(array(1, 2), $choiceList->getIndicesForChoices(array($item1, $item2))); + } + + public function testLegacyDifferentEqualObjectsAreChoosen() + { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + + $item = new Item(1, 'Foo'); + + ItemQuery::$result = array( + $item, + ); + + $choiceList = new ModelChoiceList( + self::ITEM_CLASS, + 'value', + array($item) + ); + + $choosenItem = new Item(1, 'Foo'); + + $this->assertEquals(array(1), $choiceList->getIndicesForChoices(array($choosenItem))); + } + + public function testLegacyGetIndicesForNullChoices() + { + $this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED); + + $item = new Item(1, 'Foo'); + $choiceList = new ModelChoiceList( + self::ITEM_CLASS, + 'value', + array($item) + ); + + $this->assertEquals(array(), $choiceList->getIndicesForChoices(array(null))); + } + + public function testDontAllowInvalidChoiceValues() + { + $item = new Item(1, 'Foo'); + $choiceList = new ModelChoiceList( + self::ITEM_CLASS, + 'value', + array($item) + ); + + $this->assertEquals(array(), $choiceList->getValuesForChoices(array(new Item(2, 'Bar')))); + $this->assertEquals(array(), $choiceList->getChoicesForValues(array(2))); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\MissingOptionsException + */ + public function testEmptyClass() + { + new ModelChoiceList(''); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testInvalidClass() + { + new ModelChoiceList('Foo\Bar\DoesNotExistClass'); + } + + public function testCustomIdentifier() + { + $item1 = new Item(1, 'Foo', null, null, 'slug'); + $item2 = new Item(2, 'Bar', null, null, 'slug2'); + + $choiceList = new ModelChoiceList( + self::ITEM_CLASS, + 'value', + array( + $item1, + $item2, + ), + null, + null, + array(), + null, + 'slug' + ); + + $this->assertSame(array('slug' => $item1, 'slug2' => $item2), $choiceList->getChoices()); + } +} diff --git a/Tests/Form/DataTransformer/CollectionToArrayTransformerTest.php b/Tests/Form/DataTransformer/CollectionToArrayTransformerTest.php new file mode 100644 index 0000000..06a7123 --- /dev/null +++ b/Tests/Form/DataTransformer/CollectionToArrayTransformerTest.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Tests\Form\DataTransformer; + +use Propel\PropelBundle\Form\DataTransformer\CollectionToArrayTransformer; +use Propel\PropelBundle\Tests\TestCase; + +class CollectionToArrayTransformerTest extends TestCase +{ + private $transformer; + + protected function setUp() + { + $this->transformer = new CollectionToArrayTransformer(); + } + + public function testTransform() + { + $result = $this->transformer->transform(new \PropelObjectCollection()); + + $this->assertTrue(is_array($result)); + $this->assertCount(0, $result); + } + + public function testTransformWithNull() + { + $result = $this->transformer->transform(null); + + $this->assertTrue(is_array($result)); + $this->assertCount(0, $result); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException + */ + public function testTransformThrowsExceptionIfNotPropelObjectCollection() + { + $this->transformer->transform(new DummyObject()); + } + + public function testTransformWithData() + { + $coll = new \PropelObjectCollection(); + $coll->setData(array('foo', 'bar')); + + $result = $this->transformer->transform($coll); + + $this->assertTrue(is_array($result)); + $this->assertCount(2, $result); + $this->assertEquals('foo', $result[0]); + $this->assertEquals('bar', $result[1]); + } + + public function testReverseTransformWithNull() + { + $result = $this->transformer->reverseTransform(null); + + $this->assertInstanceOf('\PropelObjectCollection', $result); + $this->assertCount(0, $result->getData()); + } + + public function testReverseTransformWithEmptyString() + { + $result = $this->transformer->reverseTransform(''); + + $this->assertInstanceOf('\PropelObjectCollection', $result); + $this->assertCount(0, $result->getData()); + } + + /** + * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException + */ + public function testReverseTransformThrowsExceptionIfNotArray() + { + $this->transformer->reverseTransform(new DummyObject()); + } + + public function testReverseTransformWithData() + { + $inputData = array('foo', 'bar'); + + $result = $this->transformer->reverseTransform($inputData); + $data = $result->getData(); + + $this->assertInstanceOf('\PropelObjectCollection', $result); + + $this->assertTrue(is_array($data)); + $this->assertCount(2, $data); + $this->assertEquals('foo', $data[0]); + $this->assertEquals('bar', $data[1]); + $this->assertsame($inputData, $data); + } +} + +class DummyObject +{ +} diff --git a/Tests/Form/PropelTypeGuesserTest.php b/Tests/Form/PropelTypeGuesserTest.php new file mode 100644 index 0000000..a801ee6 --- /dev/null +++ b/Tests/Form/PropelTypeGuesserTest.php @@ -0,0 +1,136 @@ + + * + * 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; + + protected function setUp() + { + $this->guesser = new PropelTypeGuesser(); + } + + protected function tearDown() + { + $this->guesser = null; + } + + 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('isActive', 'checkbox', Guess::HIGH_CONFIDENCE), + array('updatedAt', '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), + ); + } +} diff --git a/Tests/Form/Type/TranslationCollectionTypeTest.php b/Tests/Form/Type/TranslationCollectionTypeTest.php new file mode 100644 index 0000000..2489b05 --- /dev/null +++ b/Tests/Form/Type/TranslationCollectionTypeTest.php @@ -0,0 +1,151 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Propel\PropelBundle\Tests\Form\Type; + +use Propel\PropelBundle\Form\PropelExtension; +use Propel\PropelBundle\Tests\Fixtures\Item; +use Propel\PropelBundle\Tests\Fixtures\TranslatableItem; +use Propel\PropelBundle\Tests\Fixtures\TranslatableItemI18n; +use Symfony\Component\Form\Test\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 getExtensions() + { + return 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', 'propel1_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', 'propel1_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', 'propel1_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', 'propel1_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', 'propel1_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', 'propel1_translation_collection', null, array( + 'languages' => array('en', 'fr'), + 'options' => array( + 'data_class' => self::TRANSLATABLE_I18N_CLASS, + ), + )); + } +} diff --git a/Tests/Security/User/PropelUserProviderTest.php b/Tests/Security/User/PropelUserProviderTest.php index 1e64662..26759a9 100644 --- a/Tests/Security/User/PropelUserProviderTest.php +++ b/Tests/Security/User/PropelUserProviderTest.php @@ -7,12 +7,11 @@ * * @license MIT License */ - namespace Propel\PropelBundle\Tests\Security\User; +use Propel\PropelBundle\Security\User\PropelUserProvider; use Propel\PropelBundle\Tests\Fixtures\Model\User; use Propel\PropelBundle\Tests\TestCase; -use Symfony\Bridge\Propel1\Security\User\PropelUserProvider; /** * @author William Durand diff --git a/Tests/autoload.php.dist b/Tests/autoload.php.dist deleted file mode 100644 index 30ce93c..0000000 --- a/Tests/autoload.php.dist +++ /dev/null @@ -1,5 +0,0 @@ -=2.8.0" + }, + + "replace": { + "symfony/propel1-bridge": "<=2.8.0" + }, + "suggest": { "symfony/security-acl": "For using the Propel ACL implementation" } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c34bc77..20e8c26 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,30 +1,34 @@ - - - - - - ./Tests - - - - - ./ - - ./Tests - ./vendor - ./Resources - - - + backupStaticAttributes="false" + colors="true" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + processIsolation="false" + stopOnFailure="false" + syntaxCheck="false" + bootstrap="vendor/autoload.php"> + + + + vendor/phing/phing/classes + + + + + ./Tests + + + + + + ./ + + ./Tests + ./vendor + ./Resources + + +