Explorar o código

Merge pull request #72 from Romain-Geissler/submodel-filters

Submodel filters
Thomas %!s(int64=13) %!d(string=hai) anos
pai
achega
954d254eb7

+ 29 - 0
Admin/FieldDescription.php

@@ -67,6 +67,27 @@ class FieldDescription extends BaseFieldDescription
         $this->fieldName    = $this->fieldName ?: $fieldMapping['fieldName'];
     }
 
+    /**
+     * set the parent association mappings information
+     *
+     * @param array $parentAssociationMappings
+     * @return void
+     */
+    public function setParentAssociationMappings($parentAssociationMappings)
+    {
+        if (!is_array($parentAssociationMappings)) {
+            throw new \RuntimeException('The parent association mappings must be an array of association mappings');
+        }
+
+        foreach ($parentAssociationMappings as $parentAssociationMapping) {
+            if (!is_array($parentAssociationMapping)) {
+                throw new \RuntimeException('An association mapping must be an array');
+            }
+        }
+
+        $this->parentAssociationMappings=$parentAssociationMappings;
+    }
+
     /**
      * return true if the FieldDescription is linked to an identifier field
      *
@@ -76,4 +97,12 @@ class FieldDescription extends BaseFieldDescription
     {
         return isset($this->fieldMapping['id']) ? $this->fieldMapping['id'] : false;
     }
+
+    public function getValue($object){
+        foreach ($this->parentAssociationMappings as $parentAssociationMapping) {
+            $object = $this->getFieldValue($object, $parentAssociationMapping['fieldName']);
+        }
+
+        return $this->getFieldValue($object, $this->fieldName);
+    }
 }

+ 8 - 6
Builder/DatagridBuilder.php

@@ -56,17 +56,19 @@ class DatagridBuilder implements DatagridBuilderInterface
         $fieldDescription->setAdmin($admin);
 
         if ($admin->getModelManager()->hasMetadata($admin->getClass())) {
-            $metadata = $admin->getModelManager()->getMetadata($admin->getClass());
+            list($metadata, $lastPropertyName, $parentAssociationMappings) = $admin->getModelManager()->getParentMetadataForProperty($admin->getClass(), $fieldDescription->getName());
 
             // set the default field mapping
-            if (isset($metadata->fieldMappings[$fieldDescription->getName()])) {
-                $fieldDescription->setFieldMapping($metadata->fieldMappings[$fieldDescription->getName()]);
+            if (isset($metadata->fieldMappings[$lastPropertyName])) {
+                $fieldDescription->setOption('field_mapping', $fieldDescription->getOption('field_mapping', $metadata->fieldMappings[$lastPropertyName]));
             }
 
             // set the default association mapping
-            if (isset($metadata->associationMappings[$fieldDescription->getName()])) {
-                $fieldDescription->setAssociationMapping($metadata->associationMappings[$fieldDescription->getName()]);
+            if (isset($metadata->associationMappings[$lastPropertyName])) {
+                $fieldDescription->setOption('association_mapping', $fieldDescription->getOption('association_mapping', $metadata->associationMappings[$lastPropertyName]));
             }
+
+            $fieldDescription->setOption('parent_association_mappings', $fieldDescription->getOption('parent_association_mappings', $parentAssociationMappings));
         }
 
         $fieldDescription->setOption('code', $fieldDescription->getOption('code', $fieldDescription->getName()));
@@ -83,7 +85,7 @@ class DatagridBuilder implements DatagridBuilderInterface
     public function addFilter(DatagridInterface $datagrid, $type = null, FieldDescriptionInterface $fieldDescription, AdminInterface $admin)
     {
         if ($type == null) {
-            $guessType = $this->guesser->guessType($admin->getClass(), $fieldDescription->getName());
+            $guessType = $this->guesser->guessType($admin->getClass(), $fieldDescription->getName(), $admin->getModelManager());
 
             $type = $guessType->getType();
 

+ 10 - 7
Builder/ListBuilder.php

@@ -53,7 +53,7 @@ class ListBuilder implements ListBuilderInterface
     public function addField(FieldDescriptionCollection $list, $type = null, FieldDescriptionInterface $fieldDescription, AdminInterface $admin)
     {
         if ($type == null) {
-            $guessType = $this->guesser->guessType($admin->getClass(), $fieldDescription->getName());
+            $guessType = $this->guesser->guessType($admin->getClass(), $fieldDescription->getName(), $admin->getModelManager());
             $fieldDescription->setType($guessType->getType());
         } else {
             $fieldDescription->setType($type);
@@ -94,19 +94,22 @@ class ListBuilder implements ListBuilderInterface
         $fieldDescription->setAdmin($admin);
 
         if ($admin->getModelManager()->hasMetadata($admin->getClass())) {
-            $metadata = $admin->getModelManager()->getMetadata($admin->getClass());
+            list($metadata, $lastPropertyName, $parentAssociationMappings) = $admin->getModelManager()->getParentMetadataForProperty($admin->getClass(), $fieldDescription->getName());
+            $fieldDescription->setParentAssociationMappings($parentAssociationMappings);
 
             // set the default field mapping
-            if (isset($metadata->fieldMappings[$fieldDescription->getName()])) {
-                $fieldDescription->setFieldMapping($metadata->fieldMappings[$fieldDescription->getName()]);
+            if (isset($metadata->fieldMappings[$lastPropertyName])) {
+                $fieldDescription->setFieldMapping($metadata->fieldMappings[$lastPropertyName]);
                 if ($fieldDescription->getOption('sortable') !== false) {
-                    $fieldDescription->setOption('sortable', $fieldDescription->getOption('sortable', $fieldDescription->getName()));
+                    $fieldDescription->setOption('sortable', $fieldDescription->getOption('sortable', true));
+                    $fieldDescription->setOption('sort_parent_association_mappings', $fieldDescription->getOption('sort_parent_association_mappings', $fieldDescription->getParentAssociationMappings()));
+                    $fieldDescription->setOption('sort_field_mapping', $fieldDescription->getOption('sort_field_mapping', $fieldDescription->getFieldMapping()));
                 }
             }
 
             // set the default association mapping
-            if (isset($metadata->associationMappings[$fieldDescription->getName()])) {
-                $fieldDescription->setAssociationMapping($metadata->associationMappings[$fieldDescription->getName()]);
+            if (isset($metadata->associationMappings[$lastPropertyName])) {
+                $fieldDescription->setAssociationMapping($metadata->associationMappings[$lastPropertyName]);
             }
 
             $fieldDescription->setOption('_sort_order', $fieldDescription->getOption('_sort_order', 'ASC'));

+ 7 - 6
Builder/ShowBuilder.php

@@ -54,7 +54,7 @@ class ShowBuilder implements ShowBuilderInterface
     public function addField(FieldDescriptionCollection $list, $type = null, FieldDescriptionInterface $fieldDescription, AdminInterface $admin)
     {
         if ($type == null) {
-            $guessType = $this->guesser->guessType($admin->getClass(), $fieldDescription->getName());
+            $guessType = $this->guesser->guessType($admin->getClass(), $fieldDescription->getName(), $admin->getModelManager());
             $fieldDescription->setType($guessType->getType());
         } else {
             $fieldDescription->setType($type);
@@ -91,16 +91,17 @@ class ShowBuilder implements ShowBuilderInterface
         $fieldDescription->setAdmin($admin);
 
         if ($admin->getModelManager()->hasMetadata($admin->getClass())) {
-            $metadata = $admin->getModelManager()->getMetadata($admin->getClass());
+            list($metadata, $lastPropertyName, $parentAssociationMappings) = $admin->getModelManager()->getParentMetadataForProperty($admin->getClass(), $fieldDescription->getName());
+            $fieldDescription->setParentAssociationMappings($parentAssociationMappings);
 
             // set the default field mapping
-            if (isset($metadata->fieldMappings[$fieldDescription->getName()])) {
-                $fieldDescription->setFieldMapping($metadata->fieldMappings[$fieldDescription->getName()]);
+            if (isset($metadata->fieldMappings[$lastPropertyName])) {
+                $fieldDescription->setFieldMapping($metadata->fieldMappings[$lastPropertyName]);
             }
 
             // set the default association mapping
-            if (isset($metadata->associationMappings[$fieldDescription->getName()])) {
-                $fieldDescription->setAssociationMapping($metadata->associationMappings[$fieldDescription->getName()]);
+            if (isset($metadata->associationMappings[$lastPropertyName])) {
+                $fieldDescription->setAssociationMapping($metadata->associationMappings[$lastPropertyName]);
             }
         }
 

+ 33 - 2
Datagrid/ProxyQuery.php

@@ -25,9 +25,15 @@ class ProxyQuery implements ProxyQueryInterface
 
     protected $sortOrder;
 
+    protected $parameterUniqueId;
+
+    protected $entityJoinAliases;
+
     public function __construct(QueryBuilder $queryBuilder)
     {
         $this->queryBuilder = $queryBuilder;
+        $this->uniqueParameterId = 0;
+        $this->entityJoinAliases = array();
     }
 
     public function execute(array $params = array(), $hydrationMode = null)
@@ -82,6 +88,7 @@ class ProxyQuery implements ProxyQueryInterface
             if (strpos($sortBy, '.') === false) { // add the current alias
                 $sortBy = $queryBuilderId->getRootAlias().'.'.$sortBy;
             }
+            $sortBy .= ' AS __order_by';
             $queryBuilderId->addSelect($sortBy);
         }
 
@@ -107,9 +114,10 @@ class ProxyQuery implements ProxyQueryInterface
         return call_user_func_array(array($this->queryBuilder, $name), $args);
     }
 
-    public function setSortBy($sortBy)
+    public function setSortBy($parentAssociationMappings, $fieldMapping)
     {
-        $this->sortBy = $sortBy;
+        $alias = $this->entityJoin($parentAssociationMappings);
+        $this->sortBy = $alias.'.'.$fieldMapping['fieldName'];
     }
 
     public function getSortBy()
@@ -163,4 +171,27 @@ class ProxyQuery implements ProxyQueryInterface
     {
         $this->queryBuilder->getMaxResults();
     }
+
+    public function getUniqueParameterId()
+    {
+        return $this->uniqueParameterId++;
+    }
+
+    public function entityJoin($associationMappings)
+    {
+        $alias = $this->queryBuilder->getRootAlias();
+        $newAlias = 's';
+
+        foreach($associationMappings as $associationMapping){
+            $newAlias .= '_'.$associationMapping['fieldName'];
+            if (!in_array($newAlias, $this->entityJoinAliases)) {
+                $this->entityJoinAliases[] = $newAlias;
+                $this->queryBuilder->leftJoin(sprintf('%s.%s', $alias, $associationMapping['fieldName']), $newAlias);
+            }
+
+            $alias = $newAlias;
+        }
+
+        return $alias;
+    }
 }

+ 12 - 7
Filter/AbstractDateFilter.php

@@ -49,15 +49,18 @@ abstract class AbstractDateFilter extends Filter
             //default type for range filter
             $data['type'] = !isset($data['type']) || !is_numeric($data['type']) ?  DateRangeType::TYPE_BETWEEN : $data['type'];
 
+            $startDateParameterName = $this->getNewParameterName($queryBuilder);
+            $endDateParameterName = $this->getNewParameterName($queryBuilder);
+
             if ($data['type'] == DateRangeType::TYPE_NOT_BETWEEN) {
-                $this->applyWhere($queryBuilder, sprintf('%s.%s < :%s OR %s.%s > :%s', $alias, $field, $this->getName().'_start', $alias, $field, $this->getName().'_end'));
+                $this->applyWhere($queryBuilder, sprintf('%s.%s < :%s OR %s.%s > :%s', $alias, $field, $startDateParameterName, $alias, $field, $endDateParameterName));
             } else {
-                $this->applyWhere($queryBuilder, sprintf('%s.%s %s :%s', $alias, $field, '>=', $this->getName().'_start'));
-                $this->applyWhere($queryBuilder, sprintf('%s.%s %s :%s', $alias, $field, '<=', $this->getName().'_end'));
+                $this->applyWhere($queryBuilder, sprintf('%s.%s %s :%s', $alias, $field, '>=', $startDateParameterName));
+                $this->applyWhere($queryBuilder, sprintf('%s.%s %s :%s', $alias, $field, '<=', $endDateParameterName));
             }
 
-            $queryBuilder->setParameter($this->getName().'_start',  $data['value']['start']);
-            $queryBuilder->setParameter($this->getName().'_end',  $data['value']['end']);
+            $queryBuilder->setParameter($startDateParameterName,  $data['value']['start']);
+            $queryBuilder->setParameter($endDateParameterName,  $data['value']['end']);
         } else {
 
             if (!$data['value']) {
@@ -74,8 +77,10 @@ abstract class AbstractDateFilter extends Filter
             if (in_array($operator, array('NULL', 'NOT NULL'))) {
                 $this->applyWhere($queryBuilder, sprintf('%s.%s IS %s ', $alias, $field, $operator));
             } else {
-                $this->applyWhere($queryBuilder, sprintf('%s.%s %s :%s', $alias, $field, $operator, $this->getName()));
-                $queryBuilder->setParameter($this->getName(), $data['value']);
+                $parameterName = $this->getNewParameterName($queryBuilder);
+
+                $this->applyWhere($queryBuilder, sprintf('%s.%s %s :%s', $alias, $field, $operator, $parameterName));
+                $queryBuilder->setParameter($parameterName, $data['value']);
             }
         }
     }

+ 3 - 2
Filter/BooleanFilter.php

@@ -49,8 +49,9 @@ class BooleanFilter extends Filter
                 return;
             }
 
-            $this->applyWhere($queryBuilder, sprintf('%s.%s = :%s', $alias, $field, $this->getName()));
-            $queryBuilder->setParameter($this->getName(), ($data['value'] == BooleanType::TYPE_YES) ? 1 : 0);
+            $parameterName = $this->getNewParameterName($queryBuilder);
+            $this->applyWhere($queryBuilder, sprintf('%s.%s = :%s', $alias, $field, $parameterName));
+            $queryBuilder->setParameter($parameterName, ($data['value'] == BooleanType::TYPE_YES) ? 1 : 0);
         }
     }
 

+ 5 - 3
Filter/ChoiceFilter.php

@@ -50,13 +50,15 @@ class ChoiceFilter extends Filter
                 return;
             }
 
+            $parameterName = $this->getNewParameterName($queryBuilder);
+
             if ($data['type'] == ChoiceType::TYPE_NOT_CONTAINS) {
-                $this->applyWhere($queryBuilder, sprintf('%s.%s <> :%s', $alias, $field, $this->getName()));
+                $this->applyWhere($queryBuilder, sprintf('%s.%s <> :%s', $alias, $field, $parameterName));
             } else {
-                $this->applyWhere($queryBuilder, sprintf('%s.%s = :%s', $alias, $field, $this->getName()));
+                $this->applyWhere($queryBuilder, sprintf('%s.%s = :%s', $alias, $field, $parameterName));
             }
 
-            $queryBuilder->setParameter($this->getName(), $data['value']);
+            $queryBuilder->setParameter($parameterName, $data['value']);
         }
     }
 

+ 10 - 1
Filter/Filter.php

@@ -28,7 +28,9 @@ abstract class Filter extends BaseFilter
 
     protected function association($queryBuilder, $value)
     {
-        return array($this->getOption('alias', $queryBuilder->getRootAlias()), $this->getFieldName());
+        $alias = $queryBuilder->entityJoin($this->getParentAssociationMappings());
+
+        return array($alias, $this->getFieldName());
     }
 
     protected function applyWhere($queryBuilder, $parameter)
@@ -43,6 +45,13 @@ abstract class Filter extends BaseFilter
         $this->active = true;
     }
 
+    protected function getNewParameterName($queryBuilder)
+    {
+        // dots are not accepted in a DQL identifier so replace them
+        // by underscores.
+        return str_replace('.', '_', $this->getName()).'_'.$queryBuilder->getUniqueParameterId();
+    }
+
     public function isActive()
     {
         return $this->active;

+ 12 - 14
Filter/ModelFilter.php

@@ -43,13 +43,15 @@ class ModelFilter extends Filter
             return;
         }
 
+        $parameterName = $this->getNewParameterName($queryBuilder);
+
         if (isset($data['type']) && $data['type'] == EqualType::TYPE_IS_NOT_EQUAL) {
-            $this->applyWhere($queryBuilder, $queryBuilder->expr()->notIn($alias, ':'.$this->getName()));
+            $this->applyWhere($queryBuilder, $queryBuilder->expr()->notIn($alias, ':'.$parameterName));
         } else {
-            $this->applyWhere($queryBuilder, $queryBuilder->expr()->in($alias, ':'.$this->getName()));
+            $this->applyWhere($queryBuilder, $queryBuilder->expr()->in($alias, ':'.$parameterName));
         }
 
-        $queryBuilder->setParameter($this->getName(), $data['value']->toArray());
+        $queryBuilder->setParameter($parameterName, $data['value']->toArray());
     }
 
     protected function handleModel($queryBuilder, $alias, $field, $data)
@@ -58,13 +60,15 @@ class ModelFilter extends Filter
             return;
         }
 
+        $parameterName = $this->getNewParameterName($queryBuilder);
+
         if (isset($data['type']) && $data['type'] == EqualType::TYPE_IS_NOT_EQUAL) {
-            $this->applyWhere($queryBuilder, sprintf('%s != :%s', $alias, $this->getName()));
+            $this->applyWhere($queryBuilder, sprintf('%s != :%s', $alias, $parameterName));
         } else {
-            $this->applyWhere($queryBuilder, sprintf('%s = :%s', $alias, $this->getName()));
+            $this->applyWhere($queryBuilder, sprintf('%s = :%s', $alias, $parameterName));
         }
 
-        $queryBuilder->setParameter($this->getName(), $data['value']);
+        $queryBuilder->setParameter($parameterName, $data['value']);
     }
 
     protected function association($queryBuilder, $data)
@@ -80,15 +84,9 @@ class ModelFilter extends Filter
             throw new \RunTimeException('Invalid mapping type');
         }
 
-        if (!$this->getOption('field_name')) {
-            throw new \RunTimeException('Please provide a field_name options');
-        }
-
-        $alias = 's_'.$this->getName();
-
-        $queryBuilder->leftJoin(sprintf('%s.%s', $queryBuilder->getRootAlias(), $this->getFieldName()), $alias);
+        $alias = $queryBuilder->entityJoin($this->getParentAssociationMappings() + array($this->getAssociationMapping()));
 
-        return array($alias, 'id');
+        return array($alias, false);
     }
 
     public function getDefaultOptions()

+ 3 - 2
Filter/NumberFilter.php

@@ -37,8 +37,9 @@ class NumberFilter extends Filter
         }
 
         // c.name > '1' => c.name OPERATOR :FIELDNAME
-        $this->applyWhere($queryBuilder, sprintf('%s.%s %s :%s', $alias, $field, $operator, $this->getName()));
-        $queryBuilder->setParameter($this->getName(),  $data['value']);
+        $parameterName = $this->getNewParameterName($queryBuilder);
+        $this->applyWhere($queryBuilder, sprintf('%s.%s %s :%s', $alias, $field, $operator, $parameterName));
+        $queryBuilder->setParameter($parameterName,  $data['value']);
     }
 
     /**

+ 4 - 3
Filter/StringFilter.php

@@ -43,12 +43,13 @@ class StringFilter extends Filter
         }
 
         // c.name > '1' => c.name OPERATOR :FIELDNAME
-        $this->applyWhere($queryBuilder, sprintf('%s.%s %s :%s', $alias, $field, $operator, $this->getName()));
+        $parameterName = $this->getNewParameterName($queryBuilder);
+        $this->applyWhere($queryBuilder, sprintf('%s.%s %s :%s', $alias, $field, $operator, $parameterName));
 
         if ($data['type'] == ChoiceType::TYPE_EQUAL) {
-            $queryBuilder->setParameter($this->getName(), $data['value']);
+            $queryBuilder->setParameter($parameterName, $data['value']);
         } else {
-            $queryBuilder->setParameter($this->getName(), sprintf($this->getOption('format'), $data['value']));
+            $queryBuilder->setParameter($parameterName, sprintf($this->getOption('format'), $data['value']));
         }
     }
 

+ 36 - 0
Guesser/AbstractTypeGuesser.php

@@ -0,0 +1,36 @@
+<?php
+
+/*
+ * This file is part of the Sonata package.
+ *
+ * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Sonata\DoctrineORMAdminBundle\Guesser;
+
+use Sonata\AdminBundle\Guesser\TypeGuesserInterface;
+use Doctrine\ORM\Mapping\MappingException;
+use Sonata\AdminBundle\Model\ModelManagerInterface;
+
+abstract class AbstractTypeGuesser implements TypeGuesserInterface
+{
+    /**
+     * @param string $class
+     * @param string $property
+     * @return TypeGuess
+     */
+    abstract public function guessType($class, $property, ModelManagerInterface $modelManager);
+
+    protected function getParentMetadataForProperty($baseClass, $propertyFullName, $modelManager)
+    {
+        try {
+            return $modelManager->getParentMetadataForProperty($baseClass, $propertyFullName);
+        } catch (MappingException $e) {
+            // no metadata not found.
+            return null;
+        }
+    }
+}

