Skip to content

Commit

Permalink
Added sonata_type_model_autocomplete form type
Browse files Browse the repository at this point in the history
Added support for many-to-many in sonata_type_model_autocomplete

Changed `collection` form type to `sonata_type_native_collection`

Fixed unit tests

Fixed issues with Sf 2.3

Fixed dates in changelog

Fixed minor issues

Make code more universal.

Another improvements. Fixed bugs in paging.

Added toStringCallback option. Fixed datagrid parameters.

Fixed unit tests

Improved code coverage of Datagrid

Improved doc and added some more tests

Fixed CS
  • Loading branch information
pulzarraider committed Aug 8, 2014
1 parent 6b101d7 commit 3d86eca
Show file tree
Hide file tree
Showing 15 changed files with 1,052 additions and 15 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
CHANGELOG
=========

### 2014-08-08
* added new form type ``sonata_type_model_autocomplete``
* changed ``collection`` form type to ``sonata_type_native_collection``

### 2013-12-27

* [BC BREAK] Added KnpMenuBundle v2.x compatibility, ``buildSideMenu`` must now use the ``Admin::generateMenuUrl`` method to generate the route arguments for the KnpMenu options.
Expand All @@ -19,6 +23,9 @@ CHANGELOG
If you do not extend the Admin class, you need to add this method to
your admin.

### 2013-10-26
* added new form type ``sonata_type_model_hidden``

### 2013-10-13

* [BC BREAK] added ``setCurrentChild``, ``getCurrentChild`` to the AdminInterface
Expand Down
142 changes: 142 additions & 0 deletions Controller/HelperController.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
use Symfony\Component\Validator\ValidatorInterface;
use Sonata\AdminBundle\Admin\Pool;
use Sonata\AdminBundle\Admin\AdminHelper;
use Sonata\AdminBundle\Admin\AdminInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Sonata\AdminBundle\Filter\FilterInterface;

class HelperController
{
Expand Down Expand Up @@ -293,4 +296,143 @@ public function setObjectFieldValueAction(Request $request)

return new JsonResponse(array('status' => 'OK', 'content' => $content));
}

/**
* Retrieve list of items for autocomplete form field
*
* @param Request $request
*
* @return JsonResponse
*
* @throws \RuntimeException
* @throws AccessDeniedException
*/
public function retrieveAutocompleteItemsAction(Request $request)
{
$admin = $this->pool->getInstance($request->get('code'));
$admin->setRequest($request);

// check user permission
if (false === $admin->isGranted('LIST')) {
throw new AccessDeniedException();
}

// subject will be empty to avoid unnecessary database requests and keep autocomplete function fast
$admin->setSubject($admin->getNewInstance());

$fieldDescription = $this->retrieveFieldDescription($admin, $request->get('field'));
$formAutocomplete = $admin->getForm()->get($fieldDescription->getName());

if ($formAutocomplete->getConfig()->getAttribute('disabled')) {
throw new AccessDeniedException('Autocomplete list can`t be retrieved because the form element is disabled or read_only.');
}

$property = $formAutocomplete->getConfig()->getAttribute('property');
$callback = $formAutocomplete->getConfig()->getAttribute('callback');
$minimumInputLength = $formAutocomplete->getConfig()->getAttribute('minimum_input_length');
$itemsPerPage = $formAutocomplete->getConfig()->getAttribute('items_per_page');
$reqParamPageNumber = $formAutocomplete->getConfig()->getAttribute('req_param_name_page_number');
$toStringCallback = $formAutocomplete->getConfig()->getAttribute('to_string_callback');

$searchText = $request->get('q');

if (mb_strlen($searchText, 'UTF-8') < $minimumInputLength) {
return new JsonResponse(array('status' => 'KO', 'message' => 'Too short search string.', 403));
}

$targetAdmin = $fieldDescription->getAssociationAdmin();
$datagrid = $targetAdmin->getDatagrid();

if ($callback !== null) {
if (!is_callable($callback)) {
throw new \RuntimeException('Callback doesn`t contain callable function.');
}

call_user_func($callback, $datagrid, $property, $searchText);
} else {
if (is_array($property)) {
// multiple properties
foreach ($property as $prop) {
if (!$datagrid->hasFilter($prop)) {
throw new \RuntimeException(sprintf('To retrieve autocomplete items, you should add filter "%s" to "%s" in configureDatagridFilters() method.', $prop, get_class($targetAdmin)));
}

$filter = $datagrid->getFilter($prop);
$filter->setCondition(FilterInterface::CONDITION_OR);

$datagrid->setValue($prop, null, $searchText);
}
} else {
if (!$datagrid->hasFilter($property)) {
throw new \RuntimeException(sprintf('To retrieve autocomplete items, you should add filter "%s" to "%s" in configureDatagridFilters() method.', $prop, get_class($targetAdmin)));
}

$datagrid->setValue($property, null, $searchText);
}
}

$datagrid->setValue('_per_page', null, $itemsPerPage);
$datagrid->setValue('_page', null, $request->query->get($reqParamPageNumber, 1));
$datagrid->buildPager();

$pager = $datagrid->getPager();

$items = array();
$results = $pager->getResults();

foreach ($results as $entity) {
if ($toStringCallback !== null) {
if (!is_callable($toStringCallback)) {
throw new \RuntimeException('Option "to_string_callback" doesn`t contain callable function.');
}

$label = call_user_func($toStringCallback, $entity, $property);
} else {
$resultMetadata = $targetAdmin->getObjectMetadata($entity);
$label = $resultMetadata->getTitle();
}

$items[] = array(
'id' => $admin->id($entity),
'label' => $label,
);
}

return new JsonResponse(array(
'status' => 'OK',
'more' => !$pager->isLastPage(),
'items' => $items
));
}