+ 12 - 37
Guesser/FilterTypeGuesser.php

@@ -12,32 +12,21 @@
 namespace Sonata\DoctrineORMAdminBundle\Guesser;
 
 use Sonata\AdminBundle\Guesser\TypeGuesserInterface;
-use Symfony\Bridge\Doctrine\RegistryInterface;
 use Symfony\Component\Form\Guess\Guess;
 use Symfony\Component\Form\Guess\TypeGuess;
 use Doctrine\ORM\Mapping\ClassMetadataInfo;
-use Doctrine\ORM\Mapping\MappingException;
+use Sonata\AdminBundle\Model\ModelManagerInterface;
 
-class FilterTypeGuesser implements TypeGuesserInterface
+class FilterTypeGuesser extends AbstractTypeGuesser
 {
-    protected $registry;
-
-    private $cache;
-
-    public function __construct(RegistryInterface $registry)
-    {
-        $this->registry = $registry;
-        $this->cache = array();
-    }
-
     /**
      * @param string $class
      * @param string $property
      * @return TypeGuess
      */
-    public function guessType($class, $property)
+    public function guessType($class, $property, ModelManagerInterface $modelManager)
     {
-        if (!$ret = $this->getMetadata($class)) {
+        if (!$ret = $this->getParentMetadataForProperty($class, $property, $modelManager)) {
             return false;
         }
 
@@ -47,11 +36,12 @@ class FilterTypeGuesser implements TypeGuesserInterface
             'options'        => array(),
         );
 
-        list($metadata, $name) = $ret;
+        list($metadata, $propertyName, $parentAssociationMappings) = $ret;
+
+        $options['parent_association_mappings'] = $parentAssociationMappings;
 
-        if ($metadata->hasAssociation($property)) {
-            $multiple = $metadata->isCollectionValuedAssociation($property);
-            $mapping = $metadata->getAssociationMapping($property);
+        if ($metadata->hasAssociation($propertyName)) {
+            $mapping = $metadata->getAssociationMapping($propertyName);
 
             switch ($mapping['type']) {
                 case ClassMetadataInfo::ONE_TO_ONE:
@@ -66,6 +56,7 @@ class FilterTypeGuesser implements TypeGuesserInterface
                     $options['field_options'] = array(
                         'class' => $mapping['targetEntity']
                     );
+
                     $options['field_name'] = $mapping['fieldName'];
                     $options['mapping_type'] = $mapping['type'];
 
@@ -73,9 +64,9 @@ class FilterTypeGuesser implements TypeGuesserInterface
             }
         }
 
-        $options['field_name'] = $metadata->fieldMappings[$property]['fieldName'];
+        $options['field_name'] = $metadata->fieldMappings[$propertyName]['fieldName'];
 
-        switch ($metadata->getTypeOfField($property)) {
+        switch ($metadata->getTypeOfField($propertyName)) {
             case 'boolean':
                 $options['field_type'] = 'sonata_type_boolean';
                 $options['field_options'] = array();
@@ -110,20 +101,4 @@ class FilterTypeGuesser implements TypeGuesserInterface
                 return new TypeGuess('doctrine_orm_string', $options, Guess::LOW_CONFIDENCE);
         }
     }
-
-    protected function getMetadata($class)
-    {
-        if (array_key_exists($class, $this->cache)) {
-            return $this->cache[$class];
-        }
-
-        $this->cache[$class] = null;
-        foreach ($this->registry->getEntityManagers() as $name => $em) {
-            try {
-                return $this->cache[$class] = array($em->getClassMetadata($class), $name);
-            } catch (MappingException $e) {
-                // not an entity or mapped super class
-            }
-        }
-    }
 }

+ 8 - 35
Guesser/TypeGuesser.php

@@ -16,36 +16,25 @@ use Symfony\Bridge\Doctrine\RegistryInterface;
 use Symfony\Component\Form\Guess\Guess;
 use Symfony\Component\Form\Guess\TypeGuess;
 use Doctrine\ORM\Mapping\ClassMetadataInfo;
-use Doctrine\ORM\Mapping\MappingException;
+use Sonata\AdminBundle\Model\ModelManagerInterface;
 
-class TypeGuesser implements TypeGuesserInterface
+class TypeGuesser extends AbstractTypeGuesser
 {
-    protected $registry;
-
-    private $cache;
-
-    public function __construct(RegistryInterface $registry)
-    {
-        $this->registry = $registry;
-        $this->cache = array();
-    }
-
     /**
      * @param string $class
      * @param string $property
      * @return TypeGuess
      */
-    public function guessType($class, $property)
+    public function guessType($class, $property, ModelManagerInterface $modelManager)
     {
-        if (!$ret = $this->getMetadata($class)) {
+        if (!$ret = $this->getParentMetadataForProperty($class, $property, $modelManager)) {
             return new TypeGuess('text', array(), Guess::LOW_CONFIDENCE);
         }
 
-        list($metadata, $name) = $ret;
+        list($metadata, $propertyName, $parentAssociationMappings) = $ret;
 
-        if ($metadata->hasAssociation($property)) {
-            $multiple = $metadata->isCollectionValuedAssociation($property);
-            $mapping = $metadata->getAssociationMapping($property);
+        if ($metadata->hasAssociation($propertyName)) {
+            $mapping = $metadata->getAssociationMapping($propertyName);
 
             switch ($mapping['type']) {
                 case ClassMetadataInfo::ONE_TO_MANY:
@@ -62,7 +51,7 @@ class TypeGuesser implements TypeGuesserInterface
             }
         }
 
-        switch ($metadata->getTypeOfField($property))
+        switch ($metadata->getTypeOfField($propertyName))
         {
             case 'array':
                 return new TypeGuess('array', array(), Guess::HIGH_CONFIDENCE);
@@ -91,20 +80,4 @@ class TypeGuesser implements TypeGuesserInterface
                 return new TypeGuess('text', array(), Guess::LOW_CONFIDENCE);
         }
     }
-
-    protected function getMetadata($class)
-    {
-        if (array_key_exists($class, $this->cache)) {
-            return $this->cache[$class];
-        }
-
-        $this->cache[$class] = null;
-        foreach ($this->registry->getEntityManagers() as $name => $em) {
-            try {
-                return $this->cache[$class] = array($em->getClassMetadata($class), $name);
-            } catch (MappingException $e) {
-                // not an entity or mapped super class
-            }
-        }
-    }
 }

+ 38 - 7
Model/ModelManager.php

@@ -53,6 +53,36 @@ class ModelManager implements ModelManagerInterface
         return $this->getEntityManager($class)->getMetadataFactory()->getMetadataFor($class);
     }
 
+
+    /**
+     * Returns the model's metadata holding the fully qualified property, and the last
+     * property name
+     *
+     * @param string $baseClass The base class of the model holding the fully qualified property.
+     * @param string $propertyFullName The name of the fully qualified property (dot ('.') separated
+     * property string)
+     * @return array(
+     *     \Doctrine\ORM\Mapping\ClassMetadata $parentMetadata,
+     *     string $lastPropertyName,
+     *     array $parentAssociationMappings
+     * )
+     */
+    public function getParentMetadataForProperty($baseClass, $propertyFullName)
+    {
+        $nameElements = explode('.', $propertyFullName);
+        $lastPropertyName = array_pop($nameElements);
+        $class = $baseClass;
+        $parentAssociationMappings = array();
+
+        foreach($nameElements as $nameElement){
+            $metadata = $this->getMetadata($class);
+            $parentAssociationMappings[] = $metadata->associationMappings[$nameElement];
+            $class = $metadata->getAssociationTargetClass($nameElement);
+        }
+
+        return array($this->getMetadata($class), $lastPropertyName, $parentAssociationMappings);
+    }
+
     /**
      * Returns true is the model has some metadata
      *
@@ -79,18 +109,19 @@ class ModelManager implements ModelManagerInterface
             throw new \RunTimeException('The name argument must be a string');
         }
 
-        $metadata = $this->getMetadata($class);
+        list($metadata, $propertyName, $parentAssociationMappings) = $this->getParentMetadataForProperty($class, $name);
 
         $fieldDescription = new FieldDescription;
         $fieldDescription->setName($name);
         $fieldDescription->setOptions($options);
+        $fieldDescription->setParentAssociationMappings($parentAssociationMappings);
 
-        if (isset($metadata->associationMappings[$name])) {
-            $fieldDescription->setAssociationMapping($metadata->associationMappings[$name]);
+        if (isset($metadata->associationMappings[$propertyName])) {
+            $fieldDescription->setAssociationMapping($metadata->associationMappings[$propertyName]);
         }
 
-        if (isset($metadata->fieldMappings[$name])) {
-            $fieldDescription->setFieldMapping($metadata->fieldMappings[$name]);
+        if (isset($metadata->fieldMappings[$propertyName])) {
+            $fieldDescription->setFieldMapping($metadata->fieldMappings[$propertyName]);
         }
 
         return $fieldDescription;
@@ -388,7 +419,7 @@ class ModelManager implements ModelManagerInterface
     {
         $values = $datagrid->getValues();
 
-        if ($fieldDescription->getOption('sortable') == $values['_sort_by']) {
+        if ($fieldDescription->getName() == $values['_sort_by']) {
             if ($values['_sort_order'] == 'ASC') {
                 $values['_sort_order'] = 'DESC';
             } else {
@@ -396,7 +427,7 @@ class ModelManager implements ModelManagerInterface
             }
         } else {
             $values['_sort_order']  = 'ASC';
-            $values['_sort_by']     = $fieldDescription->getOption('sortable');
+            $values['_sort_by']     = $fieldDescription->getName();
         }
 
         return array('filter' => $values);

+ 0 - 3
Resources/config/doctrine_orm.xml

@@ -25,7 +25,6 @@
         </service>
 
         <service id="sonata.admin.guesser.orm_list" class="Sonata\DoctrineORMAdminBundle\Guesser\TypeGuesser">
-            <argument type="service" id="doctrine" />
             <tag name="sonata.admin.guesser.orm_list" />
         </service>
 
@@ -40,7 +39,6 @@
         </service>
 
         <service id="sonata.admin.guesser.orm_show" class="Sonata\DoctrineORMAdminBundle\Guesser\TypeGuesser">
-            <argument type="service" id="doctrine" />
             <tag name="sonata.admin.guesser.orm_show" />
         </service>
 
@@ -56,7 +54,6 @@
         </service>
 
         <service id="sonata.admin.guesser.orm_datagrid" class="Sonata\DoctrineORMAdminBundle\Guesser\FilterTypeGuesser">
-            <argument type="service" id="doctrine" />
             <tag name="sonata.admin.guesser.orm_datagrid" />
         </service>
 

+ 34 - 0
Resources/doc/reference/filter_field_definition.rst

@@ -68,6 +68,40 @@ Example
 Advanced usage
 --------------
 
+Filtering by sub entity properties
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If you need to filter your base entities by the value of a sub entity property,
+you can simply use the dot-separated notation (note that this only makes sense
+when the prefix path is made of entities, not collections):
+
+.. code-block:: php
+
+    <?php
+    namespace Acme\AcmeBundle\Admin;
+
+    use Sonata\AdminBundle\Admin\Admin;
+    use Sonata\AdminBundle\Form\FormMapper;
+    use Sonata\AdminBundle\Datagrid\DatagridMapper;
+    use Sonata\AdminBundle\Datagrid\ListMapper;
+    use Sonata\AdminBundle\Show\ShowMapper;
+
+    class UserAdmin extends Admin
+    {
+        protected function configureDatagridFilters(DatagridMapper $datagrid)
+        {
+            $datagrid
+                ->add('id')
+                ->add('firstName')
+                ->add('lastName')
+                ->add('address.street')
+                ->add('address.ZIPCode')
+                ->add('address.town')
+            ;
+        }
+    }
+
+
 Label
 ^^^^^
 

+ 37 - 0
Resources/doc/reference/list_field_definition.rst

@@ -93,6 +93,43 @@ You can specify your own by setting up the 'template' option like so:
 Advance Usage
 -------------
 
+Displaying sub entity properties
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If you need to display only one field from a sub entity in a dedicated column,
+you can simply use the dot-separated notation (note that this only makes sense
+when the prefix path is made of entities, not collections):
+
+.. code-block:: php
+
+    <?php
+    namespace Acme\AcmeBundle\Admin;
+
+    use Sonata\AdminBundle\Admin\Admin;
+    use Sonata\AdminBundle\Form\FormMapper;
+    use Sonata\AdminBundle\Datagrid\DatagridMapper;
+    use Sonata\AdminBundle\Datagrid\ListMapper;
+    use Sonata\AdminBundle\Show\ShowMapper;
+
+    class UserAdmin extends Admin
+    {
+        protected function configureListFields(ListMapper $listMapper)
+        {
+            $listMapper
+                ->addIdentifier('id')
+                ->addIdentifier('firstName')
+                ->addIdentifier('lastName')
+                ->addIdentifier('address.street')
+                ->addIdentifier('address.ZIPCode')
+                ->addIdentifier('address.town')
+            ;
+        }
+    }
+
+
+Custom template
+^^^^^^^^^^^^^^^
+
 If you need a specific layout for a row cell, you can define a custom template
 
 .. code-block:: php