/**
* Retrieve the field description given by field name.
*
* @param AdminInterface $admin
* @param string $field
*
* @return \Symfony\Component\Form\FormInterface
*
* @throws \RuntimeException
*/
private function retrieveFieldDescription(AdminInterface $admin, $field)
{
$admin->getFormFieldDescriptions();

$fieldDescription = $admin->getFormFieldDescription($field);

if (!$fieldDescription) {
throw new \RuntimeException(sprintf('The field "%s" does not exist.', $field));
}

if ($fieldDescription->getType() !== 'sonata_type_model_autocomplete') {
throw new \RuntimeException(sprintf('Unsupported form type "%s" for field "%s".', $fieldDescription->getType(), $field));
}

if (null === $fieldDescription->getTargetEntity()) {
throw new \RuntimeException(sprintf('No associated entity with field "%s".', $field));
}

return $fieldDescription;
}
}
18 changes: 16 additions & 2 deletions Datagrid/Datagrid.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,22 @@ function($value) { return $value instanceof FieldDescriptionInterface ? $value->
}
}

$this->pager->setMaxPerPage(isset($this->values['_per_page']) ? $this->values['_per_page'] : 25);
$this->pager->setPage(isset($this->values['_page']) ? $this->values['_page'] : 1);
$maxPerPage = 25;
if (isset($this->values['_per_page']['value'])) {
$maxPerPage = $this->values['_per_page']['value'];
} elseif (isset($this->values['_per_page'])) {
$maxPerPage = $this->values['_per_page'];
}
$this->pager->setMaxPerPage($maxPerPage);

$page = 1;
if (isset($this->values['_page']['value'])) {
$page = $this->values['_page']['value'];
} elseif (isset($this->values['_page'])) {
$page = $this->values['_page'];
}
$this->pager->setPage($page);

$this->pager->setQuery($this->query);
$this->pager->init();

Expand Down
120 changes: 120 additions & 0 deletions Form/DataTransformer/ModelToIdPropertyTransformer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

/*
* This file is part of the Sonata package.
*
* (c) Thomas Rabaix <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/

namespace Sonata\AdminBundle\Form\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;
use Sonata\AdminBundle\Model\ModelManagerInterface;
use Doctrine\Common\Util\ClassUtils;
use RuntimeException;

/**
* Transform object to ID and property label
*
* @author Andrej Hudec <[email protected]>
*/
class ModelToIdPropertyTransformer implements DataTransformerInterface
{
protected $modelManager;

protected $className;

protected $property;

protected $multiple;

protected $toStringCallback;

/**
* @param ModelManagerInterface $modelManager
* @param string $className
* @param string $property
*/
public function __construct(ModelManagerInterface $modelManager, $className, $property, $multiple=false, $toStringCallback=null)
{
$this->modelManager = $modelManager;
$this->className = $className;
$this->property = $property;
$this->multiple = $multiple;
$this->toStringCallback = $toStringCallback;
}

/**
* {@inheritDoc}
*/
public function reverseTransform($value)
{
$collection = $this->modelManager->getModelCollectionInstance($this->className);

if (empty($value) || empty($value['identifiers'])) {
if (!$this->multiple) {
return null;
} else {
return $collection;
}
}

if (!$this->multiple) {
return $this->modelManager->find($this->className, current($value['identifiers']));
}

foreach ($value['identifiers'] as $id) {
$collection->add($this->modelManager->find($this->className, $id));
}

return $collection;
}

/**
* {@inheritDoc}
*/
public function transform($entityOrCollection)
{
$result = array('identifiers' => array(), 'labels' => array());

if (!$entityOrCollection) {
return $result;
}
if ($entityOrCollection instanceof \ArrayAccess) {
$collection = $entityOrCollection;
} else {
$collection = array($entityOrCollection);
}

if (empty($this->property)) {
throw new RuntimeException('Please define "property" parameter.');
}

foreach ($collection as $entity) {
$id = current($this->modelManager->getIdentifierValues($entity));

if ($this->toStringCallback !== null) {
if (!is_callable($this->toStringCallback)) {
throw new RuntimeException('Callback in "to_string_callback" option doesn`t contain callable function.');
}

$label = call_user_func($this->toStringCallback, $entity, $this->property);
} else {
try {
$label = (string) $entity;
} catch (\Exception $e) {
throw new RuntimeException(sprintf("Unable to convert the entity %s to String, entity must have a '__toString()' method defined", ClassUtils::getClass($entity)), 0, $e);
}
}

$result['identifiers'][] = $id;
$result['labels'][] = $label;
}

return $result;
}
}
6 changes: 6 additions & 0 deletions Form/FormMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ public function add($name, $type = null, array $options = array(), array $fieldD
$fieldName = str_replace('.', '__', $fieldName);
}

// change `collection` to `sonata_type_native_collection` form type to
// avoid BC break problems
if ($type == 'collection') {
$type = 'sonata_type_native_collection';
}

$label = $fieldName;

$group = $this->addFieldToCurrentGroup($label);
Expand Down
40 changes: 40 additions & 0 deletions Form/Type/CollectionType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

/*
* This file is part of the Sonata package.
*
* (c) Thomas Rabaix <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/

namespace Sonata\AdminBundle\Form\Type;

use Symfony\Component\Form\AbstractType;

/**
* This type wrap native `collection` form type and render `add` and `delete`
* buttons in standard Symfony` collection form type.
*
* @author Andrej Hudec <[email protected]>
*/
class CollectionType extends AbstractType
{
/**
* {@inheritDoc}
*/
public function getParent()
{
return 'collection';
}

/**
* {@inheritDoc}
*/
public function getName()
{
return 'sonata_type_native_collection';
}
}
Loading

0 comments on commit 3d86eca

Please sign in to comment.