This commit is contained in:
Xes
2025-08-14 22:41:49 +02:00
parent 2de81ccc46
commit 8ce45119b6
39774 changed files with 4309466 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,199 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Admin;
use Knp\Menu\ItemInterface as MenuItemInterface;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Route\RouteCollection;
use Sonata\AdminBundle\Show\ShowMapper;
use Sonata\CoreBundle\Validator\ErrorElement;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
abstract class AbstractAdminExtension implements AdminExtensionInterface
{
/**
* {@inheritdoc}
*/
public function configureFormFields(FormMapper $formMapper)
{
}
/**
* {@inheritdoc}
*/
public function configureListFields(ListMapper $listMapper)
{
}
/**
* {@inheritdoc}
*/
public function configureDatagridFilters(DatagridMapper $datagridMapper)
{
}
/**
* {@inheritdoc}
*/
public function configureShowFields(ShowMapper $showMapper)
{
}
/**
* {@inheritdoc}
*/
public function configureRoutes(AdminInterface $admin, RouteCollection $collection)
{
}
/**
* {@inheritdoc}
*/
public function configureSideMenu(AdminInterface $admin, MenuItemInterface $menu, $action, AdminInterface $childAdmin = null)
{
}
/**
* {@inheritdoc}
*/
public function configureTabMenu(AdminInterface $admin, MenuItemInterface $menu, $action, AdminInterface $childAdmin = null)
{
// Use configureSideMenu not to mess with previous overrides
// TODO remove once deprecation period is over
$this->configureSideMenu($admin, $menu, $action, $childAdmin);
}
/**
* {@inheritdoc}
*/
public function validate(AdminInterface $admin, ErrorElement $errorElement, $object)
{
}
/**
* {@inheritdoc}
*/
public function configureQuery(AdminInterface $admin, ProxyQueryInterface $query, $context = 'list')
{
}
/**
* {@inheritdoc}
*/
public function alterNewInstance(AdminInterface $admin, $object)
{
}
/**
* {@inheritdoc}
*/
public function alterObject(AdminInterface $admin, $object)
{
}
/**
* {@inheritdoc}
*/
public function getPersistentParameters(AdminInterface $admin)
{
return [];
}
/**
* {@inheritdoc}
*/
public function getAccessMapping(AdminInterface $admin)
{
return [];
}
/**
* {@inheritdoc}
*/
public function configureBatchActions(AdminInterface $admin, array $actions)
{
return $actions;
}
/**
* {@inheritdoc}
*/
public function configureExportFields(AdminInterface $admin, array $fields)
{
return $fields;
}
/**
* {@inheritdoc}
*/
public function preUpdate(AdminInterface $admin, $object)
{
}
/**
* {@inheritdoc}
*/
public function postUpdate(AdminInterface $admin, $object)
{
}
/**
* {@inheritdoc}
*/
public function prePersist(AdminInterface $admin, $object)
{
}
/**
* {@inheritdoc}
*/
public function postPersist(AdminInterface $admin, $object)
{
}
/**
* {@inheritdoc}
*/
public function preRemove(AdminInterface $admin, $object)
{
}
/**
* {@inheritdoc}
*/
public function postRemove(AdminInterface $admin, $object)
{
}
/**
* {@inheritdoc}
*/
public function configureActionButtons(AdminInterface $admin, $list, $action, $object)
{
return $list;
}
/**
* Returns a list of default filters.
*
* @param AdminInterface $admin
* @param array $filterValues
*/
public function configureDefaultFilterValues(AdminInterface $admin, array &$filterValues)
{
}
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Admin;
/**
* Tells if the current user has access to a given action.
*
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
interface AccessRegistryInterface
{
/**
* Return the controller access mapping.
*
* @return array
*/
public function getAccessMapping();
/**
* Hook to handle access authorization.
*
* @param string $action
* @param object $object
*/
public function checkAccess($action, $object = null);
/*
* Hook to handle access authorization, without throwing an exception.
*
* @param string $action
* @param object $object
*
* @return bool
* TODO: uncomment this method for next major release
*/
// public function hasAccess($action, $object = null);
}

View File

@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Admin;
@trigger_error(
'The '.__NAMESPACE__.'\Admin class is deprecated since version 3.1 and will be removed in 4.0.'
.' Use '.__NAMESPACE__.'\AbstractAdmin instead.',
E_USER_DEPRECATED
);
/**
* NEXT_MAJOR: remove this class.
*
* @deprecated since version 3.1, to be removed in 4.0. Use Sonata\AdminBundle\AbstractAdmin instead
*
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
abstract class Admin extends AbstractAdmin
{
}

View File

@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Admin;
@trigger_error(
'The '.__NAMESPACE__.'\AdminExtension class is deprecated since version 3.1 and will be removed in 4.0.'
.' Use '.__NAMESPACE__.'\AbstractAdminExtension instead.',
E_USER_DEPRECATED
);
/**
* NEXT_MAJOR: remove this class.
*
* @deprecated since version 3.1, to be removed in 4.0. Use Sonata\AdminBundle\AbstractAdminExtension instead
*
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
abstract class AdminExtension extends AbstractAdminExtension
{
}

View File

@@ -0,0 +1,207 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Admin;
use Knp\Menu\ItemInterface as MenuItemInterface;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Route\RouteCollection;
use Sonata\AdminBundle\Show\ShowMapper;
use Sonata\CoreBundle\Validator\ErrorElement;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
interface AdminExtensionInterface
{
/**
* @param FormMapper $formMapper
*/
public function configureFormFields(FormMapper $formMapper);
/**
* @param ListMapper $listMapper
*/
public function configureListFields(ListMapper $listMapper);
/**
* @param DatagridMapper $datagridMapper
*/
public function configureDatagridFilters(DatagridMapper $datagridMapper);
/**
* @param ShowMapper $showMapper
*/
public function configureShowFields(ShowMapper $showMapper);
/**
* @param AdminInterface $admin
* @param RouteCollection $collection
*/
public function configureRoutes(AdminInterface $admin, RouteCollection $collection);
/**
* DEPRECATED: Use configureTabMenu instead.
*
* NEXT_MAJOR: remove this method.
*
* @param AdminInterface $admin
* @param MenuItemInterface $menu
* @param string $action
* @param AdminInterface $childAdmin
*
* @deprecated
*/
public function configureSideMenu(AdminInterface $admin, MenuItemInterface $menu, $action, AdminInterface $childAdmin = null);
/**
* Builds the tab menu.
*
* @param AdminInterface $admin
* @param MenuItemInterface $menu
* @param string $action
* @param AdminInterface $childAdmin
*/
public function configureTabMenu(AdminInterface $admin, MenuItemInterface $menu, $action, AdminInterface $childAdmin = null);
/**
* @param AdminInterface $admin
* @param ErrorElement $errorElement
* @param mixed $object
*/
public function validate(AdminInterface $admin, ErrorElement $errorElement, $object);
/**
* @param AdminInterface $admin
* @param ProxyQueryInterface $query
* @param string $context
*/
public function configureQuery(AdminInterface $admin, ProxyQueryInterface $query, $context = 'list');
/**
* Get a chance to modify a newly created instance.
*
* @param AdminInterface $admin
* @param mixed $object
*/
public function alterNewInstance(AdminInterface $admin, $object);
/**
* Get a chance to modify object instance.
*
* @param AdminInterface $admin
* @param mixed $object
*/
public function alterObject(AdminInterface $admin, $object);
/**
* Get a chance to add persistent parameters.
*
* @param AdminInterface $admin
*
* @return array
*/
public function getPersistentParameters(AdminInterface $admin);
/**
* Return the controller access mapping.
*
* @param AdminInterface $admin
*
* @return array
*/
// TODO: Uncomment in next major release
// public function getAccessMapping(AdminInterface $admin);
/**
* Returns the list of batch actions.
*
* @param AdminInterface $admin
* @param array $actions
*
* @return array
*/
// TODO: Uncomment in next major release
// public function configureBatchActions(AdminInterface $admin, array $actions);
/**
* Get a chance to modify export fields.
*
* @param AdminInterface $admin
* @param string[] $fields
*
* @return string[]
*/
// TODO: Uncomment in next major release
// public function configureExportFields(AdminInterface $admin, array $fields);
/**
* @param AdminInterface $admin
* @param mixed $object
*/
public function preUpdate(AdminInterface $admin, $object);
/**
* @param AdminInterface $admin
* @param mixed $object
*/
public function postUpdate(AdminInterface $admin, $object);
/**
* @param AdminInterface $admin
* @param mixed $object
*/
public function prePersist(AdminInterface $admin, $object);
/**
* @param AdminInterface $admin
* @param mixed $object
*/
public function postPersist(AdminInterface $admin, $object);
/**
* @param AdminInterface $admin
* @param mixed $object
*/
public function preRemove(AdminInterface $admin, $object);
/**
* @param AdminInterface $admin
* @param mixed $object
*/
public function postRemove(AdminInterface $admin, $object);
/*
* Get all action buttons for an action
*
* @param AdminInterface $admin
* @param array $list
* @param string $action
* @param mixed $object
*
* @return array
*/
// TODO: Uncomment in next major release
// public function configureActionButtons(AdminInterface $admin, $list, $action, $object);
/*
* NEXT_MAJOR: Uncomment in next major release
*
* Returns a list of default filters
*
* @param AdminInterface $admin
* @param array $filterValues
*/
// public function configureDefaultFilterValues(AdminInterface $admin, array &$filterValues);
}

View File

@@ -0,0 +1,338 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Admin;
use Doctrine\Common\Inflector\Inflector;
use Doctrine\Common\Util\ClassUtils;
use Sonata\AdminBundle\Exception\NoValueException;
use Sonata\AdminBundle\Util\FormBuilderIterator;
use Sonata\AdminBundle\Util\FormViewIterator;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class AdminHelper
{
/**
* @var Pool
*/
protected $pool;
/**
* @param Pool $pool
*/
public function __construct(Pool $pool)
{
$this->pool = $pool;
}
/**
* @throws \RuntimeException
*
* @param FormBuilderInterface $formBuilder
* @param string $elementId
*
* @return FormBuilderInterface|null
*/
public function getChildFormBuilder(FormBuilderInterface $formBuilder, $elementId)
{
foreach (new FormBuilderIterator($formBuilder) as $name => $formBuilder) {
if ($name == $elementId) {
return $formBuilder;
}
}
return;
}
/**
* @param FormView $formView
* @param string $elementId
*
* @return null|FormView
*/
public function getChildFormView(FormView $formView, $elementId)
{
foreach (new \RecursiveIteratorIterator(new FormViewIterator($formView), \RecursiveIteratorIterator::SELF_FIRST) as $name => $formView) {
if ($name === $elementId) {
return $formView;
}
}
return;
}
/**
* NEXT_MAJOR: remove this method.
*
* @deprecated
*
* @param string $code
*
* @return AdminInterface
*/
public function getAdmin($code)
{
return $this->pool->getInstance($code);
}
/**
* Note:
* This code is ugly, but there is no better way of doing it.
* For now the append form element action used to add a new row works
* only for direct FieldDescription (not nested one).
*
* @throws \RuntimeException
*
* @param AdminInterface $admin
* @param object $subject
* @param string $elementId
*
* @return array
*
* @throws \Exception
*/
public function appendFormFieldElement(AdminInterface $admin, $subject, $elementId)
{
// retrieve the subject
$formBuilder = $admin->getFormBuilder();
$form = $formBuilder->getForm();
$form->setData($subject);
$form->handleRequest($admin->getRequest());
// get the field element
$childFormBuilder = $this->getChildFormBuilder($formBuilder, $elementId);
//Child form not found (probably nested one)
//if childFormBuilder was not found resulted in fatal error getName() method call on non object
if (!$childFormBuilder) {
$propertyAccessor = $this->pool->getPropertyAccessor();
$entity = $admin->getSubject();
$path = $this->getElementAccessPath($elementId, $entity);
$collection = $propertyAccessor->getValue($entity, $path);
if ($collection instanceof \Doctrine\ORM\PersistentCollection || $collection instanceof \Doctrine\ODM\MongoDB\PersistentCollection) {
//since doctrine 2.4
$entityClassName = $collection->getTypeClass()->getName();
} elseif ($collection instanceof \Doctrine\Common\Collections\Collection) {
$entityClassName = $this->getEntityClassName($admin, explode('.', preg_replace('#\[\d*?\]#', '', $path)));
} else {
throw new \Exception('unknown collection class');
}
$collection->add(new $entityClassName());
$propertyAccessor->setValue($entity, $path, $collection);
$fieldDescription = null;
} else {
// retrieve the FieldDescription
$fieldDescription = $admin->getFormFieldDescription($childFormBuilder->getName());
try {
$value = $fieldDescription->getValue($form->getData());
} catch (NoValueException $e) {
$value = null;
}
// retrieve the posted data
$data = $admin->getRequest()->get($formBuilder->getName());
if (!isset($data[$childFormBuilder->getName()])) {
$data[$childFormBuilder->getName()] = [];
}
$objectCount = count($value);
$postCount = count($data[$childFormBuilder->getName()]);
$fields = array_keys($fieldDescription->getAssociationAdmin()->getFormFieldDescriptions());
// for now, not sure how to do that
$value = [];
foreach ($fields as $name) {
$value[$name] = '';
}
// add new elements to the subject
while ($objectCount < $postCount) {
// append a new instance into the object
$this->addNewInstance($form->getData(), $fieldDescription);
++$objectCount;
}
$this->addNewInstance($form->getData(), $fieldDescription);
}
$finalForm = $admin->getFormBuilder()->getForm();
$finalForm->setData($subject);
// bind the data
$finalForm->setData($form->getData());
return [$fieldDescription, $finalForm];
}
/**
* Add a new instance to the related FieldDescriptionInterface value.
*
* @param object $object
* @param FieldDescriptionInterface $fieldDescription
*
* @throws \RuntimeException
*/
public function addNewInstance($object, FieldDescriptionInterface $fieldDescription)
{
$instance = $fieldDescription->getAssociationAdmin()->getNewInstance();
$mapping = $fieldDescription->getAssociationMapping();
$method = sprintf('add%s', Inflector::classify($mapping['fieldName']));
if (!method_exists($object, $method)) {
$method = rtrim($method, 's');
if (!method_exists($object, $method)) {
$method = sprintf('add%s', Inflector::classify(Inflector::singularize($mapping['fieldName'])));
if (!method_exists($object, $method)) {
throw new \RuntimeException(
sprintf('Please add a method %s in the %s class!', $method, ClassUtils::getClass($object))
);
}
}
}
$object->$method($instance);
}
/**
* Camelize a string.
*
* NEXT_MAJOR: remove this method.
*
* @static
*
* @param string $property
*
* @return string
*
* @deprecated Deprecated since version 3.1. Use \Doctrine\Common\Inflector\Inflector::classify() instead
*/
public function camelize($property)
{
@trigger_error(
sprintf(
'The %s method is deprecated since 3.1 and will be removed in 4.0. '.
'Use \Doctrine\Common\Inflector\Inflector::classify() instead.',
__METHOD__
),
E_USER_DEPRECATED
);
return Inflector::classify($property);
}
/**
* Get access path to element which works with PropertyAccessor.
*
* @param string $elementId expects string in format used in form id field.
* (uniqueIdentifier_model_sub_model or uniqueIdentifier_model_1_sub_model etc.)
* @param mixed $entity
*
* @return string
*
* @throws \Exception
*/
public function getElementAccessPath($elementId, $entity)
{
$propertyAccessor = $this->pool->getPropertyAccessor();
$idWithoutIdentifier = preg_replace('/^[^_]*_/', '', $elementId);
$initialPath = preg_replace('#(_(\d+)_)#', '[$2]_', $idWithoutIdentifier);
$parts = explode('_', $initialPath);
$totalPath = '';
$currentPath = '';
foreach ($parts as $part) {
$currentPath .= empty($currentPath) ? $part : '_'.$part;
$separator = empty($totalPath) ? '' : '.';
if ($this->pathExists($propertyAccessor, $entity, $totalPath.$separator.$currentPath)) {
$totalPath .= $separator.$currentPath;
$currentPath = '';
}
}
if (!empty($currentPath)) {
throw new \Exception(
sprintf('Could not get element id from %s Failing part: %s', $elementId, $currentPath)
);
}
return $totalPath;
}
/**
* Recursively find the class name of the admin responsible for the element at the end of an association chain.
*
* @param AdminInterface $admin
* @param array $elements
*
* @return string
*/
protected function getEntityClassName(AdminInterface $admin, $elements)
{
$element = array_shift($elements);
$associationAdmin = $admin->getFormFieldDescription($element)->getAssociationAdmin();
if (count($elements) == 0) {
return $associationAdmin->getClass();
}
return $this->getEntityClassName($associationAdmin, $elements);
}
/**
* Check if given path exists in $entity.
*
* @param PropertyAccessorInterface $propertyAccessor
* @param mixed $entity
* @param string $path
*
* @return bool
*
* @throws \RuntimeException
*/
private function pathExists(PropertyAccessorInterface $propertyAccessor, $entity, $path)
{
// Symfony <= 2.3 did not have isReadable method for PropertyAccessor
if (method_exists($propertyAccessor, 'isReadable')) {
return $propertyAccessor->isReadable($entity, $path);
}
try {
$propertyAccessor->getValue($entity, $path);
return true;
} catch (NoSuchPropertyException $e) {
return false;
} catch (UnexpectedTypeException $e) {
return false;
}
}
}

View File

@@ -0,0 +1,775 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Admin;
use Knp\Menu\FactoryInterface as MenuFactoryInterface;
use Sonata\AdminBundle\Builder\DatagridBuilderInterface;
use Sonata\AdminBundle\Builder\FormContractorInterface;
use Sonata\AdminBundle\Builder\ListBuilderInterface;
use Sonata\AdminBundle\Builder\RouteBuilderInterface;
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
use Sonata\AdminBundle\Security\Handler\SecurityHandlerInterface;
use Sonata\AdminBundle\Translator\LabelTranslatorStrategyInterface;
use Sonata\CoreBundle\Model\Metadata;
use Sonata\CoreBundle\Validator\ErrorElement;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
interface AdminInterface extends AccessRegistryInterface, FieldDescriptionRegistryInterface, LifecycleHookProviderInterface, MenuBuilderInterface, ParentAdminInterface, UrlGeneratorInterface
{
/**
* @param MenuFactoryInterface $menuFactory
*/
public function setMenuFactory(MenuFactoryInterface $menuFactory);
/**
* @return MenuFactoryInterface
*/
public function getMenuFactory();
/**
* @param FormContractorInterface $formContractor
*/
public function setFormContractor(FormContractorInterface $formContractor);
/**
* Set ListBuilder.
*
* @param ListBuilderInterface $listBuilder
*/
public function setListBuilder(ListBuilderInterface $listBuilder);
/**
* Get ListBuilder.
*
* @return ListBuilderInterface
*/
public function getListBuilder();
/**
* Set DatagridBuilder.
*
* @param DatagridBuilderInterface $datagridBuilder
*/
public function setDatagridBuilder(DatagridBuilderInterface $datagridBuilder);
/**
* Get DatagridBuilder.
*
* @return DatagridBuilderInterface
*/
public function getDatagridBuilder();
/**
* Set translator.
*
* @param TranslatorInterface $translator
*/
public function setTranslator(TranslatorInterface $translator);
/**
* Get translator.
*
* @return TranslatorInterface
*/
public function getTranslator();
/**
* @param Request $request
*/
public function setRequest(Request $request);
/**
* @param Pool $pool
*/
public function setConfigurationPool(Pool $pool);
/**
* Returns subjectClass/class/subclass name managed
* - subclass name if subclass parameter is defined
* - subject class name if subject is defined
* - class name if not.
*
* @return string
*/
public function getClass();
/**
* @param \Sonata\AdminBundle\Admin\FieldDescriptionInterface $fieldDescription
*/
public function attachAdminClass(FieldDescriptionInterface $fieldDescription);
/**
* @return \Sonata\AdminBundle\Datagrid\DatagridInterface
*/
public function getDatagrid();
/**
* Set base controller name.
*
* @param string $baseControllerName
*/
public function setBaseControllerName($baseControllerName);
/**
* Get base controller name.
*
* @return string
*/
public function getBaseControllerName();
/**
* @return \Sonata\AdminBundle\Model\ModelManagerInterface
*/
public function getModelManager();
/**
* @return string the manager type of the admin
*/
public function getManagerType();
/**
* @param string $context NEXT_MAJOR: remove this argument
*
* @return ProxyQueryInterface
*/
public function createQuery($context = 'list');
/**
* @return FormBuilderInterface the form builder
*/
public function getFormBuilder();
/**
* Returns a form depend on the given $object.
*
* @return Form
*/
public function getForm();
/**
* @return Request
*
* @throws \RuntimeException if no request is set
*/
public function getRequest();
/**
* @return bool true if a request object is linked to this Admin, false
* otherwise
*/
public function hasRequest();
/**
* @return string
*/
public function getCode();
/**
* @return string
*/
public function getBaseCodeRoute();
/**
* Return the roles and permissions per role
* - different permissions per role for the acl handler
* - one permission that has the same name as the role for the role handler
* This should be used by experimented users.
*
* @return array [role] => array([permission], [permission])
*/
public function getSecurityInformation();
/**
* @param FieldDescriptionInterface $parentFieldDescription
*/
public function setParentFieldDescription(FieldDescriptionInterface $parentFieldDescription);
/**
* Get parent field description.
*
* @return FieldDescriptionInterface The parent field description
*/
public function getParentFieldDescription();
/**
* Returns true if the Admin is linked to a parent FieldDescription.
*
* @return bool
*/
public function hasParentFieldDescription();
/**
* translate a message id.
*
* NEXT_MAJOR: remove this method
*
* @param string $id
* @param array $parameters
* @param null $domain
* @param null $locale
*
* @return string the translated string
*
* @deprecated since 3.9, to be removed in 4.0
*/
public function trans($id, array $parameters = [], $domain = null, $locale = null);
/**
* Returns the parameter representing request id, ie: id or childId.
*
* @return string
*/
public function getIdParameter();
/**
* Returns true if the route $name is available.
*
* @param string $name
*
* @return bool
*/
public function hasRoute($name);
/**
* Check the current request is given route or not.
*
* TODO: uncomment this method before releasing 4.0
*
* ```
* $this->isCurrentRoute('create'); // is create page?
* $this->isCurrentRoute('edit', 'some.admin.code'); // is some.admin.code admin's edit page?
* ```
*
* @param string $name
* @param string $adminCode
*
* @return bool
*/
// public function isCurrentRoute($name, $adminCode = null);
/**
* @param SecurityHandlerInterface $securityHandler
*/
public function setSecurityHandler(SecurityHandlerInterface $securityHandler);
/**
* @return SecurityHandlerInterface|null
*/
public function getSecurityHandler();
/**
* @param string $name
* @param object|null $object
*
* @return bool
*/
public function isGranted($name, $object = null);
/**
* @param mixed $entity
*
* @return string a string representation of the identifiers for this instance
*/
public function getNormalizedIdentifier($entity);
/**
* Shorthand method for templating.
*
* @param object $entity
*
* @return mixed
*/
public function id($entity);
/**
* @param ValidatorInterface|LegacyValidatorInterface $validator
*/
public function setValidator($validator);
/**
* @return ValidatorInterface|LegacyValidatorInterface
*/
public function getValidator();
/**
* @return array
*/
public function getShow();
/**
* @param array $formTheme
*/
public function setFormTheme(array $formTheme);
/**
* @return array
*/
public function getFormTheme();
/**
* @param array $filterTheme
*/
public function setFilterTheme(array $filterTheme);
/**
* @return array
*/
public function getFilterTheme();
/**
* @param AdminExtensionInterface $extension
*/
public function addExtension(AdminExtensionInterface $extension);
/**
* Returns an array of extension related to the current Admin.
*
* @return AdminExtensionInterface[]
*/
public function getExtensions();
/**
* @param RouteBuilderInterface $routeBuilder
*/
public function setRouteBuilder(RouteBuilderInterface $routeBuilder);
/**
* @return RouteBuilderInterface
*/
public function getRouteBuilder();
/**
* @param mixed $object
*
* @return string
*/
public function toString($object);
/**
* @param LabelTranslatorStrategyInterface $labelTranslatorStrategy
*/
public function setLabelTranslatorStrategy(LabelTranslatorStrategyInterface $labelTranslatorStrategy);
/**
* @return LabelTranslatorStrategyInterface
*/
public function getLabelTranslatorStrategy();
/**
* Returning true will enable preview mode for
* the target entity and show a preview button
* when editing/creating an entity.
*
* @return bool
*/
public function supportsPreviewMode();
/**
* @return mixed a new object instance
*/
public function getNewInstance();
/**
* @param string $uniqId
*/
public function setUniqid($uniqId);
/**
* Returns the uniqid.
*
* @return int
*/
public function getUniqid();
/**
* @param mixed $id
*
* @return mixed
*/
public function getObject($id);
/**
* @param object $subject
*/
public function setSubject($subject);
/**
* @return mixed
*/
public function getSubject();
/**
* Returns a list FieldDescription.
*
* @param string $name
*
* @return FieldDescriptionInterface
*/
public function getListFieldDescription($name);
/**
* Returns true if the list FieldDescription exists.
*
* @param string $name
*
* @return bool
*/
public function hasListFieldDescription($name);
/**
* Returns the collection of list FieldDescriptions.
*
* @return array
*/
public function getListFieldDescriptions();
/**
* Returns the array of allowed export formats.
*
* @return array
*/
public function getExportFormats();
/**
* Returns SourceIterator.
*
* @return \Exporter\Source\SourceIteratorInterface
*/
public function getDataSourceIterator();
public function configure();
/**
* Call before the batch action, allow you to alter the query and the idx.
*
* @param string $actionName
* @param ProxyQueryInterface $query
* @param array $idx
* @param bool $allElements
*/
public function preBatchAction($actionName, ProxyQueryInterface $query, array &$idx, $allElements);
/**
* Return array of filter parameters.
*
* @return array
*/
public function getFilterParameters();
/**
* Return true if the Admin is related to a subject.
*
* @return bool
*/
public function hasSubject();
/**
* NEXT_MAJOR: remove this method.
*
* @param ErrorElement $errorElement
* @param mixed $object
*
* @deprecated this feature cannot be stable, use a custom validator,
* the feature will be removed with Symfony 2.2
*/
public function validate(ErrorElement $errorElement, $object);
/**
* @param string $context
*
* @return bool
*/
public function showIn($context);
/**
* Add object security, fe. make the current user owner of the object.
*
* @param mixed $object
*/
public function createObjectSecurity($object);
/**
* @return AdminInterface
*/
public function getParent();
/**
* @param AdminInterface $admin
*/
public function setParent(AdminInterface $admin);
/**
* Returns true if the Admin class has an Parent Admin defined.
*
* @return bool
*/
public function isChild();
/**
* Returns template.
*
* @param string $name
*
* @return null|string
*/
public function getTemplate($name);
/**
* Set the translation domain.
*
* @param string $translationDomain the translation domain
*/
public function setTranslationDomain($translationDomain);
/**
* Returns the translation domain.
*
* @return string the translation domain
*/
public function getTranslationDomain();
/**
* Return the form groups.
*
* @return array
*/
public function getFormGroups();
/**
* Set the form groups.
*
* @param array $formGroups
*/
public function setFormGroups(array $formGroups);
public function getFormTabs();
public function setFormTabs(array $formTabs);
public function getShowTabs();
public function setShowTabs(array $showTabs);
/**
* Remove a form group field.
*
* @param string $key
*/
public function removeFieldFromFormGroup($key);
/**
* Returns the show groups.
*
* @return array
*/
public function getShowGroups();
/**
* Set the show groups.
*
* @param array $showGroups
*/
public function setShowGroups(array $showGroups);
/**
* Reorder items in showGroup.
*
* @param string $group
* @param array $keys
*/
public function reorderShowGroup($group, array $keys);
/**
* add a FieldDescription.
*
* @param string $name
* @param FieldDescriptionInterface $fieldDescription
*/
public function addFormFieldDescription($name, FieldDescriptionInterface $fieldDescription);
/**
* Remove a FieldDescription.
*
* @param string $name
*/
public function removeFormFieldDescription($name);
/**
* Returns true if this admin uses ACL.
*
* @return bool
*/
public function isAclEnabled();
/**
* Sets the list of supported sub classes.
*
* @param array $subClasses the list of sub classes
*/
public function setSubClasses(array $subClasses);
/**
* Returns true if the admin has the sub classes.
*
* @param string $name The name of the sub class
*
* @return bool
*/
public function hasSubClass($name);
/**
* Returns true if a subclass is currently active.
*
* @return bool
*/
public function hasActiveSubClass();
/**
* Returns the currently active sub class.
*
* @return string the active sub class
*/
public function getActiveSubClass();
/**
* Returns the currently active sub class code.
*
* @return string the code for active sub class
*/
public function getActiveSubclassCode();
/**
* Returns the list of batchs actions.
*
* @return array the list of batchs actions
*/
public function getBatchActions();
/**
* Returns Admin`s label.
*
* @return string
*/
public function getLabel();
/**
* Returns an array of persistent parameters.
*
* @return array
*/
public function getPersistentParameters();
/**
* NEXT_MAJOR: remove this signature
* Get breadcrumbs for $action.
*
* @param string $action
*
* @return mixed array|Traversable
*/
public function getBreadcrumbs($action);
/**
* Set the current child status.
*
* @param bool $currentChild
*/
public function setCurrentChild($currentChild);
/**
* Returns the current child status.
*
* @return bool
*/
public function getCurrentChild();
/**
* Get translation label using the current TranslationStrategy.
*
* @param string $label
* @param string $context
* @param string $type
*
* @return string
*/
public function getTranslationLabel($label, $context = '', $type = '');
/**
* @param $object
*
* @return Metadata
*/
public function getObjectMetadata($object);
/**
* @return array
*/
public function getListModes();
/**
* @param string $mode
*/
public function setListMode($mode);
/**
* return the list mode.
*
* @return string
*/
public function getListMode();
/*
* Configure buttons for an action
*
* @param string $action
* @param object $object
*
*/
// public function configureActionButtons($action, $object = null);
//TODO: uncomment this method for 4.0
/*
* Returns the result link for an object.
*
* @param mixed $object
*
* @return string|null
*/
//public function getSearchResultLink($object)
// TODO: uncomment this method in 4.0
// /**
// * Setting to true will enable mosaic button for the admin screen.
// * Setting to false will hide mosaic button for the admin screen.
// *
// * @param bool $isShown
// */
// public function showMosaicButton($isShown);
/*
* Checks if a filter type is set to a default value
*
* @param string $name
*
* @return bool
*/
// NEXT_MAJOR: uncomment this method in 4.0
// public function isDefaultFilter($name);
}

View File

@@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Admin;
/**
* @author Jules Lamur <contact@juleslamur.fr>
*/
interface AdminTreeInterface
{
/**
* Returns the root ancestor or itself if not a child.
*
* @return AdminInterface
*/
public function getRootAncestor();
/**
* Returns the depth of the admin.
* e.g. 0 if not a child; 2 if child of a child; etc...
*
* @return int
*/
public function getChildDepth();
/**
* Returns the current leaf child admin instance,
* or null if there's no current child.
*
* @return AdminInterface|null
*/
public function getCurrentLeafChildAdmin();
}

View File

@@ -0,0 +1,512 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Admin;
use Doctrine\Common\Inflector\Inflector;
use Sonata\AdminBundle\Exception\NoValueException;
/**
* A FieldDescription hold the information about a field. A typical
* admin instance contains different collections of fields.
*
* - form: used by the form
* - list: used by the list
* - filter: used by the list filter
*
* Some options are global across the different contexts, other are
* context specifics.
*
* Global options :
* - type (m): define the field type (use to tweak the form or the list)
* - template (o) : the template used to render the field
* - name (o) : the name used (label in the form, title in the list)
* - link_parameters (o) : add link parameter to the related Admin class when
* the Admin.generateUrl is called
* - code : the method name to retrieve the related value
* - associated_tostring : (deprecated, use associated_property option)
* the method to retrieve the "string" representation
* of the collection element.
* - associated_property : property path to retrieve the "string" representation
* of the collection element.
*
* Form Field options :
* - field_type (o): the widget class to use to render the field
* - field_options (o): the options to give to the widget
* - edit (o) : list|inline|standard (only used for associated admin)
* - list : open a popup where the user can search, filter and click on one field
* to select one item
* - inline : the associated form admin is embedded into the current form
* - standard : the associated admin is created through a popup
*
* List Field options :
* - identifier (o): if set to true a link appear on to edit the element
*
* Filter Field options :
* - options (o): options given to the Filter object
* - field_type (o): the widget class to use to render the field
* - field_options (o): the options to give to the widget
*
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
abstract class BaseFieldDescription implements FieldDescriptionInterface
{
/**
* @var string the field name
*/
protected $name;
/**
* @var string|int the type
*/
protected $type;
/**
* @var string|int the original mapping type
*/
protected $mappingType;
/**
* @var string the field name (of the form)
*/
protected $fieldName;
/**
* @var array the ORM association mapping
*/
protected $associationMapping;
/**
* @var array the ORM field information
*/
protected $fieldMapping;
/**
* @var array the ORM parent mapping association
*/
protected $parentAssociationMappings;
/**
* @var string the template name
*/
protected $template;
/**
* @var array the option collection
*/
protected $options = [];
/**
* @var AdminInterface|null the parent Admin instance
*/
protected $parent = null;
/**
* @var AdminInterface the related admin instance
*/
protected $admin;
/**
* @var AdminInterface the associated admin class if the object is associated to another entity
*/
protected $associationAdmin;
/**
* @var string the help message to display
*/
protected $help;
/**
* {@inheritdoc}
*/
public function setFieldName($fieldName)
{
$this->fieldName = $fieldName;
}
/**
* {@inheritdoc}
*/
public function getFieldName()
{
return $this->fieldName;
}
/**
* {@inheritdoc}
*/
public function setName($name)
{
$this->name = $name;
if (!$this->getFieldName()) {
$this->setFieldName(substr(strrchr('.'.$name, '.'), 1));
}
}
/**
* {@inheritdoc}
*/
public function getName()
{
return $this->name;
}
/**
* {@inheritdoc}
*/
public function getOption($name, $default = null)
{
return isset($this->options[$name]) ? $this->options[$name] : $default;
}
/**
* {@inheritdoc}
*/
public function setOption($name, $value)
{
$this->options[$name] = $value;
}
/**
* {@inheritdoc}
*/
public function setOptions(array $options)
{
// set the type if provided
if (isset($options['type'])) {
$this->setType($options['type']);
unset($options['type']);
}
// remove property value
if (isset($options['template'])) {
$this->setTemplate($options['template']);
unset($options['template']);
}
// set help if provided
if (isset($options['help'])) {
$this->setHelp($options['help']);
unset($options['help']);
}
// set default placeholder
if (!isset($options['placeholder'])) {
$options['placeholder'] = 'short_object_description_placeholder';
}
if (!isset($options['link_parameters'])) {
$options['link_parameters'] = [];
}
$this->options = $options;
}
/**
* {@inheritdoc}
*/
public function getOptions()
{
return $this->options;
}
/**
* {@inheritdoc}
*/
public function setTemplate($template)
{
$this->template = $template;
}
/**
* {@inheritdoc}
*/
public function getTemplate()
{
return $this->template;
}
/**
* {@inheritdoc}
*/
public function setType($type)
{
$this->type = $type;
}
/**
* {@inheritdoc}
*/
public function getType()
{
return $this->type;
}
/**
* {@inheritdoc}
*/
public function setParent(AdminInterface $parent)
{
$this->parent = $parent;
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return $this->parent;
}
/**
* {@inheritdoc}
*/
public function getAssociationMapping()
{
return $this->associationMapping;
}
/**
* {@inheritdoc}
*/
public function getFieldMapping()
{
return $this->fieldMapping;
}
/**
* {@inheritdoc}
*/
public function getParentAssociationMappings()
{
return $this->parentAssociationMappings;
}
/**
* {@inheritdoc}
*/
public function setAssociationAdmin(AdminInterface $associationAdmin)
{
$this->associationAdmin = $associationAdmin;
$this->associationAdmin->setParentFieldDescription($this);
}
/**
* {@inheritdoc}
*/
public function getAssociationAdmin()
{
return $this->associationAdmin;
}
/**
* {@inheritdoc}
*/
public function hasAssociationAdmin()
{
return $this->associationAdmin !== null;
}
/**
* {@inheritdoc}
*/
public function getFieldValue($object, $fieldName)
{
if ($this->isVirtual()) {
return;
}
$camelizedFieldName = Inflector::classify($fieldName);
$getters = [];
$parameters = [];
// prefer method name given in the code option
if ($this->getOption('code')) {
$getters[] = $this->getOption('code');
}
// parameters for the method given in the code option
if ($this->getOption('parameters')) {
$parameters = $this->getOption('parameters');
}
$getters[] = 'get'.$camelizedFieldName;
$getters[] = 'is'.$camelizedFieldName;
$getters[] = 'has'.$camelizedFieldName;
foreach ($getters as $getter) {
if (method_exists($object, $getter)) {
return call_user_func_array([$object, $getter], $parameters);
}
}
if (method_exists($object, '__call')) {
return call_user_func_array([$object, '__call'], [$fieldName, $parameters]);
}
if (isset($object->{$fieldName})) {
return $object->{$fieldName};
}
throw new NoValueException(sprintf('Unable to retrieve the value of `%s`', $this->getName()));
}
/**
* {@inheritdoc}
*/
public function setAdmin(AdminInterface $admin)
{
$this->admin = $admin;
}
/**
* {@inheritdoc}
*/
public function getAdmin()
{
return $this->admin;
}
/**
* {@inheritdoc}
*/
public function mergeOption($name, array $options = [])
{
if (!isset($this->options[$name])) {
$this->options[$name] = [];
}
if (!is_array($this->options[$name])) {
throw new \RuntimeException(sprintf('The key `%s` does not point to an array value', $name));
}
$this->options[$name] = array_merge($this->options[$name], $options);
}
/**
* {@inheritdoc}
*/
public function mergeOptions(array $options = [])
{
$this->setOptions(array_merge_recursive($this->options, $options));
}
/**
* {@inheritdoc}
*/
public function setMappingType($mappingType)
{
$this->mappingType = $mappingType;
}
/**
* {@inheritdoc}
*/
public function getMappingType()
{
return $this->mappingType;
}
/**
* Camelize a string.
*
* NEXT_MAJOR: remove this method.
*
* @static
*
* @param string $property
*
* @return string
*
* @deprecated Deprecated since version 3.1. Use \Doctrine\Common\Inflector\Inflector::classify() instead
*/
public static function camelize($property)
{
@trigger_error(
sprintf(
'The %s method is deprecated since 3.1 and will be removed in 4.0. '.
'Use \Doctrine\Common\Inflector\Inflector::classify() instead.',
__METHOD__
),
E_USER_DEPRECATED
);
return Inflector::classify($property);
}
/**
* Defines the help message.
*
* @param string $help
*/
public function setHelp($help)
{
$this->help = $help;
}
/**
* {@inheritdoc}
*/
public function getHelp()
{
return $this->help;
}
/**
* {@inheritdoc}
*/
public function getLabel()
{
return $this->getOption('label');
}
/**
* {@inheritdoc}
*/
public function isSortable()
{
return false !== $this->getOption('sortable', false);
}
/**
* {@inheritdoc}
*/
public function getSortFieldMapping()
{
return $this->getOption('sort_field_mapping');
}
/**
* {@inheritdoc}
*/
public function getSortParentAssociationMapping()
{
return $this->getOption('sort_parent_association_mappings');
}
/**
* {@inheritdoc}
*/
public function getTranslationDomain()
{
return $this->getOption('translation_domain') ?: $this->getAdmin()->getTranslationDomain();
}
/**
* Return true if field is virtual.
*
* @return bool
*/
public function isVirtual()
{
return false !== $this->getOption('virtual_field', false);
}
}

View File

@@ -0,0 +1,178 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Admin;
use Knp\Menu\ItemInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Stateless breadcrumbs builder (each method needs an Admin object).
*
* @author Grégoire Paris <postmaster@greg0ire.fr>
*/
final class BreadcrumbsBuilder implements BreadcrumbsBuilderInterface
{
/**
* @var string[]
*/
protected $config = [];
/**
* @param string[] $config
*/
public function __construct(array $config = [])
{
$resolver = new OptionsResolver();
$this->configureOptions($resolver);
$this->config = $resolver->resolve($config);
}
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'child_admin_route' => 'edit',
]);
}
/**
* {@inheritdoc}
*/
public function getBreadcrumbs(AdminInterface $admin, $action)
{
$breadcrumbs = [];
if ($admin->isChild()) {
return $this->getBreadcrumbs($admin->getParent(), $action);
}
$menu = $this->buildBreadcrumbs($admin, $action);
do {
$breadcrumbs[] = $menu;
} while ($menu = $menu->getParent());
$breadcrumbs = array_reverse($breadcrumbs);
array_shift($breadcrumbs);
return $breadcrumbs;
}
/**
* {@inheritdoc}
* NEXT_MAJOR : make this method private.
*/
public function buildBreadcrumbs(AdminInterface $admin, $action, ItemInterface $menu = null)
{
if (!$menu) {
$menu = $admin->getMenuFactory()->createItem('root');
$menu = $menu->addChild(
'link_breadcrumb_dashboard',
[
'uri' => $admin->getRouteGenerator()->generate('sonata_admin_dashboard'),
'extras' => ['translation_domain' => 'SonataAdminBundle'],
]
);
}
$menu = $this->createMenuItem(
$admin,
$menu,
sprintf('%s_list', $admin->getClassnameLabel()),
$admin->getTranslationDomain(),
[
'uri' => $admin->hasRoute('list') && $admin->hasAccess('list') ?
$admin->generateUrl('list') :
null,
]
);
$childAdmin = $admin->getCurrentChildAdmin();
if ($childAdmin) {
$id = $admin->getRequest()->get($admin->getIdParameter());
$menu = $menu->addChild(
$admin->toString($admin->getSubject()),
[
'uri' => $admin->hasRoute($this->config['child_admin_route']) && $admin->hasAccess($this->config['child_admin_route'], $admin->getSubject()) ?
$admin->generateUrl($this->config['child_admin_route'], ['id' => $id]) :
null,
'extras' => [
'translation_domain' => false,
],
]
);
$menu->setExtra('safe_label', false);
return $this->buildBreadcrumbs($childAdmin, $action, $menu);
}
if ('list' === $action) {
$menu->setUri(false);
} elseif ('create' !== $action && $admin->hasSubject()) {
$menu = $menu->addChild($admin->toString($admin->getSubject()), [
'extras' => [
'translation_domain' => false,
],
]);
} else {
$menu = $this->createMenuItem(
$admin,
$menu,
sprintf('%s_%s', $admin->getClassnameLabel(), $action),
$admin->getTranslationDomain()
);
}
return $menu;
}
/**
* Creates a new menu item from a simple name. The name is normalized and
* translated with the specified translation domain.
*
* @param AdminInterface $admin used for translation
* @param ItemInterface $menu will be modified and returned
* @param string $name the source of the final label
* @param string $translationDomain for label translation
* @param array $options menu item options
*
* @return ItemInterface
*/
private function createMenuItem(
AdminInterface $admin,
ItemInterface $menu,
$name,
$translationDomain = null,
$options = []
) {
$options = array_merge([
'extras' => [
'translation_domain' => $translationDomain,
],
], $options);
return $menu->addChild(
$admin->getLabelTranslatorStrategy()->getLabel(
$name,
'breadcrumb',
'link'
),
$options
);
}
}

View File

@@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Admin;
use Knp\Menu\ItemInterface;
/**
* Builds a breacrumbs. There is a dependency on the AdminInterface because
* this object holds useful object to deal with this task, but there is
* probably a better design.
*
* @author Grégoire Paris <postmaster@greg0ire.fr>
*/
interface BreadcrumbsBuilderInterface
{
/**
* Get breadcrumbs for $action.
*
* @param AdminInterface $admin
* @param string $action the name of the action we want to get a
* breadcrumbs for
*
* @return mixed array|Traversable the breadcrumbs
*/
public function getBreadcrumbs(AdminInterface $admin, $action);
/**
* Builds breadcrumbs for $action, starting from $menu.
*
* Note: the method will be called by the top admin instance (parent => child)
* NEXT_MAJOR : remove this method from the public interface.
*
* @param AdminInterface $admin
* @param string $action
* @param ItemInterface|null $menu
*/
public function buildBreadcrumbs(
AdminInterface $admin,
$action,
ItemInterface $menu = null
);
}

View File

@@ -0,0 +1,95 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Admin\Extension;
use Sonata\AdminBundle\Admin\AbstractAdminExtension;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Model\LockInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
/**
* @author Emmanuel Vella <vella.emmanuel@gmail.com>
*/
class LockExtension extends AbstractAdminExtension
{
/**
* @var string
*/
protected $fieldName = '_lock_version';
/**
* {@inheritdoc}
*/
public function configureFormFields(FormMapper $form)
{
$admin = $form->getAdmin();
$formBuilder = $form->getFormBuilder();
// PHP 5.3 BC
$fieldName = $this->fieldName;
$formBuilder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($admin, $fieldName) {
$data = $event->getData();
$form = $event->getForm();
if (null === $data || $form->getParent()) {
return;
}
$modelManager = $admin->getModelManager();
if (!$modelManager instanceof LockInterface) {
return;
}
if (null === $lockVersion = $modelManager->getLockVersion($data)) {
return;
}
$form->add(
$fieldName,
// NEXT_MAJOR: remove the check and add the FQCN
method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')
? 'Symfony\Component\Form\Extension\Core\Type\HiddenType'
: 'hidden',
[
'mapped' => false,
'data' => $lockVersion,
]
);
});
}
/**
* {@inheritdoc}
*/
public function preUpdate(AdminInterface $admin, $object)
{
if (!$admin->hasRequest() || !$data = $admin->getRequest()->get($admin->getUniqid())) {
return;
}
if (!isset($data[$this->fieldName])) {
return;
}
$modelManager = $admin->getModelManager();
if (!$modelManager instanceof LockInterface) {
return;
}
$modelManager->lock($object, $data[$this->fieldName]);
}
}

View File

@@ -0,0 +1,127 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Admin;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class FieldDescriptionCollection implements \ArrayAccess, \Countable
{
/**
* @var FieldDescriptionInterface[]
*/
protected $elements = [];
/**
* @param FieldDescriptionInterface $fieldDescription
*/
public function add(FieldDescriptionInterface $fieldDescription)
{
$this->elements[$fieldDescription->getName()] = $fieldDescription;
}
/**
* @return array
*/
public function getElements()
{
return $this->elements;
}
/**
* @param string $name
*
* @return bool
*/
public function has($name)
{
return array_key_exists($name, $this->elements);
}
/**
* @throws \InvalidArgumentException
*
* @param string $name
*
* @return FieldDescriptionInterface
*/
public function get($name)
{
if ($this->has($name)) {
return $this->elements[$name];
}
throw new \InvalidArgumentException(sprintf('Element "%s" does not exist.', $name));
}
/**
* @param string $name
*/
public function remove($name)
{
if ($this->has($name)) {
unset($this->elements[$name]);
}
}
/**
* {@inheritdoc}
*/
public function offsetExists($offset)
{
return $this->has($offset);
}
/**
* {@inheritdoc}
*/
public function offsetGet($offset)
{
return $this->get($offset);
}
/**
* {@inheritdoc}
*/
public function offsetSet($offset, $value)
{
throw new \RuntimeException('Cannot set value, use add');
}
/**
* {@inheritdoc}
*/
public function offsetUnset($offset)
{
$this->remove($offset);
}
/**
* {@inheritdoc}
*/
public function count()
{
return count($this->elements);
}
/**
* @param array $keys
*/
public function reorder(array $keys)
{
if ($this->has('batch')) {
array_unshift($keys, 'batch');
}
$this->elements = array_merge(array_flip($keys), $this->elements);
}
}

View File

@@ -0,0 +1,290 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Admin;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
interface FieldDescriptionInterface
{
/**
* set the field name.
*
* @param string $fieldName
*/
public function setFieldName($fieldName);
/**
* return the field name.
*
* @return string the field name
*/
public function getFieldName();
/**
* Set the name.
*
* @param string $name
*/
public function setName($name);
/**
* Return the name, the name can be used as a form label or table header.
*
* @return string the name
*/
public function getName();
/**
* Return the value represented by the provided name.
*
* @param string $name
* @param null $default
*
* @return array|null the value represented by the provided name
*/
public function getOption($name, $default = null);
/**
* Define an option, an option is has a name and a value.
*
* @param string $name
* @param mixed $value
*/
public function setOption($name, $value);
/**
* Define the options value, if the options array contains the reserved keywords
* - type
* - template.
*
* Then the value are copied across to the related property value
*
* @param array $options
*/
public function setOptions(array $options);
/**
* return options.
*
* @return array options
*/
public function getOptions();
/**
* return the template used to render the field.
*
* @param string $template
*/
public function setTemplate($template);
/**
* return the template name.
*
* @return string the template name
*/
public function getTemplate();
/**
* return the field type, the type is a mandatory field as it used to select the correct template
* or the logic associated to the current FieldDescription object.
*
* @param string $type
*/
public function setType($type);
/**
* return the type.
*
* @return int|string
*/
public function getType();
/**
* set the parent Admin (only used in nested admin).
*
* @param AdminInterface $parent
*/
public function setParent(AdminInterface $parent);
/**
* return the parent Admin (only used in nested admin).
*
* @return AdminInterface
*/
public function getParent();
/**
* Define the association mapping definition.
*
* @param array $associationMapping
*/
public function setAssociationMapping($associationMapping);
/**
* return the association mapping definition.
*
* @return array
*/
public function getAssociationMapping();
/**
* return the related Target Entity.
*
* @return string|null
*/
public function getTargetEntity();
/**
* set the field mapping information.
*
* @param array $fieldMapping
*/
public function setFieldMapping($fieldMapping);
/**
* return the field mapping definition.
*
* @return array the field mapping definition
*/
public function getFieldMapping();
/**
* set the parent association mappings information.
*
* @param array $parentAssociationMappings
*/
public function setParentAssociationMappings(array $parentAssociationMappings);
/**
* return the parent association mapping definitions.
*
* @return array the parent association mapping definitions
*/
public function getParentAssociationMappings();
/**
* set the association admin instance (only used if the field is linked to an Admin).
*
* @param AdminInterface $associationAdmin the associated admin
*/
public function setAssociationAdmin(AdminInterface $associationAdmin);
/**
* return the associated Admin instance (only used if the field is linked to an Admin).
*
* @return AdminInterface
*/
public function getAssociationAdmin();
/**
* return true if the FieldDescription is linked to an identifier field.
*
* @return bool
*/
public function isIdentifier();
/**
* return the value linked to the description.
*
* @param mixed $object
*
* @return bool|mixed
*/
public function getValue($object);
/**
* set the admin class linked to this FieldDescription.
*
* @param AdminInterface $admin
*/
public function setAdmin(AdminInterface $admin);
/**
* @return AdminInterface the admin class linked to this FieldDescription
*/
public function getAdmin();
/**
* merge option values related to the provided option name.
*
* @throws \RuntimeException
*
* @param string $name
* @param array $options
*/
public function mergeOption($name, array $options = []);
/**
* merge options values.
*
* @param array $options
*/
public function mergeOptions(array $options = []);
/**
* set the original mapping type (only used if the field is linked to an entity).
*
* @param string|int $mappingType
*/
public function setMappingType($mappingType);
/**
* return the mapping type.
*
* @return int|string
*/
public function getMappingType();
/**
* return the label to use for the current field.
*
* @return string
*/
public function getLabel();
/**
* Return the translation domain to use for the current field.
*
* @return string
*/
public function getTranslationDomain();
/**
* Return true if field is sortable.
*
* @return bool
*/
public function isSortable();
/**
* return the field mapping definition used when sorting.
*
* @return array the field mapping definition
*/
public function getSortFieldMapping();
/**
* return the parent association mapping definitions used when sorting.
*
* @return array the parent association mapping definitions
*/
public function getSortParentAssociationMapping();
/**
* @param object $object
* @param string $fieldName
*
* @return mixed
*/
public function getFieldValue($object, $fieldName);
}

View File

@@ -0,0 +1,122 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Admin;
/**
* Implementations should provide arrays of FieldDescriptionInterface instances.
*
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
interface FieldDescriptionRegistryInterface
{
/**
* Return FormFieldDescription.
*
* @param string $name
*
* @return FieldDescriptionInterface
*/
public function getFormFieldDescription($name);
/**
* Build and return the collection of form FieldDescription.
*
* @return FieldDescriptionInterface[] collection of form FieldDescription
*/
public function getFormFieldDescriptions();
/**
* Returns true if the admin has a FieldDescription with the given $name.
*
* @param string $name
*
* @return bool
*/
public function hasShowFieldDescription($name);
/**
* Adds a FieldDescription.
*
* @param string $name
* @param FieldDescriptionInterface $fieldDescription
*/
public function addShowFieldDescription($name, FieldDescriptionInterface $fieldDescription);
/**
* Removes a ShowFieldDescription.
*
* @param string $name
*/
public function removeShowFieldDescription($name);
/**
* Adds a list FieldDescription.
*
* @param string $name
* @param FieldDescriptionInterface $fieldDescription
*/
public function addListFieldDescription($name, FieldDescriptionInterface $fieldDescription);
/**
* Removes a list FieldDescription.
*
* @param string $name
*/
public function removeListFieldDescription($name);
/**
* Returns a list depend on the given $object.
*
* @return FieldDescriptionCollection
*/
public function getList();
/**
* Returns true if the filter FieldDescription exists.
*
* @param string $name
*
* @return bool
*/
public function hasFilterFieldDescription($name);
/**
* Adds a filter FieldDescription.
*
* @param string $name
* @param FieldDescriptionInterface $fieldDescription
*/
public function addFilterFieldDescription($name, FieldDescriptionInterface $fieldDescription);
/**
* Removes a filter FieldDescription.
*
* @param string $name
*/
public function removeFilterFieldDescription($name);
/**
* Returns the filter FieldDescription collection.
*
* @return FieldDescriptionInterface[]
*/
public function getFilterFieldDescriptions();
/**
* Returns a filter FieldDescription.
*
* @param string $name
*
* @return FieldDescriptionInterface|null
*/
public function getFilterFieldDescription($name);
}

View File

@@ -0,0 +1,82 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Admin;
/**
* This interface can be implemented to provide hooks that will be called
* during the lifecycle of the object.
*
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
interface LifecycleHookProviderInterface
{
/**
* This method should call preUpdate, do the update, and call postUpdate.
*
* @param object $object
*
* @return object
*/
public function update($object);
/**
* This method should call prePersist, do the creation, and call postPersist.
*
* @param object $object
*
* @return object
*/
public function create($object);
/**
* This method should call preRemove, do the removal, and call postRemove.
*
* @param object $object
*/
public function delete($object);
//NEXT_MAJOR: uncomment this method for 4.0
// /**
// * @param object $object
// */
// public function preValidate($object);
/**
* @param object $object
*/
public function preUpdate($object);
/**
* @param object $object
*/
public function postUpdate($object);
/**
* @param object $object
*/
public function prePersist($object);
/**
* @param object $object
*/
public function postPersist($object);
/**
* @param object $object
*/
public function preRemove($object);
/**
* @param object $object
*/
public function postRemove($object);
}

View File

@@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Admin;
use Knp\Menu\ItemInterface;
/**
* This interface can be implemented by admins that need to build menus.
*
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
interface MenuBuilderInterface
{
/**
* NEXT_MAJOR: remove this method.
*
* @param string $action
* @param AdminInterface $childAdmin
*
* @return ItemInterface|bool
*
* @deprecated Use buildTabMenu instead
*/
public function buildSideMenu($action, AdminInterface $childAdmin = null);
/**
* Build the tab menu related to the current action.
*
* @param string $action
* @param AdminInterface $childAdmin
*
* @return ItemInterface|bool
*/
public function buildTabMenu($action, AdminInterface $childAdmin = null);
}

View File

@@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Admin;
/**
* This interface can be used to implement an admin that can have children
* admins, meaning admin that correspond to objects with a relationship with
* the object managed by this admin.
*
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
interface ParentAdminInterface
{
/**
* add an Admin child to the current one.
*
* @param AdminInterface $child
*/
public function addChild(AdminInterface $child);
/**
* Returns true or false if an Admin child exists for the given $code.
*
* @param string $code Admin code
*
* @return bool True if child exist, false otherwise
*/
public function hasChild($code);
/**
* Returns an collection of admin children.
*
* @return array list of Admin children
*/
public function getChildren();
/**
* Returns an admin child with the given $code.
*
* @param string $code
*
* @return AdminInterface|null
*/
public function getChild($code);
}

View File

@@ -0,0 +1,390 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Admin;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class Pool
{
/**
* @var ContainerInterface
*/
protected $container;
/**
* @var string[]
*/
protected $adminServiceIds = [];
/**
* @var array
*/
protected $adminGroups = [];
/**
* @var array
*/
protected $adminClasses = [];
/**
* @var string[]
*/
protected $templates = [];
/**
* @var array
*/
protected $assets = [];
/**
* @var string
*/
protected $title;
/**
* @var string
*/
protected $titleLogo;
/**
* @var array
*/
protected $options;
/**
* @var PropertyAccessorInterface
*/
protected $propertyAccessor;
/**
* @param ContainerInterface $container
* @param string $title
* @param string $logoTitle
* @param array $options
*/
public function __construct(ContainerInterface $container, $title, $logoTitle, $options = [], PropertyAccessorInterface $propertyAccessor = null)
{
$this->container = $container;
$this->title = $title;
$this->titleLogo = $logoTitle;
$this->options = $options;
$this->propertyAccessor = $propertyAccessor;
}
/**
* @return array
*/
public function getGroups()
{
$groups = $this->adminGroups;
foreach ($this->adminGroups as $name => $adminGroup) {
foreach ($adminGroup as $id => $options) {
$groups[$name][$id] = $this->getInstance($id);
}
}
return $groups;
}
/**
* Returns whether an admin group exists or not.
*
* @param string $group
*
* @return bool
*/
public function hasGroup($group)
{
return isset($this->adminGroups[$group]);
}
/**
* @return array
*/
public function getDashboardGroups()
{
$groups = $this->adminGroups;
foreach ($this->adminGroups as $name => $adminGroup) {
if (isset($adminGroup['items'])) {
foreach ($adminGroup['items'] as $key => $item) {
// Only Admin Group should be returned
if ('' != $item['admin']) {
$admin = $this->getInstance($item['admin']);
if ($admin->showIn(AbstractAdmin::CONTEXT_DASHBOARD)) {
$groups[$name]['items'][$key] = $admin;
} else {
unset($groups[$name]['items'][$key]);
}
} else {
unset($groups[$name]['items'][$key]);
}
}
}
if (empty($groups[$name]['items'])) {
unset($groups[$name]);
}
}
return $groups;
}
/**
* Returns all admins related to the given $group.
*
* @param string $group
*
* @return array
*
* @throws \InvalidArgumentException
*/
public function getAdminsByGroup($group)
{
if (!isset($this->adminGroups[$group])) {
throw new \InvalidArgumentException(sprintf('Group "%s" not found in admin pool.', $group));
}
$admins = [];
if (!isset($this->adminGroups[$group]['items'])) {
return $admins;
}
foreach ($this->adminGroups[$group]['items'] as $item) {
$admins[] = $this->getInstance($item['admin']);
}
return $admins;
}
/**
* Return the admin related to the given $class.
*
* @param string $class
*
* @return \Sonata\AdminBundle\Admin\AdminInterface|null
*/
public function getAdminByClass($class)
{
if (!$this->hasAdminByClass($class)) {
return;
}
if (!is_array($this->adminClasses[$class])) {
throw new \RuntimeException('Invalid format for the Pool::adminClass property');
}
if (count($this->adminClasses[$class]) > 1) {
throw new \RuntimeException(sprintf(
'Unable to find a valid admin for the class: %s, there are too many registered: %s',
$class,
implode(', ', $this->adminClasses[$class])
));
}
return $this->getInstance($this->adminClasses[$class][0]);
}
/**
* @param string $class
*
* @return bool
*/
public function hasAdminByClass($class)
{
return isset($this->adminClasses[$class]);
}
/**
* Returns an admin class by its Admin code
* ie : sonata.news.admin.post|sonata.news.admin.comment => return the child class of post.
*
* @param string $adminCode
*
* @return \Sonata\AdminBundle\Admin\AdminInterface|false|null
*/
public function getAdminByAdminCode($adminCode)
{
$codes = explode('|', $adminCode);
if (false === $codes) {
return false;
}
$admin = $this->getInstance($codes[0]);
array_shift($codes);
if (empty($codes)) {
return $admin;
}
foreach ($codes as $code) {
if (!$admin->hasChild($code)) {
return false;
}
$admin = $admin->getChild($code);
}
return $admin;
}
/**
* Returns a new admin instance depends on the given code.
*
* @param string $id
*
* @return AdminInterface
*
* @throws \InvalidArgumentException
*/
public function getInstance($id)
{
if (!in_array($id, $this->adminServiceIds)) {
throw new \InvalidArgumentException(sprintf('Admin service "%s" not found in admin pool.', $id));
}
return $this->container->get($id);
}
/**
* @return ContainerInterface|null
*/
public function getContainer()
{
return $this->container;
}
/**
* @param array $adminGroups
*/
public function setAdminGroups(array $adminGroups)
{
$this->adminGroups = $adminGroups;
}
/**
* @return array
*/
public function getAdminGroups()
{
return $this->adminGroups;
}
/**
* @param array $adminServiceIds
*/
public function setAdminServiceIds(array $adminServiceIds)
{
$this->adminServiceIds = $adminServiceIds;
}
/**
* @return array
*/
public function getAdminServiceIds()
{
return $this->adminServiceIds;
}
/**
* @param array $adminClasses
*/
public function setAdminClasses(array $adminClasses)
{
$this->adminClasses = $adminClasses;
}
/**
* @return array
*/
public function getAdminClasses()
{
return $this->adminClasses;
}
/**
* @param array $templates
*/
public function setTemplates(array $templates)
{
$this->templates = $templates;
}
/**
* @return array
*/
public function getTemplates()
{
return $this->templates;
}
/**
* @param string $name
*
* @return null|string
*/
public function getTemplate($name)
{
if (isset($this->templates[$name])) {
return $this->templates[$name];
}
}
/**
* @return string
*/
public function getTitleLogo()
{
return $this->titleLogo;
}
/**
* @return string
*/
public function getTitle()
{
return $this->title;
}
/**
* @param string $name
* @param mixed $default
*
* @return mixed
*/
public function getOption($name, $default = null)
{
if (isset($this->options[$name])) {
return $this->options[$name];
}
return $default;
}
public function getPropertyAccessor()
{
if (null === $this->propertyAccessor) {
$this->propertyAccessor = PropertyAccess::createPropertyAccessor();
}
return $this->propertyAccessor;
}
}

View File

@@ -0,0 +1,82 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Admin;
use Sonata\AdminBundle\Route\RouteGeneratorInterface;
/**
* Contains url generation logic related to an admin.
*
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
interface UrlGeneratorInterface
{
/**
* Returns the list of available urls.
*
* @return RouteCollection the list of available urls
*/
public function getRoutes();
/**
* Return the parameter name used to represent the id in the url.
*
* @return string
*/
public function getRouterIdParameter();
/**
* @param RouteGeneratorInterface $routeGenerator
*/
public function setRouteGenerator(RouteGeneratorInterface $routeGenerator);
/**
* Generates the object url with the given $name.
*
* @param string $name
* @param mixed $object
* @param array $parameters
* @param bool $absolute
*
* @return string return a complete url
*/
public function generateObjectUrl($name, $object, array $parameters = [], $absolute = false);
/**
* Generates a url for the given parameters.
*
* @param string $name
* @param array $parameters
* @param bool $absolute
*
* @return string return a complete url
*/
public function generateUrl($name, array $parameters = [], $absolute = false);
/**
* Generates a url for the given parameters.
*
* @param string $name
* @param array $parameters
* @param bool $absolute
*
* @return array return url parts: 'route', 'routeParameters', 'routeAbsolute'
*/
public function generateMenuUrl($name, array $parameters = [], $absolute = false);
/**
* @param mixed $entity
*
* @return string a string representation of the id that is safe to use in a url
*/
public function getUrlsafeIdentifier($entity);
}

View File

@@ -0,0 +1,179 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Annotation;
use JMS\DiExtraBundle\Annotation\MetadataProcessorInterface;
use JMS\DiExtraBundle\Metadata\ClassMetadata;
use Sonata\AdminBundle\Admin\AbstractAdmin as AdminClass;
/**
* Use annotations to define admin classes.
*
* @Annotation
* @Target("CLASS")
*/
class Admin implements MetadataProcessorInterface
{
/**
* Service id - autogenerated per default.
*
* @var string
*/
public $id;
/**
* Admin class.
*
* @var string
*/
public $class;
/**
* Data storage.
*
* @var string
*/
public $managerType = 'orm';
/**
* @var string
*/
public $pagerType;
/**
* @var string
*/
public $persistFilters;
/**
* Admin group with fallback to class.
*
* @var string
*/
public $group;
/**
* Icon for admin group default is '<i class="fa fa-folder"></i>'.
*
* @var string
*/
public $icon;
/**
* Admin label with fallback to class.
*
* @var string
*/
public $label;
/**
* @var string
*/
public $baseControllerName = 'SonataAdminBundle:CRUD';
/**
* @var string
*/
public $translationDomain;
/**
* @var bool
*/
public $showInDashboard = true;
/**
* @var bool
*/
public $keepOpen = false;
/**
* @var bool
*/
public $onTop = false;
/**
* @param ClassMetadata $metadata
*/
public function processMetadata(ClassMetadata $metadata)
{
$this->generateFallback($this->class);
$this->validate();
$tag = [
'manager_type' => $this->managerType,
'group' => $this->group,
'label' => $this->label,
'show_in_dashboard' => $this->showInDashboard,
'icon' => $this->icon,
'pager_type' => $this->pagerType,
'persist_filters' => $this->persistFilters,
'keep_open' => $this->keepOpen,
'on_top' => $this->onTop,
];
$tag = array_filter($tag, function ($v) {
return !is_null($v);
});
if (!empty($this->id)) {
$metadata->id = $this->id;
}
$metadata->tags['sonata.admin'][] = $tag;
$metadata->arguments = [$this->id, $this->class, $this->baseControllerName];
if ($this->translationDomain) {
$metadata->methodCalls[] = ['setTranslationDomain', [$this->translationDomain]];
}
}
/**
* Check if all the required fields are given.
*/
private function validate()
{
if (!$this->showInDashboard) {
return;
}
if (empty($this->group) || empty($this->label)) {
throw new \LogicException(
sprintf(
'Unable to generate admin group and label for class %s.',
$this->class
)
);
}
}
/**
* Set group and label from class name it not set.
*
* @param $name
*/
private function generateFallback($name)
{
if (empty($name)) {
return;
}
if (preg_match(AdminClass::CLASS_REGEX, $name, $matches)) {
if (empty($this->group)) {
$this->group = $matches[3];
}
if (empty($this->label)) {
$this->label = $matches[5];
}
}
}
}

View File

@@ -0,0 +1,90 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Block;
use Sonata\AdminBundle\Admin\Pool;
use Sonata\BlockBundle\Block\BlockContextInterface;
use Sonata\BlockBundle\Block\Service\AbstractBlockService;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class AdminListBlockService extends AbstractBlockService
{
protected $pool;
/**
* @param string $name
* @param EngineInterface $templating
* @param Pool $pool
*/
public function __construct($name, EngineInterface $templating, Pool $pool)
{
parent::__construct($name, $templating);
$this->pool = $pool;
}
/**
* {@inheritdoc}
*/
public function execute(BlockContextInterface $blockContext, Response $response = null)
{
$dashboardGroups = $this->pool->getDashboardGroups();
$settings = $blockContext->getSettings();
$visibleGroups = [];
foreach ($dashboardGroups as $name => $dashboardGroup) {
if (!$settings['groups'] || in_array($name, $settings['groups'])) {
$visibleGroups[] = $dashboardGroup;
}
}
return $this->renderPrivateResponse($this->pool->getTemplate('list_block'), [
'block' => $blockContext->getBlock(),
'settings' => $settings,
'admin_pool' => $this->pool,
'groups' => $visibleGroups,
], $response);
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'Admin List';
}
/**
* {@inheritdoc}
*/
public function configureSettings(OptionsResolver $resolver)
{
$resolver->setDefaults([
'groups' => false,
]);
// Symfony < 2.6 BC
if (method_exists($resolver, 'setNormalizer')) {
$resolver->setAllowedTypes('groups', ['bool', 'array']);
} else {
$resolver->setAllowedTypes([
'groups' => ['bool', 'array'],
]);
}
}
}

View File

@@ -0,0 +1,107 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Block;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Admin\Pool;
use Sonata\AdminBundle\Search\SearchHandler;
use Sonata\BlockBundle\Block\BlockContextInterface;
use Sonata\BlockBundle\Block\Service\AbstractBlockService;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class AdminSearchBlockService extends AbstractBlockService
{
/**
* @var Pool
*/
protected $pool;
/**
* @var SearchHandler
*/
protected $searchHandler;
/**
* @param string $name
* @param EngineInterface $templating
* @param Pool $pool
* @param SearchHandler $searchHandler
*/
public function __construct($name, EngineInterface $templating, Pool $pool, SearchHandler $searchHandler)
{
parent::__construct($name, $templating);
$this->pool = $pool;
$this->searchHandler = $searchHandler;
}
/**
* {@inheritdoc}
*/
public function execute(BlockContextInterface $blockContext, Response $response = null)
{
try {
$admin = $this->pool->getAdminByAdminCode($blockContext->getSetting('admin_code'));
} catch (ServiceNotFoundException $e) {
throw new \RuntimeException('Unable to find the Admin instance', $e->getCode(), $e);
}
if (!$admin instanceof AdminInterface) {
throw new \RuntimeException('The requested service is not an Admin instance');
}
$admin->checkAccess('list');
$pager = $this->searchHandler->search(
$admin,
$blockContext->getSetting('query'),
$blockContext->getSetting('page'),
$blockContext->getSetting('per_page')
);
return $this->renderPrivateResponse($admin->getTemplate('search_result_block'), [
'block' => $blockContext->getBlock(),
'settings' => $blockContext->getSettings(),
'admin_pool' => $this->pool,
'pager' => $pager,
'admin' => $admin,
], $response);
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'Admin Search Result';
}
/**
* {@inheritdoc}
*/
public function configureSettings(OptionsResolver $resolver)
{
$resolver->setDefaults([
'admin_code' => false,
'query' => '',
'page' => 0,
'per_page' => 10,
'icon' => '<i class="fa fa-list"></i>',
]);
}
}

View File

@@ -0,0 +1,97 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Block;
use Sonata\AdminBundle\Admin\Pool;
use Sonata\BlockBundle\Block\BlockContextInterface;
use Sonata\BlockBundle\Block\Service\AbstractBlockService;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class AdminStatsBlockService extends AbstractBlockService
{
/**
* @var Pool
*/
protected $pool;
/**
* @param string $name
* @param EngineInterface $templating
* @param Pool $pool
*/
public function __construct($name, EngineInterface $templating, Pool $pool)
{
parent::__construct($name, $templating);
$this->pool = $pool;
}
/**
* {@inheritdoc}
*/
public function execute(BlockContextInterface $blockContext, Response $response = null)
{
$admin = $this->pool->getAdminByAdminCode($blockContext->getSetting('code'));
$datagrid = $admin->getDatagrid();
$filters = $blockContext->getSetting('filters');
if (!isset($filters['_per_page'])) {
$filters['_per_page'] = ['value' => $blockContext->getSetting('limit')];
}
foreach ($filters as $name => $data) {
$datagrid->setValue($name, isset($data['type']) ? $data['type'] : null, $data['value']);
}
$datagrid->buildPager();
return $this->renderPrivateResponse($blockContext->getTemplate(), [
'block' => $blockContext->getBlock(),
'settings' => $blockContext->getSettings(),
'admin_pool' => $this->pool,
'admin' => $admin,
'pager' => $datagrid->getPager(),
'datagrid' => $datagrid,
], $response);
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'Admin Stats';
}
/**
* {@inheritdoc}
*/
public function configureSettings(OptionsResolver $resolver)
{
$resolver->setDefaults([
'icon' => 'fa-line-chart',
'text' => 'Statistics',
'color' => 'bg-aqua',
'code' => false,
'filters' => [],
'limit' => 1000,
'template' => 'SonataAdminBundle:Block:block_stats.html.twig',
]);
}
}

View File

@@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Bridge\Exporter;
use Exporter\Exporter;
use Sonata\AdminBundle\Admin\AdminInterface;
/**
* @author Grégoire Paris <postmaster@greg0ire.fr>
*/
final class AdminExporter
{
/**
* @var Exporter service from the exporter bundle
*/
private $exporter;
/**
* @param Exporter will be used to get global settings
*/
public function __construct(Exporter $exporter)
{
$this->exporter = $exporter;
}
/**
* Queries an admin for its default export formats, and falls back on global settings.
*
* @param AdminInterface $admin the current admin object
*
* @return string[] an array of formats
*/
public function getAvailableFormats(AdminInterface $admin)
{
$adminExportFormats = $admin->getExportFormats();
// NEXT_MAJOR : compare with null
if ($adminExportFormats != ['json', 'xml', 'csv', 'xls']) {
return $adminExportFormats;
}
return $this->exporter->getAvailableFormats();
}
/**
* Builds an export filename from the class associated with the provided admin,
* the current date, and the provided format.
*
* @param AdminInterface $admin the current admin object
* @param string $format the format of the export file
*/
public function getExportFilename(AdminInterface $admin, $format)
{
$class = $admin->getClass();
return sprintf(
'export_%s_%s.%s',
strtolower(substr($class, strripos($class, '\\') + 1)),
date('Y_m_d_H_i_s', strtotime('now')),
$format
);
}
}

View File

@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Builder;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
interface BuilderInterface
{
/**
* Adds missing information to the given field description from the model manager metadata, and the given admin.
*
* @param AdminInterface $admin will be used to gather information
* @param FieldDescriptionInterface $fieldDescription will be modified
*/
public function fixFieldDescription(AdminInterface $admin, FieldDescriptionInterface $fieldDescription);
}

View File

@@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Builder;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
use Sonata\AdminBundle\Datagrid\DatagridInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
interface DatagridBuilderInterface extends BuilderInterface
{
/**
* @abstract
*
* @param DatagridInterface $datagrid
* @param string $type
* @param FieldDescriptionInterface $fieldDescription
* @param AdminInterface $admin
*/
public function addFilter(DatagridInterface $datagrid, $type, FieldDescriptionInterface $fieldDescription, AdminInterface $admin);
/**
* @param AdminInterface $admin
* @param array $values
*
* @return DatagridInterface
*/
public function getBaseDatagrid(AdminInterface $admin, array $values = []);
}

View File

@@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Builder;
use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormFactoryInterface;
/**
* This interface should be implemented in persistence bundles.
*
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
interface FormContractorInterface extends BuilderInterface
{
/**
* @abstract
*
* @param FormFactoryInterface $formFactory
*/
public function __construct(FormFactoryInterface $formFactory);
/**
* @abstract
*
* @param string $name
* @param array $options
*
* @return FormBuilder
*/
public function getFormBuilder($name, array $options = []);
/**
* Should provide Symfony form options.
*
* @abstract
*
* @param string $type
* @param FieldDescriptionInterface $fieldDescription
*
* @return array
*/
public function getDefaultOptions($type, FieldDescriptionInterface $fieldDescription);
}

View File

@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Builder;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Admin\FieldDescriptionCollection;
use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
interface ListBuilderInterface extends BuilderInterface
{
/**
* @param array $options
*
* @return FieldDescriptionCollection
*/
public function getBaseList(array $options = []);
/**
* Modify a field description to display it in the list view.
*
* @param null|mixed $type
* @param FieldDescriptionInterface $fieldDescription
* @param AdminInterface $admin
*/
public function buildField($type, FieldDescriptionInterface $fieldDescription, AdminInterface $admin);
/**
* Modify a field description and add it to the displayed columns.
*
* @param FieldDescriptionCollection $list
* @param null|mixed $type
* @param FieldDescriptionInterface $fieldDescription
* @param AdminInterface $admin
*/
public function addField(FieldDescriptionCollection $list, $type, FieldDescriptionInterface $fieldDescription, AdminInterface $admin);
}

View File

@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Builder;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Route\RouteCollection;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
interface RouteBuilderInterface
{
/**
* @param AdminInterface $admin
* @param RouteCollection $collection
*/
public function build(AdminInterface $admin, RouteCollection $collection);
}

View File

@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Builder;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Admin\FieldDescriptionCollection;
use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
interface ShowBuilderInterface extends BuilderInterface
{
/**
* @abstract
*
* @param array $options
*/
public function getBaseList(array $options = []);
/**
* @abstract
*
* @param FieldDescriptionCollection $list
* @param string|null $type
* @param FieldDescriptionInterface $fieldDescription
* @param AdminInterface $admin
*/
public function addField(FieldDescriptionCollection $list, $type, FieldDescriptionInterface $fieldDescription, AdminInterface $admin);
}

View File

@@ -0,0 +1,61 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\ClassLoader\ClassCollectionLoader;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class CreateClassCacheCommand extends ContainerAwareCommand
{
/**
* {@inheritdoc}
*/
public function configure()
{
$this->setName('cache:create-cache-class');
$this->setDescription('Generate the classes.php files');
}
/**
* {@inheritdoc}
*/
public function execute(InputInterface $input, OutputInterface $output)
{
$kernel = $this->getContainer()->get('kernel');
$classmap = $kernel->getCacheDir().'/classes.map';
if (!is_file($classmap)) {
throw new \RuntimeException(sprintf('The file %s does not exist', $classmap));
}
$name = 'classes';
$extension = '.php';
$output->write('<info>Writing cache file ...</info>');
ClassCollectionLoader::load(
include($classmap),
$kernel->getCacheDir(),
$name,
$kernel->isDebug(),
false,
$extension
);
$output->writeln(' done!');
}
}

View File

@@ -0,0 +1,162 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class ExplainAdminCommand extends ContainerAwareCommand
{
/**
* {@inheritdoc}
*/
public function configure()
{
$this->setName('sonata:admin:explain');
$this->setDescription('Explain an admin service');
$this->addArgument('admin', InputArgument::REQUIRED, 'The admin service id');
}
/**
* {@inheritdoc}
*/
public function execute(InputInterface $input, OutputInterface $output)
{
$admin = $this->getContainer()->get($input->getArgument('admin'));
if (!$admin instanceof \Sonata\AdminBundle\Admin\AdminInterface) {
throw new \RuntimeException(sprintf('Service "%s" is not an admin class', $input->getArgument('admin')));
}
$output->writeln('<comment>AdminBundle Information</comment>');
$output->writeln(sprintf('<info>% -20s</info> : %s', 'id', $admin->getCode()));
$output->writeln(sprintf('<info>% -20s</info> : %s', 'Admin', get_class($admin)));
$output->writeln(sprintf('<info>% -20s</info> : %s', 'Model', $admin->getClass()));
$output->writeln(sprintf('<info>% -20s</info> : %s', 'Controller', $admin->getBaseControllerName()));
$output->writeln(sprintf('<info>% -20s</info> : %s', 'Model Manager', get_class($admin->getModelManager())));
$output->writeln(sprintf('<info>% -20s</info> : %s', 'Form Builder', get_class($admin->getFormBuilder())));
$output->writeln(sprintf('<info>% -20s</info> : %s', 'Datagrid Builder', get_class($admin->getDatagridBuilder())));
$output->writeln(sprintf('<info>% -20s</info> : %s', 'List Builder', get_class($admin->getListBuilder())));
if ($admin->isChild()) {
$output->writeln(sprintf('<info>% -15s</info> : %s', 'Parent', $admin->getParent()->getCode()));
}
$output->writeln('');
$output->writeln('<info>Routes</info>');
foreach ($admin->getRoutes()->getElements() as $route) {
$output->writeln(sprintf(' - % -25s %s', $route->getDefault('_sonata_name'), $route->getPath()));
}
$output->writeln('');
$output->writeln('<info>Datagrid Columns</info>');
foreach ($admin->getListFieldDescriptions() as $name => $fieldDescription) {
$output->writeln(sprintf(
' - % -25s % -15s % -15s',
$name,
$fieldDescription->getType(),
$fieldDescription->getTemplate()
));
}
$output->writeln('');
$output->writeln('<info>Datagrid Filters</info>');
foreach ($admin->getFilterFieldDescriptions() as $name => $fieldDescription) {
$output->writeln(sprintf(
' - % -25s % -15s % -15s',
$name,
$fieldDescription->getType(),
$fieldDescription->getTemplate()
));
}
$output->writeln('');
$output->writeln('<info>Form theme(s)</info>');
foreach ($admin->getFormTheme() as $template) {
$output->writeln(sprintf(' - %s', $template));
}
$output->writeln('');
$output->writeln('<info>Form Fields</info>');
foreach ($admin->getFormFieldDescriptions() as $name => $fieldDescription) {
$output->writeln(sprintf(
' - % -25s % -15s % -15s',
$name,
$fieldDescription->getType(),
$fieldDescription->getTemplate()
));
}
$metadata = false;
if ($this->getContainer()->has('validator.validator_factory')) {
$factory = $this->getContainer()->get('validator.validator_factory');
if (method_exists($factory, 'getMetadataFor')) {
$metadata = $factory->getMetadataFor($admin->getClass());
}
}
// NEXT_MAJOR: remove method check in next major release
if (!$metadata) {
$metadata = $this->getContainer()->get('validator')->getMetadataFor($admin->getClass());
}
$output->writeln('');
$output->writeln('<comment>Validation Framework</comment> - http://symfony.com/doc/3.0/book/validation.html');
$output->writeln('<info>Properties constraints</info>');
if (count($metadata->properties) == 0) {
$output->writeln(' <error>no property constraints defined !!</error>');
} else {
foreach ($metadata->properties as $name => $property) {
$output->writeln(sprintf(' - %s', $name));
foreach ($property->getConstraints() as $constraint) {
$output->writeln(sprintf(
' % -70s %s',
get_class($constraint),
implode('|', $constraint->groups)
));
}
}
}
$output->writeln('');
$output->writeln('<info>Getters constraints</info>');
if (count($metadata->getters) == 0) {
$output->writeln(' <error>no getter constraints defined !!</error>');
} else {
foreach ($metadata->getters as $name => $property) {
$output->writeln(sprintf(' - %s', $name));
foreach ($property->getConstraints() as $constraint) {
$output->writeln(sprintf(
' % -70s %s',
get_class($constraint),
implode('|', $constraint->groups)
));
}
}
}
$output->writeln('');
$output->writeln('<info>done!</info>');
}
}

View File

@@ -0,0 +1,353 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Command;
use Sonata\AdminBundle\Generator\AdminGenerator;
use Sonata\AdminBundle\Generator\ControllerGenerator;
use Sonata\AdminBundle\Manipulator\ServicesManipulator;
use Sonata\AdminBundle\Model\ModelManagerInterface;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\HttpKernel\Bundle\BundleInterface;
use Symfony\Component\HttpKernel\KernelInterface;
/**
* @author Marek Stipek <mario.dweller@seznam.cz>
* @author Simon Cosandey <simon.cosandey@simseo.ch>
*/
class GenerateAdminCommand extends QuestionableCommand
{
/**
* @var string[]
*/
private $managerTypes;
/**
* {@inheritdoc}
*/
public function configure()
{
$this
->setName('sonata:admin:generate')
->setDescription('Generates an admin class based on the given model class')
->addArgument('model', InputArgument::REQUIRED, 'The fully qualified model class')
->addOption('bundle', 'b', InputOption::VALUE_OPTIONAL, 'The bundle name')
->addOption('admin', 'a', InputOption::VALUE_OPTIONAL, 'The admin class basename')
->addOption('controller', 'c', InputOption::VALUE_OPTIONAL, 'The controller class basename')
->addOption('manager', 'm', InputOption::VALUE_OPTIONAL, 'The model manager type')
->addOption('services', 'y', InputOption::VALUE_OPTIONAL, 'The services YAML file', 'services.yml')
->addOption('id', 'i', InputOption::VALUE_OPTIONAL, 'The admin service ID')
;
}
/**
* {@inheritdoc}
*/
public function isEnabled()
{
return class_exists('Sensio\\Bundle\\GeneratorBundle\\SensioGeneratorBundle');
}
/**
* @param string $managerType
*
* @return string
*
* @throws \InvalidArgumentException
*/
public function validateManagerType($managerType)
{
$managerTypes = $this->getAvailableManagerTypes();
if (!isset($managerTypes[$managerType])) {
throw new \InvalidArgumentException(sprintf(
'Invalid manager type "%s". Available manager types are "%s".',
$managerType,
implode('", "', $managerTypes)
));
}
return $managerType;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$modelClass = Validators::validateClass($input->getArgument('model'));
$modelClassBasename = current(array_slice(explode('\\', $modelClass), -1));
$bundle = $this->getBundle($input->getOption('bundle') ?: $this->getBundleNameFromClass($modelClass));
$adminClassBasename = $input->getOption('admin') ?: $modelClassBasename.'Admin';
$adminClassBasename = Validators::validateAdminClassBasename($adminClassBasename);
$managerType = $input->getOption('manager') ?: $this->getDefaultManagerType();
$modelManager = $this->getModelManager($managerType);
$skeletonDirectory = __DIR__.'/../Resources/skeleton';
$adminGenerator = new AdminGenerator($modelManager, $skeletonDirectory);
try {
$adminGenerator->generate($bundle, $adminClassBasename, $modelClass);
$output->writeln(sprintf(
'%sThe admin class "<info>%s</info>" has been generated under the file "<info>%s</info>".',
PHP_EOL,
$adminGenerator->getClass(),
realpath($adminGenerator->getFile())
));
} catch (\Exception $e) {
$this->writeError($output, $e->getMessage());
}
if ($controllerClassBasename = $input->getOption('controller')) {
$controllerClassBasename = Validators::validateControllerClassBasename($controllerClassBasename);
$controllerGenerator = new ControllerGenerator($skeletonDirectory);
try {
$controllerGenerator->generate($bundle, $controllerClassBasename);
$output->writeln(sprintf(
'%sThe controller class "<info>%s</info>" has been generated under the file "<info>%s</info>".',
PHP_EOL,
$controllerGenerator->getClass(),
realpath($controllerGenerator->getFile())
));
} catch (\Exception $e) {
$this->writeError($output, $e->getMessage());
}
}
if ($servicesFile = $input->getOption('services')) {
$adminClass = $adminGenerator->getClass();
$file = sprintf('%s/Resources/config/%s', $bundle->getPath(), $servicesFile);
$servicesManipulator = new ServicesManipulator($file);
$controllerName = $controllerClassBasename
? sprintf('%s:%s', $bundle->getName(), substr($controllerClassBasename, 0, -10))
: 'SonataAdminBundle:CRUD'
;
try {
$id = $input->getOption('id') ?: $this->getAdminServiceId($bundle->getName(), $adminClassBasename);
$servicesManipulator->addResource($id, $modelClass, $adminClass, $controllerName, $managerType);
$output->writeln(sprintf(
'%sThe service "<info>%s</info>" has been appended to the file <info>"%s</info>".',
PHP_EOL,
$id,
realpath($file)
));
} catch (\Exception $e) {
$this->writeError($output, $e->getMessage());
}
}
return 0;
}
/**
* {@inheritdoc}
*/
protected function interact(InputInterface $input, OutputInterface $output)
{
$questionHelper = $this->getQuestionHelper();
$questionHelper->writeSection($output, 'Welcome to the Sonata admin generator');
$modelClass = $this->askAndValidate(
$input,
$output,
'The fully qualified model class',
$input->getArgument('model'),
'Sonata\AdminBundle\Command\Validators::validateClass'
);
$modelClassBasename = current(array_slice(explode('\\', $modelClass), -1));
$bundleName = $this->askAndValidate(
$input,
$output,
'The bundle name',
$input->getOption('bundle') ?: $this->getBundleNameFromClass($modelClass),
'Sensio\Bundle\GeneratorBundle\Command\Validators::validateBundleName'
);
$adminClassBasename = $this->askAndValidate(
$input,
$output,
'The admin class basename',
$input->getOption('admin') ?: $modelClassBasename.'Admin',
'Sonata\AdminBundle\Command\Validators::validateAdminClassBasename'
);
if (count($this->getAvailableManagerTypes()) > 1) {
$managerType = $this->askAndValidate(
$input,
$output,
'The manager type',
$input->getOption('manager') ?: $this->getDefaultManagerType(),
[$this, 'validateManagerType']
);
$input->setOption('manager', $managerType);
}
if ($this->askConfirmation($input, $output, 'Do you want to generate a controller', 'no', '?')) {
$controllerClassBasename = $this->askAndValidate(
$input,
$output,
'The controller class basename',
$input->getOption('controller') ?: $modelClassBasename.'AdminController',
'Sonata\AdminBundle\Command\Validators::validateControllerClassBasename'
);
$input->setOption('controller', $controllerClassBasename);
}
if ($this->askConfirmation($input, $output, 'Do you want to update the services YAML configuration file', 'yes', '?')) {
$path = $this->getBundle($bundleName)->getPath().'/Resources/config/';
$servicesFile = $this->askAndValidate(
$input,
$output,
'The services YAML configuration file',
is_file($path.'admin.yml') ? 'admin.yml' : 'services.yml',
'Sonata\AdminBundle\Command\Validators::validateServicesFile'
);
$id = $this->askAndValidate(
$input,
$output,
'The admin service ID',
$this->getAdminServiceId($bundleName, $adminClassBasename),
'Sonata\AdminBundle\Command\Validators::validateServiceId'
);
$input->setOption('services', $servicesFile);
$input->setOption('id', $id);
} else {
$input->setOption('services', false);
}
$input->setArgument('model', $modelClass);
$input->setOption('admin', $adminClassBasename);
$input->setOption('bundle', $bundleName);
}
/**
* @param string $class
*
* @return string|null
*
* @throws \InvalidArgumentException
*/
private function getBundleNameFromClass($class)
{
$application = $this->getApplication();
/* @var $application Application */
foreach ($application->getKernel()->getBundles() as $bundle) {
if (strpos($class, $bundle->getNamespace().'\\') === 0) {
return $bundle->getName();
}
}
return;
}
/**
* @param string $name
*
* @return BundleInterface
*/
private function getBundle($name)
{
return $this->getKernel()->getBundle($name);
}
/**
* @param OutputInterface $output
* @param string $message
*/
private function writeError(OutputInterface $output, $message)
{
$output->writeln(sprintf("\n<error>%s</error>", $message));
}
/**
* @return string
*
* @throws \RuntimeException
*/
private function getDefaultManagerType()
{
if (!$managerTypes = $this->getAvailableManagerTypes()) {
throw new \RuntimeException('There are no model managers registered.');
}
return current($managerTypes);
}
/**
* @param string $managerType
*
* @return ModelManagerInterface
*/
private function getModelManager($managerType)
{
return $this->getContainer()->get('sonata.admin.manager.'.$managerType);
}
/**
* @param string $bundleName
* @param string $adminClassBasename
*
* @return string
*/
private function getAdminServiceId($bundleName, $adminClassBasename)
{
$prefix = substr($bundleName, -6) == 'Bundle' ? substr($bundleName, 0, -6) : $bundleName;
$suffix = substr($adminClassBasename, -5) == 'Admin' ? substr($adminClassBasename, 0, -5) : $adminClassBasename;
$suffix = str_replace('\\', '.', $suffix);
return Container::underscore(sprintf(
'%s.admin.%s',
$prefix,
$suffix
));
}
/**
* @return string[]
*/
private function getAvailableManagerTypes()
{
$container = $this->getContainer();
if (!$container instanceof Container) {
return [];
}
if ($this->managerTypes === null) {
$this->managerTypes = [];
foreach ($container->getServiceIds() as $id) {
if (strpos($id, 'sonata.admin.manager.') === 0) {
$managerType = substr($id, 21);
$this->managerTypes[$managerType] = $managerType;
}
}
}
return $this->managerTypes;
}
/**
* @return KernelInterface
*/
private function getKernel()
{
/* @var $application Application */
$application = $this->getApplication();
return $application->getKernel();
}
}

View File

@@ -0,0 +1,132 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Command;
use Sonata\AdminBundle\Util\ObjectAclManipulatorInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class GenerateObjectAclCommand extends QuestionableCommand
{
/**
* @var string
*/
protected $userEntityClass = '';
/**
* {@inheritdoc}
*/
public function configure()
{
$this
->setName('sonata:admin:generate-object-acl')
->setDescription('Install ACL for the objects of the Admin Classes.')
->addOption('object_owner', null, InputOption::VALUE_OPTIONAL, 'If set, the task will set the object owner for each admin.')
->addOption('user_entity', null, InputOption::VALUE_OPTIONAL, 'Shortcut notation like <comment>AcmeDemoBundle:User</comment>. If not set, it will be asked the first time an object owner is set.')
->addOption('step', null, InputOption::VALUE_NONE, 'If set, the task will ask for each admin if the ACLs need to be generated and what object owner to set, if any.')
;
}
/**
* {@inheritdoc}
*/
public function execute(InputInterface $input, OutputInterface $output)
{
$output->writeln('Welcome to the AdminBundle object ACL generator');
$output->writeln([
'',
'This command helps you to generate ACL entities for the objects handled by the AdminBundle.',
'',
'If the step option is used, you will be asked if you want to generate the object ACL entities for each Admin.',
'You must use the shortcut notation like <comment>AcmeDemoBundle:User</comment> if you want to set an object owner.',
'',
]);
if ($input->getOption('user_entity')) {
try {
$this->getUserEntityClass($input, $output);
} catch (\Exception $e) {
$output->writeln(sprintf('<error>%s</error>', $e->getMessage()));
return;
}
}
foreach ($this->getContainer()->get('sonata.admin.pool')->getAdminServiceIds() as $id) {
try {
$admin = $this->getContainer()->get($id);
} catch (\Exception $e) {
$output->writeln('<error>Warning : The admin class cannot be initiated from the command line</error>');
$output->writeln(sprintf('<error>%s</error>', $e->getMessage()));
continue;
}
if ($input->getOption('step') && !$this->askConfirmation($input, $output, sprintf("<question>Generate ACLs for the object instances handled by \"%s\"?</question>\n", $id), 'no', '?')) {
continue;
}
$securityIdentity = null;
if ($input->getOption('step') && $this->askConfirmation($input, $output, "<question>Set an object owner?</question>\n", 'no', '?')) {
$username = $this->askAndValidate($input, $output, 'Please enter the username: ', '', 'Sonata\AdminBundle\Command\Validators::validateUsername');
$securityIdentity = new UserSecurityIdentity($username, $this->getUserEntityClass($input, $output));
}
if (!$input->getOption('step') && $input->getOption('object_owner')) {
$securityIdentity = new UserSecurityIdentity($input->getOption('object_owner'), $this->getUserEntityClass($input, $output));
}
$manipulatorId = sprintf('sonata.admin.manipulator.acl.object.%s', $admin->getManagerType());
if (!$this->getContainer()->has($manipulatorId)) {
$output->writeln('Admin class is using a manager type that has no manipulator implemented : <info>ignoring</info>');
continue;
}
$manipulator = $this->getContainer()->get($manipulatorId);
if (!$manipulator instanceof ObjectAclManipulatorInterface) {
$output->writeln(sprintf('The interface "ObjectAclManipulatorInterface" is not implemented for %s: <info>ignoring</info>', get_class($manipulator)));
continue;
}
$manipulator->batchConfigureAcls($output, $admin, $securityIdentity);
}
}
/**
* @param InputInterface $input
* @param OutputInterface $output
*
* @return string
*/
protected function getUserEntityClass(InputInterface $input, OutputInterface $output)
{
if ($this->userEntityClass === '') {
if ($input->getOption('user_entity')) {
list($userBundle, $userEntity) = Validators::validateEntityName($input->getOption('user_entity'));
$this->userEntityClass = $this->getContainer()->get('doctrine')->getEntityNamespace($userBundle).'\\'.$userEntity;
} else {
list($userBundle, $userEntity) = $this->askAndValidate($input, $output, 'Please enter the User Entity shortcut name: ', '', 'Sonata\AdminBundle\Command\Validators::validateEntityName');
// Entity exists?
$this->userEntityClass = $this->getContainer()->get('doctrine')->getEntityNamespace($userBundle).'\\'.$userEntity;
}
}
return $this->userEntityClass;
}
}

View File

@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class ListAdminCommand extends ContainerAwareCommand
{
/**
* {@inheritdoc}
*/
public function configure()
{
$this->setName('sonata:admin:list');
$this->setDescription('List all admin services available');
}
/**
* {@inheritdoc}
*/
public function execute(InputInterface $input, OutputInterface $output)
{
$pool = $this->getContainer()->get('sonata.admin.pool');
$output->writeln('<info>Admin services:</info>');
foreach ($pool->getAdminServiceIds() as $id) {
$instance = $this->getContainer()->get($id);
$output->writeln(sprintf(' <info>%-40s</info> %-60s',
$id,
$instance->getClass()
));
}
}
}

View File

@@ -0,0 +1,108 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Command;
use Sensio\Bundle\GeneratorBundle\Command\Helper\DialogHelper;
use Sensio\Bundle\GeneratorBundle\Command\Helper\QuestionHelper;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
abstract class QuestionableCommand extends ContainerAwareCommand
{
/**
* @param InputInterface $input
* @param OutputInterface $output
* @param string $questionText
* @param mixed $default
* @param callable $validator
*
* @return mixed
*/
final protected function askAndValidate(InputInterface $input, OutputInterface $output, $questionText, $default, $validator)
{
$questionHelper = $this->getQuestionHelper();
// NEXT_MAJOR: Remove this BC code for SensioGeneratorBundle 2.3/2.4 after dropping support for Symfony 2.3
if ($questionHelper instanceof DialogHelper) {
return $questionHelper->askAndValidate(
$output,
$questionHelper->getQuestion($questionText, $default),
$validator,
false,
$default
);
}
$question = new Question($questionHelper->getQuestion($questionText, $default), $default);
$question->setValidator($validator);
return $questionHelper->ask($input, $output, $question);
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @param string $questionText
* @param string $default
* @param string $separator
*
* @return string
*/
final protected function askConfirmation(InputInterface $input, OutputInterface $output, $questionText, $default, $separator)
{
$questionHelper = $this->getQuestionHelper();
// NEXT_MAJOR: Remove this BC code for SensioGeneratorBundle 2.3/2.4 after dropping support for Symfony 2.3
if ($questionHelper instanceof DialogHelper) {
$question = $questionHelper->getQuestion($questionText, $default, $separator);
return $questionHelper->askConfirmation($output, $question, ($default === 'no' ? false : true));
}
$question = new ConfirmationQuestion($questionHelper->getQuestion(
$questionText,
$default,
$separator
), ($default === 'no' ? false : true));
return $questionHelper->ask($input, $output, $question);
}
/**
* @return QuestionHelper|DialogHelper
*/
final protected function getQuestionHelper()
{
// NEXT_MAJOR: Remove this BC code for SensioGeneratorBundle 2.3/2.4 after dropping support for Symfony 2.3
if (class_exists('Sensio\Bundle\GeneratorBundle\Command\Helper\DialogHelper')) {
$questionHelper = $this->getHelper('dialog');
if (!$questionHelper instanceof DialogHelper) {
$questionHelper = new DialogHelper();
$this->getHelperSet()->set($questionHelper);
}
} else {
$questionHelper = $this->getHelper('question');
if (!$questionHelper instanceof QuestionHelper) {
$questionHelper = new QuestionHelper();
$this->getHelperSet()->set($questionHelper);
}
}
return $questionHelper;
}
}

View File

@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Command;
use Sonata\AdminBundle\Util\AdminAclManipulatorInterface;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class SetupAclCommand extends ContainerAwareCommand
{
/**
* {@inheritdoc}
*/
public function configure()
{
$this->setName('sonata:admin:setup-acl');
$this->setDescription('Install ACL for Admin Classes');
}
/**
* {@inheritdoc}
*/
public function execute(InputInterface $input, OutputInterface $output)
{
$output->writeln('Starting ACL AdminBundle configuration');
foreach ($this->getContainer()->get('sonata.admin.pool')->getAdminServiceIds() as $id) {
try {
$admin = $this->getContainer()->get($id);
} catch (\Exception $e) {
$output->writeln('<error>Warning : The admin class cannot be initiated from the command line</error>');
$output->writeln(sprintf('<error>%s</error>', $e->getMessage()));
continue;
}
$manipulator = $this->getContainer()->get('sonata.admin.manipulator.acl.admin');
if (!$manipulator instanceof AdminAclManipulatorInterface) {
$output->writeln(sprintf(
'The interface "AdminAclManipulatorInterface" is not implemented for %s: <info>ignoring</info>',
get_class($manipulator)
));
continue;
}
$manipulator->configureAcls($output, $admin);
}
}
}

View File

@@ -0,0 +1,166 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Command;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class Validators
{
/**
* @static
*
* @param string $username
*
* @return mixed
*
* @throws \InvalidArgumentException
*/
public static function validateUsername($username)
{
if (is_null($username)) {
throw new \InvalidArgumentException('The username must be set');
}
return $username;
}
/**
* @static
*
* @param string $shortcut
*
* @return array
*
* @throws \InvalidArgumentException
*/
public static function validateEntityName($shortcut)
{
$entity = str_replace('/', '\\', $shortcut);
if (false === $pos = strpos($entity, ':')) {
throw new \InvalidArgumentException(sprintf(
'The entity name must contain a ":" (colon sign) '
.'("%s" given, expecting something like AcmeBlogBundle:Post)',
$entity
));
}
return [substr($entity, 0, $pos), substr($entity, $pos + 1)];
}
/**
* @static
*
* @param string $class
*
* @return string
*
* @throws \InvalidArgumentException
*/
public static function validateClass($class)
{
$class = str_replace('/', '\\', $class);
if (!class_exists($class)) {
throw new \InvalidArgumentException(sprintf('The class "%s" does not exist.', $class));
}
return $class;
}
/**
* @static
*
* @param string $adminClassBasename
*
* @return string
*
* @throws \InvalidArgumentException
*/
public static function validateAdminClassBasename($adminClassBasename)
{
$adminClassBasename = str_replace('/', '\\', $adminClassBasename);
if (false !== strpos($adminClassBasename, ':')) {
throw new \InvalidArgumentException(sprintf(
'The admin class name must not contain a ":" (colon sign) '
.'("%s" given, expecting something like PostAdmin")',
$adminClassBasename
));
}
return $adminClassBasename;
}
/**
* @static
*
* @param string $controllerClassBasename
*
* @return string
*
* @throws \InvalidArgumentException
*/
public static function validateControllerClassBasename($controllerClassBasename)
{
$controllerClassBasename = str_replace('/', '\\', $controllerClassBasename);
if (false !== strpos($controllerClassBasename, ':')) {
throw new \InvalidArgumentException(sprintf(
'The controller class name must not contain a ":" (colon sign) ("%s" given, '
.'expecting something like PostAdminController")',
$controllerClassBasename
));
}
if (substr($controllerClassBasename, -10) != 'Controller') {
throw new \InvalidArgumentException('The controller class name must end with "Controller".');
}
return $controllerClassBasename;
}
/**
* @static
*
* @param string $servicesFile
*
* @return string
*/
public static function validateServicesFile($servicesFile)
{
return trim($servicesFile, '/');
}
/**
* @static
*
* @param string $serviceId
*
* @return string
*
* @throws \InvalidArgumentException
*/
public static function validateServiceId($serviceId)
{
if (preg_match('/[^A-Za-z\._0-9]/', $serviceId, $matches)) {
throw new \InvalidArgumentException(sprintf(
'Service ID "%s" contains invalid character "%s".',
$serviceId,
$matches[0]
));
}
return $serviceId;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,179 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Controller;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Admin\Pool;
use Sonata\AdminBundle\Search\SearchHandler;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class CoreController extends Controller
{
/**
* @return Response
*/
public function dashboardAction()
{
$blocks = [
'top' => [],
'left' => [],
'center' => [],
'right' => [],
'bottom' => [],
];
foreach ($this->container->getParameter('sonata.admin.configuration.dashboard_blocks') as $block) {
$blocks[$block['position']][] = $block;
}
$parameters = [
'base_template' => $this->getBaseTemplate(),
'admin_pool' => $this->container->get('sonata.admin.pool'),
'blocks' => $blocks,
];
if (!$this->getCurrentRequest()->isXmlHttpRequest()) {
$parameters['breadcrumbs_builder'] = $this->get('sonata.admin.breadcrumbs_builder');
}
return $this->render($this->getAdminPool()->getTemplate('dashboard'), $parameters);
}
/**
* The search action first render an empty page, if the query is set, then the template generates
* some ajax request to retrieve results for each admin. The Ajax query returns a JSON response.
*
* @param Request $request
*
* @return JsonResponse|Response
*
* @throws \RuntimeException
*/
public function searchAction(Request $request)
{
if ($request->get('admin') && $request->isXmlHttpRequest()) {
try {
$admin = $this->getAdminPool()->getAdminByAdminCode($request->get('admin'));
} catch (ServiceNotFoundException $e) {
throw new \RuntimeException('Unable to find the Admin instance', $e->getCode(), $e);
}
if (!$admin instanceof AdminInterface) {
throw new \RuntimeException('The requested service is not an Admin instance');
}
$handler = $this->getSearchHandler();
$results = [];
if ($pager = $handler->search($admin, $request->get('q'), $request->get('page'), $request->get('offset'))) {
foreach ($pager->getResults() as $result) {
$results[] = [
'label' => $admin->toString($result),
'link' => $admin->generateObjectUrl('edit', $result),
'id' => $admin->id($result),
];
}
}
$response = new JsonResponse([
'results' => $results,
'page' => $pager ? (int) $pager->getPage() : false,
'total' => $pager ? (int) $pager->getNbResults() : false,
]);
$response->setPrivate();
return $response;
}
return $this->render($this->container->get('sonata.admin.pool')->getTemplate('search'), [
'base_template' => $this->getBaseTemplate(),
'breadcrumbs_builder' => $this->get('sonata.admin.breadcrumbs_builder'),
'admin_pool' => $this->container->get('sonata.admin.pool'),
'query' => $request->get('q'),
'groups' => $this->getAdminPool()->getDashboardGroups(),
]);
}
/**
* Get the request object from the container.
*
* This method is compatible with both Symfony 2.3 and Symfony 3
*
* NEXT_MAJOR: remove this method.
*
* @deprecated since 3.0, to be removed in 4.0 and action methods will be adjusted.
* Use Symfony\Component\HttpFoundation\Request as an action argument
*
* @return Request
*/
public function getRequest()
{
@trigger_error(
'The '.__METHOD__.' method is deprecated since 3.0 and will be removed in 4.0.'.
' Inject the Symfony\Component\HttpFoundation\Request into the actions instead.',
E_USER_DEPRECATED
);
return $this->getCurrentRequest();
}
/**
* @return Pool
*/
protected function getAdminPool()
{
return $this->container->get('sonata.admin.pool');
}
/**
* @return SearchHandler
*/
protected function getSearchHandler()
{
return $this->get('sonata.admin.search.handler');
}
/**
* @return string
*/
protected function getBaseTemplate()
{
if ($this->getCurrentRequest()->isXmlHttpRequest()) {
return $this->getAdminPool()->getTemplate('ajax');
}
return $this->getAdminPool()->getTemplate('layout');
}
/**
* Get the request object from the container.
*
* @return Request
*/
private function getCurrentRequest()
{
// NEXT_MAJOR: simplify this when dropping sf < 2.4
if ($this->container->has('request_stack')) {
return $this->container->get('request_stack')->getCurrentRequest();
}
return $this->container->get('request');
}
}

View File

@@ -0,0 +1,563 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Controller;
use Sonata\AdminBundle\Admin\AdminHelper;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Admin\Pool;
use Sonata\AdminBundle\Filter\FilterInterface;
use Symfony\Bridge\Twig\Form\TwigRenderer;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Validator\ValidatorInterface as LegacyValidatorInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class HelperController
{
/**
* @var \Twig_Environment
*/
protected $twig;
/**
* @var AdminHelper
*/
protected $helper;
/**
* @var Pool
*/
protected $pool;
/**
* @var ValidatorInterface|ValidatorInterface
*/
protected $validator;
/**
* @param \Twig_Environment $twig
* @param Pool $pool
* @param AdminHelper $helper
* @param ValidatorInterface $validator
*/
public function __construct(\Twig_Environment $twig, Pool $pool, AdminHelper $helper, $validator)
{
if (!($validator instanceof ValidatorInterface) && !($validator instanceof LegacyValidatorInterface)) {
throw new \InvalidArgumentException(
'Argument 4 is an instance of '.get_class($validator).', expecting an instance of'
.' \Symfony\Component\Validator\Validator\ValidatorInterface or'
.' \Symfony\Component\Validator\ValidatorInterface'
);
}
$this->twig = $twig;
$this->pool = $pool;
$this->helper = $helper;
$this->validator = $validator;
}
/**
* @throws NotFoundHttpException
*
* @param Request $request
*
* @return Response
*/
public function appendFormFieldElementAction(Request $request)
{
$code = $request->get('code');
$elementId = $request->get('elementId');
$objectId = $request->get('objectId');
$uniqid = $request->get('uniqid');
$admin = $this->pool->getInstance($code);
$admin->setRequest($request);
if ($uniqid) {
$admin->setUniqid($uniqid);
}
$subject = $admin->getModelManager()->find($admin->getClass(), $objectId);
if ($objectId && !$subject) {
throw new NotFoundHttpException();
}
if (!$subject) {
$subject = $admin->getNewInstance();
}
$admin->setSubject($subject);
list(, $form) = $this->helper->appendFormFieldElement($admin, $subject, $elementId);
/* @var $form \Symfony\Component\Form\Form */
$view = $this->helper->getChildFormView($form->createView(), $elementId);
// render the widget
$renderer = $this->getFormRenderer();
$renderer->setTheme($view, $admin->getFormTheme());
return new Response($renderer->searchAndRenderBlock($view, 'widget'));
}
/**
* @throws NotFoundHttpException
*
* @param Request $request
*
* @return Response
*/
public function retrieveFormFieldElementAction(Request $request)
{
$code = $request->get('code');
$elementId = $request->get('elementId');
$objectId = $request->get('objectId');
$uniqid = $request->get('uniqid');
$admin = $this->pool->getInstance($code);
$admin->setRequest($request);
if ($uniqid) {
$admin->setUniqid($uniqid);
}
if ($objectId) {
$subject = $admin->getModelManager()->find($admin->getClass(), $objectId);
if (!$subject) {
throw new NotFoundHttpException(
sprintf('Unable to find the object id: %s, class: %s', $objectId, $admin->getClass())
);
}
} else {
$subject = $admin->getNewInstance();
}
$admin->setSubject($subject);
$formBuilder = $admin->getFormBuilder();
$form = $formBuilder->getForm();
$form->setData($subject);
$form->handleRequest($request);
$view = $this->helper->getChildFormView($form->createView(), $elementId);
// render the widget
$renderer = $this->getFormRenderer();
$renderer->setTheme($view, $admin->getFormTheme());
return new Response($renderer->searchAndRenderBlock($view, 'widget'));
}
/**
* @throws NotFoundHttpException|\RuntimeException
*
* @param Request $request
*
* @return Response
*/
public function getShortObjectDescriptionAction(Request $request)
{
$code = $request->get('code');
$objectId = $request->get('objectId');
$uniqid = $request->get('uniqid');
$linkParameters = $request->get('linkParameters', []);
$admin = $this->pool->getInstance($code);
if (!$admin) {
throw new NotFoundHttpException();
}
$admin->setRequest($request);
if ($uniqid) {
$admin->setUniqid($uniqid);
}
if (!$objectId) {
$objectId = null;
}
$object = $admin->getObject($objectId);
if (!$object && 'html' == $request->get('_format')) {
return new Response();
}
if ('json' == $request->get('_format')) {
return new JsonResponse(['result' => [
'id' => $admin->id($object),
'label' => $admin->toString($object),
]]);
} elseif ('html' == $request->get('_format')) {
return new Response($this->twig->render($admin->getTemplate('short_object_description'), [
'admin' => $admin,
'description' => $admin->toString($object),
'object' => $object,
'link_parameters' => $linkParameters,
]));
}
throw new \RuntimeException('Invalid format');
}
/**
* @param Request $request
*
* @return Response
*/
public function setObjectFieldValueAction(Request $request)
{
$field = $request->get('field');
$code = $request->get('code');
$objectId = $request->get('objectId');
$value = $originalValue = $request->get('value');
$context = $request->get('context');
$admin = $this->pool->getInstance($code);
$admin->setRequest($request);
// alter should be done by using a post method
if (!$request->isXmlHttpRequest()) {
return new JsonResponse('Expected an XmlHttpRequest request header', 405);
}
if ($request->getMethod() != 'POST') {
return new JsonResponse('Expected a POST Request', 405);
}
$rootObject = $object = $admin->getObject($objectId);
if (!$object) {
return new JsonResponse('Object does not exist', 404);
}
// check user permission
if (false === $admin->hasAccess('edit', $object)) {
return new JsonResponse('Invalid permissions', 403);
}
if ($context == 'list') {
$fieldDescription = $admin->getListFieldDescription($field);
} else {
return new JsonResponse('Invalid context', 400);
}
if (!$fieldDescription) {
return new JsonResponse('The field does not exist', 400);
}
if (!$fieldDescription->getOption('editable')) {
return new JsonResponse('The field cannot be edited, editable option must be set to true', 400);
}
$propertyPath = new PropertyPath($field);
// If property path has more than 1 element, take the last object in order to validate it
if ($propertyPath->getLength() > 1) {
$object = $this->pool->getPropertyAccessor()->getValue($object, $propertyPath->getParent());
$elements = $propertyPath->getElements();
$field = end($elements);
$propertyPath = new PropertyPath($field);
}
// Handle date type has setter expect a DateTime object
if ('' !== $value && $fieldDescription->getType() == 'date') {
$value = new \DateTime($value);
}
// Handle boolean type transforming the value into a boolean
if ('' !== $value && $fieldDescription->getType() == 'boolean') {
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
}
// Handle entity choice association type, transforming the value into entity
if ('' !== $value && $fieldDescription->getType() == 'choice' && $fieldDescription->getOption('class')) {
// Get existing associations for current object
$associations = $admin->getModelManager()
->getEntityManager($admin->getClass())->getClassMetadata($admin->getClass())
->getAssociationNames();
if (!in_array($field, $associations)) {
return new JsonResponse(
sprintf(
'Unknown association "%s", association does not exist in entity "%s", available associations are "%s".',
$field,
$this->admin->getClass(),
implode(', ', $associations)),
404);
}
$value = $admin->getConfigurationPool()->getContainer()->get('doctrine')->getManager()
->getRepository($fieldDescription->getOption('class'))
->find($value);
if (!$value) {
return new JsonResponse(
sprintf(
'Edit failed, object with id "%s" not found in association "%s".',
$originalValue,
$field),
404);
}
}
$this->pool->getPropertyAccessor()->setValue($object, $propertyPath, '' !== $value ? $value : null);
$violations = $this->validator->validate($object);
if (count($violations)) {
$messages = [];
foreach ($violations as $violation) {
$messages[] = $violation->getMessage();
}
return new JsonResponse(implode("\n", $messages), 400);
}
$admin->update($object);
// render the widget
// todo : fix this, the twig environment variable is not set inside the extension ...
$extension = $this->twig->getExtension('Sonata\AdminBundle\Twig\Extension\SonataAdminExtension');
$content = $extension->renderListElement($this->twig, $rootObject, $fieldDescription);
return new JsonResponse($content, 200);
}
/**
* 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('admin_code'));
$admin->setRequest($request);
$context = $request->get('_context', '');
if ($context === 'filter') {
$admin->checkAccess('list');
} elseif (!$admin->hasAccess('create') && !$admin->hasAccess('edit')) {
throw new AccessDeniedException();
}
// subject will be empty to avoid unnecessary database requests and keep autocomplete function fast
$admin->setSubject($admin->getNewInstance());
if ($context === 'filter') {
// filter
$fieldDescription = $this->retrieveFilterFieldDescription($admin, $request->get('field'));
$filterAutocomplete = $admin->getDatagrid()->getFilter($fieldDescription->getName());
$property = $filterAutocomplete->getFieldOption('property');
$callback = $filterAutocomplete->getFieldOption('callback');
$minimumInputLength = $filterAutocomplete->getFieldOption('minimum_input_length', 3);
$itemsPerPage = $filterAutocomplete->getFieldOption('items_per_page', 10);
$reqParamPageNumber = $filterAutocomplete->getFieldOption('req_param_name_page_number', '_page');
$toStringCallback = $filterAutocomplete->getFieldOption('to_string_callback');
$targetAdminAccessAction = $filterAutocomplete->getFieldOption('target_admin_access_action', 'list');
} else {
// create/edit form
$fieldDescription = $this->retrieveFormFieldDescription($admin, $request->get('field'));
$formAutocomplete = $admin->getForm()->get($fieldDescription->getName());
$formAutocompleteConfig = $formAutocomplete->getConfig();
if ($formAutocompleteConfig->getAttribute('disabled')) {
throw new AccessDeniedException(
'Autocomplete list can`t be retrieved because the form element is disabled or read_only.'
);
}
$property = $formAutocompleteConfig->getAttribute('property');
$callback = $formAutocompleteConfig->getAttribute('callback');
$minimumInputLength = $formAutocompleteConfig->getAttribute('minimum_input_length');
$itemsPerPage = $formAutocompleteConfig->getAttribute('items_per_page');
$reqParamPageNumber = $formAutocompleteConfig->getAttribute('req_param_name_page_number');
$toStringCallback = $formAutocompleteConfig->getAttribute('to_string_callback');
$targetAdminAccessAction = $formAutocompleteConfig->getAttribute('target_admin_access_action');
}
$searchText = $request->get('q');
$targetAdmin = $fieldDescription->getAssociationAdmin();
// check user permission
$targetAdmin->checkAccess($targetAdminAccessAction);
if (mb_strlen($searchText, 'UTF-8') < $minimumInputLength) {
return new JsonResponse(['status' => 'KO', 'message' => 'Too short search string.'], 403);
}
$targetAdmin->setPersistFilters(false);
$datagrid = $targetAdmin->getDatagrid();
if ($callback !== null) {
if (!is_callable($callback)) {
throw new \RuntimeException('Callback does not contain callable function.');
}
call_user_func($callback, $targetAdmin, $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.',
$property,
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 = [];
$results = $pager->getResults();
foreach ($results as $entity) {
if ($toStringCallback !== null) {
if (!is_callable($toStringCallback)) {
throw new \RuntimeException('Option "to_string_callback" does not contain callable function.');
}
$label = call_user_func($toStringCallback, $entity, $property);
} else {
$resultMetadata = $targetAdmin->getObjectMetadata($entity);
$label = $resultMetadata->getTitle();
}
$items[] = [
'id' => $admin->id($entity),
'label' => $label,
];
}
return new JsonResponse([
'status' => 'OK',
'more' => !$pager->isLastPage(),
'items' => $items,
]);
}
/**
* Retrieve the form field description given by field name.
*
* @param AdminInterface $admin
* @param string $field
*
* @return \Sonata\AdminBundle\Admin\FieldDescriptionInterface
*
* @throws \RuntimeException
*/
private function retrieveFormFieldDescription(AdminInterface $admin, $field)
{
$admin->getFormFieldDescriptions();
$fieldDescription = $admin->getFormFieldDescription($field);
if (!$fieldDescription) {
throw new \RuntimeException(sprintf('The field "%s" does not exist.', $field));
}
if (null === $fieldDescription->getTargetEntity()) {
throw new \RuntimeException(sprintf('No associated entity with field "%s".', $field));
}
return $fieldDescription;
}
/**
* Retrieve the filter field description given by field name.
*
* @param AdminInterface $admin
* @param string $field
*
* @return \Sonata\AdminBundle\Admin\FieldDescriptionInterface
*
* @throws \RuntimeException
*/
private function retrieveFilterFieldDescription(AdminInterface $admin, $field)
{
$admin->getFilterFieldDescriptions();
$fieldDescription = $admin->getFilterFieldDescription($field);
if (!$fieldDescription) {
throw new \RuntimeException(sprintf('The field "%s" does not exist.', $field));
}
if (null === $fieldDescription->getTargetEntity()) {
throw new \RuntimeException(sprintf('No associated entity with field "%s".', $field));
}
return $fieldDescription;
}
/**
* @return TwigRenderer
*/
private function getFormRenderer()
{
try {
$runtime = $this->twig->getRuntime('Symfony\Bridge\Twig\Form\TwigRenderer');
$runtime->setEnvironment($this->twig);
return $runtime;
} catch (\Twig_Error_Runtime $e) {
// BC for Symfony < 3.2 where this runtime not exists
$extension = $this->twig->getExtension('Symfony\Bridge\Twig\Extension\FormExtension');
$extension->initRuntime($this->twig);
return $extension->renderer;
}
}
}

View File

@@ -0,0 +1,321 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Datagrid;
use Sonata\AdminBundle\Admin\FieldDescriptionCollection;
use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
use Sonata\AdminBundle\Filter\FilterInterface;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class Datagrid implements DatagridInterface
{
/**
* The filter instances.
*
* @var array
*/
protected $filters = [];
/**
* @var array
*/
protected $values;
/**
* @var FieldDescriptionCollection
*/
protected $columns;
/**
* @var PagerInterface
*/
protected $pager;
/**
* @var bool
*/
protected $bound = false;
/**
* @var ProxyQueryInterface
*/
protected $query;
/**
* @var FormBuilderInterface
*/
protected $formBuilder;
/**
* @var FormInterface
*/
protected $form;
/**
* @var array
*/
protected $results;
/**
* @param ProxyQueryInterface $query
* @param FieldDescriptionCollection $columns
* @param PagerInterface $pager
* @param FormBuilderInterface $formBuilder
* @param array $values
*/
public function __construct(ProxyQueryInterface $query, FieldDescriptionCollection $columns, PagerInterface $pager, FormBuilderInterface $formBuilder, array $values = [])
{
$this->pager = $pager;
$this->query = $query;
$this->values = $values;
$this->columns = $columns;
$this->formBuilder = $formBuilder;
}
/**
* {@inheritdoc}
*/
public function getPager()
{
return $this->pager;
}
/**
* {@inheritdoc}
*/
public function getResults()
{
$this->buildPager();
if (null === $this->results) {
$this->results = $this->pager->getResults();
}
return $this->results;
}
/**
* {@inheritdoc}
*/
public function buildPager()
{
if ($this->bound) {
return;
}
foreach ($this->getFilters() as $name => $filter) {
list($type, $options) = $filter->getRenderSettings();
$this->formBuilder->add($filter->getFormName(), $type, $options);
}
// NEXT_MAJOR: Remove BC trick when bumping Symfony requirement to 2.8+
$hiddenType = method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')
? 'Symfony\Component\Form\Extension\Core\Type\HiddenType'
: 'hidden';
$this->formBuilder->add('_sort_by', $hiddenType);
$this->formBuilder->get('_sort_by')->addViewTransformer(new CallbackTransformer(
function ($value) {
return $value;
},
function ($value) {
return $value instanceof FieldDescriptionInterface ? $value->getName() : $value;
}
));
$this->formBuilder->add('_sort_order', $hiddenType);
$this->formBuilder->add('_page', $hiddenType);
$this->formBuilder->add('_per_page', $hiddenType);
$this->form = $this->formBuilder->getForm();
$this->form->submit($this->values);
$data = $this->form->getData();
foreach ($this->getFilters() as $name => $filter) {
$this->values[$name] = isset($this->values[$name]) ? $this->values[$name] : null;
$filter->apply($this->query, $data[$filter->getFormName()]);
}
if (isset($this->values['_sort_by'])) {
if (!$this->values['_sort_by'] instanceof FieldDescriptionInterface) {
throw new UnexpectedTypeException($this->values['_sort_by'], 'FieldDescriptionInterface');
}
if ($this->values['_sort_by']->isSortable()) {
$this->query->setSortBy($this->values['_sort_by']->getSortParentAssociationMapping(), $this->values['_sort_by']->getSortFieldMapping());
$this->query->setSortOrder(isset($this->values['_sort_order']) ? $this->values['_sort_order'] : null);
}
}
$maxPerPage = 25;
if (isset($this->values['_per_page'])) {
// check for `is_array` can be safely removed if php 5.3 support will be dropped
if (is_array($this->values['_per_page'])) {
if (isset($this->values['_per_page']['value'])) {
$maxPerPage = $this->values['_per_page']['value'];
}
} else {
$maxPerPage = $this->values['_per_page'];
}
}
$this->pager->setMaxPerPage($maxPerPage);
$page = 1;
if (isset($this->values['_page'])) {
// check for `is_array` can be safely removed if php 5.3 support will be dropped
if (is_array($this->values['_page'])) {
if (isset($this->values['_page']['value'])) {
$page = $this->values['_page']['value'];
}
} else {
$page = $this->values['_page'];
}
}
$this->pager->setPage($page);
$this->pager->setQuery($this->query);
$this->pager->init();
$this->bound = true;
}
/**
* {@inheritdoc}
*/
public function addFilter(FilterInterface $filter)
{
$this->filters[$filter->getName()] = $filter;
}
/**
* {@inheritdoc}
*/
public function hasFilter($name)
{
return isset($this->filters[$name]);
}
/**
* {@inheritdoc}
*/
public function removeFilter($name)
{
unset($this->filters[$name]);
}
/**
* {@inheritdoc}
*/
public function getFilter($name)
{
return $this->hasFilter($name) ? $this->filters[$name] : null;
}
/**
* {@inheritdoc}
*/
public function getFilters()
{
return $this->filters;
}
/**
* {@inheritdoc}
*/
public function reorderFilters(array $keys)
{
$this->filters = array_merge(array_flip($keys), $this->filters);
}
/**
* {@inheritdoc}
*/
public function getValues()
{
return $this->values;
}
/**
* {@inheritdoc}
*/
public function setValue($name, $operator, $value)
{
$this->values[$name] = [
'type' => $operator,
'value' => $value,
];
}
/**
* {@inheritdoc}
*/
public function hasActiveFilters()
{
foreach ($this->filters as $name => $filter) {
if ($filter->isActive()) {
return true;
}
}
return false;
}
/**
* {@inheritdoc}
*/
public function hasDisplayableFilters()
{
foreach ($this->filters as $name => $filter) {
$showFilter = $filter->getOption('show_filter', null);
if (($filter->isActive() && $showFilter === null) || ($showFilter === true)) {
return true;
}
}
return false;
}
/**
* {@inheritdoc}
*/
public function getColumns()
{
return $this->columns;
}
/**
* {@inheritdoc}
*/
public function getQuery()
{
return $this->query;
}
/**
* {@inheritdoc}
*/
public function getForm()
{
$this->buildPager();
return $this->form;
}
}

View File

@@ -0,0 +1,109 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Datagrid;
use Sonata\AdminBundle\Admin\FieldDescriptionCollection;
use Sonata\AdminBundle\Filter\FilterInterface;
use Symfony\Component\Form\FormInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
interface DatagridInterface
{
/**
* @return PagerInterface
*/
public function getPager();
/**
* @return ProxyQueryInterface
*/
public function getQuery();
/**
* @return array
*/
public function getResults();
public function buildPager();
/**
* @param FilterInterface $filter
*
* @return FilterInterface
*/
public function addFilter(FilterInterface $filter);
/**
* @return array
*/
public function getFilters();
/**
* Reorder filters.
*
* @param array $keys
*/
public function reorderFilters(array $keys);
/**
* @return array
*/
public function getValues();
/**
* @return FieldDescriptionCollection
*/
public function getColumns();
/**
* @param string $name
* @param string $operator
* @param mixed $value
*/
public function setValue($name, $operator, $value);
/**
* @return FormInterface
*/
public function getForm();
/**
* @param string $name
*
* @return FilterInterface
*/
public function getFilter($name);
/**
* @param string $name
*
* @return bool
*/
public function hasFilter($name);
/**
* @param string $name
*/
public function removeFilter($name);
/**
* @return bool
*/
public function hasActiveFilters();
/**
* @return bool
*/
public function hasDisplayableFilters();
}

View File

@@ -0,0 +1,138 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Datagrid;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
use Sonata\AdminBundle\Builder\DatagridBuilderInterface;
use Sonata\AdminBundle\Mapper\BaseMapper;
/**
* This class is use to simulate the Form API.
*
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class DatagridMapper extends BaseMapper
{
/**
* @var DatagridInterface
*/
protected $datagrid;
/**
* @param DatagridBuilderInterface $datagridBuilder
* @param DatagridInterface $datagrid
* @param AdminInterface $admin
*/
public function __construct(DatagridBuilderInterface $datagridBuilder, DatagridInterface $datagrid, AdminInterface $admin)
{
parent::__construct($datagridBuilder, $admin);
$this->datagrid = $datagrid;
}
/**
* @throws \RuntimeException
*
* @param string $name
* @param string $type
* @param array $filterOptions
* @param string $fieldType
* @param array $fieldOptions
* @param array $fieldDescriptionOptions
*
* @return DatagridMapper
*/
public function add($name, $type = null, array $filterOptions = [], $fieldType = null, $fieldOptions = null, array $fieldDescriptionOptions = [])
{
if (is_array($fieldOptions)) {
$filterOptions['field_options'] = $fieldOptions;
}
if ($fieldType) {
$filterOptions['field_type'] = $fieldType;
}
if ($name instanceof FieldDescriptionInterface) {
$fieldDescription = $name;
$fieldDescription->mergeOptions($filterOptions);
} elseif (is_string($name)) {
if ($this->admin->hasFilterFieldDescription($name)) {
throw new \RuntimeException(sprintf('Duplicate field name "%s" in datagrid mapper. Names should be unique.', $name));
}
if (!isset($filterOptions['field_name'])) {
$filterOptions['field_name'] = substr(strrchr('.'.$name, '.'), 1);
}
$fieldDescription = $this->admin->getModelManager()->getNewFieldDescriptionInstance(
$this->admin->getClass(),
$name,
array_merge($filterOptions, $fieldDescriptionOptions)
);
} else {
throw new \RuntimeException(
'Unknown field name in datagrid mapper.'
.' Field name should be either of FieldDescriptionInterface interface or string.'
);
}
// add the field with the DatagridBuilder
$this->builder->addFilter($this->datagrid, $type, $fieldDescription, $this->admin);
return $this;
}
/**
* {@inheritdoc}
*/
public function get($name)
{
return $this->datagrid->getFilter($name);
}
/**
* {@inheritdoc}
*/
public function has($key)
{
return $this->datagrid->hasFilter($key);
}
/**
* {@inheritdoc}
*/
final public function keys()
{
return array_keys($this->datagrid->getFilters());
}
/**
* {@inheritdoc}
*/
public function remove($key)
{
$this->admin->removeFilterFieldDescription($key);
$this->datagrid->removeFilter($key);
return $this;
}
/**
* {@inheritdoc}
*/
public function reorder(array $keys)
{
$this->datagrid->reorderFilters($keys);
return $this;
}
}

View File

@@ -0,0 +1,177 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Datagrid;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Admin\FieldDescriptionCollection;
use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
use Sonata\AdminBundle\Builder\ListBuilderInterface;
use Sonata\AdminBundle\Mapper\BaseMapper;
/**
* This class is used to simulate the Form API.
*
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class ListMapper extends BaseMapper
{
/**
* @var FieldDescriptionCollection
*/
protected $list;
/**
* @param ListBuilderInterface $listBuilder
* @param FieldDescriptionCollection $list
* @param AdminInterface $admin
*/
public function __construct(ListBuilderInterface $listBuilder, FieldDescriptionCollection $list, AdminInterface $admin)
{
parent::__construct($listBuilder, $admin);
$this->list = $list;
}
/**
* @param string $name
* @param null $type
* @param array $fieldDescriptionOptions
*
* @return ListMapper
*/
public function addIdentifier($name, $type = null, array $fieldDescriptionOptions = [])
{
$fieldDescriptionOptions['identifier'] = true;
if (!isset($fieldDescriptionOptions['route']['name'])) {
$routeName = ($this->admin->hasAccess('edit') && $this->admin->hasRoute('edit')) ? 'edit' : 'show';
$fieldDescriptionOptions['route']['name'] = $routeName;
}
if (!isset($fieldDescriptionOptions['route']['parameters'])) {
$fieldDescriptionOptions['route']['parameters'] = [];
}
return $this->add($name, $type, $fieldDescriptionOptions);
}
/**
* @throws \RuntimeException
*
* @param mixed $name
* @param mixed $type
* @param array $fieldDescriptionOptions
*
* @return ListMapper
*/
public function add($name, $type = null, array $fieldDescriptionOptions = [])
{
// Change deprecated inline action "view" to "show"
if ($name == '_action' && $type == 'actions') {
if (isset($fieldDescriptionOptions['actions']['view'])) {
@trigger_error(
'Inline action "view" is deprecated since version 2.2.4 and will be removed in 4.0. '
.'Use inline action "show" instead.',
E_USER_DEPRECATED
);
$fieldDescriptionOptions['actions']['show'] = $fieldDescriptionOptions['actions']['view'];
unset($fieldDescriptionOptions['actions']['view']);
}
}
// Ensure batch and action pseudo-fields are tagged as virtual
if (in_array($type, ['actions', 'batch', 'select'])) {
$fieldDescriptionOptions['virtual_field'] = true;
}
if ($name instanceof FieldDescriptionInterface) {
$fieldDescription = $name;
$fieldDescription->mergeOptions($fieldDescriptionOptions);
} elseif (is_string($name)) {
if ($this->admin->hasListFieldDescription($name)) {
throw new \RuntimeException(sprintf(
'Duplicate field name "%s" in list mapper. Names should be unique.',
$name
));
}
$fieldDescription = $this->admin->getModelManager()->getNewFieldDescriptionInstance(
$this->admin->getClass(),
$name,
$fieldDescriptionOptions
);
} else {
throw new \RuntimeException(
'Unknown field name in list mapper. '
.'Field name should be either of FieldDescriptionInterface interface or string.'
);
}
if ($fieldDescription->getLabel() === null) {
$fieldDescription->setOption(
'label',
$this->admin->getLabelTranslatorStrategy()->getLabel($fieldDescription->getName(), 'list', 'label')
);
}
// add the field with the FormBuilder
$this->builder->addField($this->list, $type, $fieldDescription, $this->admin);
return $this;
}
/**
* {@inheritdoc}
*/
public function get($name)
{
return $this->list->get($name);
}
/**
* {@inheritdoc}
*/
public function has($key)
{
return $this->list->has($key);
}
/**
* {@inheritdoc}
*/
public function remove($key)
{
$this->admin->removeListFieldDescription($key);
$this->list->remove($key);
return $this;
}
/**
* {@inheritdoc}
*/
final public function keys()
{
return array_keys($this->list->getElements());
}
/**
* {@inheritdoc}
*/
public function reorder(array $keys)
{
$this->list->reorder($keys);
return $this;
}
}

View File

@@ -0,0 +1,693 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Datagrid;
/**
* @author Fabien Potencier <fabien.potencier@symfony-project.com>
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
abstract class Pager implements \Iterator, \Countable, \Serializable, PagerInterface
{
const TYPE_DEFAULT = 'default';
const TYPE_SIMPLE = 'simple';
/**
* @var int
*/
protected $page = 1;
/**
* @var int
*/
protected $maxPerPage = 0;
/**
* @var int
*/
protected $lastPage = 1;
/**
* @var int
*/
protected $nbResults = 0;
/**
* @var int
*/
protected $cursor = 1;
/**
* @var array
*/
protected $parameters = [];
/**
* @var int
*/
protected $currentMaxLink = 1;
/**
* @var mixed bool|int
*/
protected $maxRecordLimit = false;
/**
* @var int
*/
protected $maxPageLinks = 0;
// used by iterator interface
/**
* @var \Traversable|array|null
*/
protected $results = null;
/**
* @var int
*/
protected $resultsCounter = 0;
/**
* @var ProxyQueryInterface|null
*/
protected $query = null;
/**
* @var array
*/
protected $countColumn = ['id'];
/**
* @param int $maxPerPage Number of records to display per page
*/
public function __construct($maxPerPage = 10)
{
$this->setMaxPerPage($maxPerPage);
}
/**
* Returns the current pager's max link.
*
* @return int
*/
public function getCurrentMaxLink()
{
return $this->currentMaxLink;
}
/**
* Returns the current pager's max record limit.
*
* @return int
*/
public function getMaxRecordLimit()
{
return $this->maxRecordLimit;
}
/**
* Sets the current pager's max record limit.
*
* @param int $limit
*/
public function setMaxRecordLimit($limit)
{
$this->maxRecordLimit = $limit;
}
/**
* Returns an array of page numbers to use in pagination links.
*
* @param int $nbLinks The maximum number of page numbers to return
*
* @return array
*/
public function getLinks($nbLinks = null)
{
if ($nbLinks == null) {
$nbLinks = $this->getMaxPageLinks();
}
$links = [];
$tmp = $this->page - floor($nbLinks / 2);
$check = $this->lastPage - $nbLinks + 1;
$limit = $check > 0 ? $check : 1;
$begin = $tmp > 0 ? ($tmp > $limit ? $limit : $tmp) : 1;
$i = (int) $begin;
while ($i < $begin + $nbLinks && $i <= $this->lastPage) {
$links[] = $i++;
}
$this->currentMaxLink = count($links) ? $links[count($links) - 1] : 1;
return $links;
}
/**
* Returns true if the current query requires pagination.
*
* @return bool
*/
public function haveToPaginate()
{
return $this->getMaxPerPage() && $this->getNbResults() > $this->getMaxPerPage();
}
/**
* Returns the current cursor.
*
* @return int
*/
public function getCursor()
{
return $this->cursor;
}
/**
* Sets the current cursor.
*
* @param int $pos
*/
public function setCursor($pos)
{
if ($pos < 1) {
$this->cursor = 1;
} else {
if ($pos > $this->nbResults) {
$this->cursor = $this->nbResults;
} else {
$this->cursor = $pos;
}
}
}
/**
* Returns an object by cursor position.
*
* @param int $pos
*
* @return mixed
*/
public function getObjectByCursor($pos)
{
$this->setCursor($pos);
return $this->getCurrent();
}
/**
* Returns the current object.
*
* @return mixed
*/
public function getCurrent()
{
return $this->retrieveObject($this->cursor);
}
/**
* Returns the next object.
*
* @return mixed|null
*/
public function getNext()
{
if ($this->cursor + 1 > $this->nbResults) {
return;
}
return $this->retrieveObject($this->cursor + 1);
}
/**
* Returns the previous object.
*
* @return mixed|null
*/
public function getPrevious()
{
if ($this->cursor - 1 < 1) {
return;
}
return $this->retrieveObject($this->cursor - 1);
}
/**
* Returns the first index on the current page.
*
* @return int
*/
public function getFirstIndex()
{
if ($this->page == 0) {
return 1;
}
return ($this->page - 1) * $this->maxPerPage + 1;
}
/**
* NEXT_MAJOR: remove this method.
*
* @deprecated since 3.11, will be removed in 4.0
*/
public function getFirstIndice()
{
@trigger_error(
'Method '.__METHOD__.' is deprecated since version 3.11 and will be removed in 4.0, '.
'please use getFirstIndex() instead.',
E_USER_DEPRECATED
);
return $this->getFirstIndex();
}
/**
* Returns the last index on the current page.
*
* @return int
*/
public function getLastIndex()
{
if ($this->page == 0) {
return $this->nbResults;
}
if ($this->page * $this->maxPerPage >= $this->nbResults) {
return $this->nbResults;
}
return $this->page * $this->maxPerPage;
}
/**
* NEXT_MAJOR: remove this method.
*
* @deprecated since 3.11, will be removed in 4.0
*/
public function getLastIndice()
{
@trigger_error(
'Method '.__METHOD__.' is deprecated since version 3.11 and will be removed in 4.0, '.
'please use getLastIndex() instead.',
E_USER_DEPRECATED
);
return $this->getLastIndex();
}
/**
* Returns the number of results.
*
* @return int
*/
public function getNbResults()
{
return $this->nbResults;
}
/**
* Returns the first page number.
*
* @return int
*/
public function getFirstPage()
{
return 1;
}
/**
* Returns the last page number.
*
* @return int
*/
public function getLastPage()
{
return $this->lastPage;
}
/**
* Returns the current page.
*
* @return int
*/
public function getPage()
{
return $this->page;
}
/**
* Returns the next page.
*
* @return int
*/
public function getNextPage()
{
return min($this->getPage() + 1, $this->getLastPage());
}
/**
* Returns the previous page.
*
* @return int
*/
public function getPreviousPage()
{
return max($this->getPage() - 1, $this->getFirstPage());
}
/**
* {@inheritdoc}
*/
public function setPage($page)
{
$this->page = intval($page);
if ($this->page <= 0) {
// set first page, which depends on a maximum set
$this->page = $this->getMaxPerPage() ? 1 : 0;
}
}
/**
* {@inheritdoc}
*/
public function getMaxPerPage()
{
return $this->maxPerPage;
}
/**
* {@inheritdoc}
*/
public function setMaxPerPage($max)
{
if ($max > 0) {
$this->maxPerPage = $max;
if ($this->page == 0) {
$this->page = 1;
}
} else {
if ($max == 0) {
$this->maxPerPage = 0;
$this->page = 0;
} else {
$this->maxPerPage = 1;
if ($this->page == 0) {
$this->page = 1;
}
}
}
}
/**
* {@inheritdoc}
*/
public function getMaxPageLinks()
{
return $this->maxPageLinks;
}
/**
* {@inheritdoc}
*/
public function setMaxPageLinks($maxPageLinks)
{
$this->maxPageLinks = $maxPageLinks;
}
/**
* Returns true if on the first page.
*
* @return bool
*/
public function isFirstPage()
{
return 1 == $this->page;
}
/**
* Returns true if on the last page.
*
* @return bool
*/
public function isLastPage()
{
return $this->page == $this->lastPage;
}
/**
* Returns the current pager's parameter holder.
*
* @return array
*/
public function getParameters()
{
return $this->parameters;
}
/**
* Returns a parameter.
*
* @param string $name
* @param mixed $default
*
* @return mixed
*/
public function getParameter($name, $default = null)
{
return isset($this->parameters[$name]) ? $this->parameters[$name] : $default;
}
/**
* Checks whether a parameter has been set.
*
* @param string $name
*
* @return bool
*/
public function hasParameter($name)
{
return isset($this->parameters[$name]);
}
/**
* Sets a parameter.
*
* @param string $name
* @param mixed $value
*/
public function setParameter($name, $value)
{
$this->parameters[$name] = $value;
}
/**
* {@inheritdoc}
*/
public function current()
{
if (!$this->isIteratorInitialized()) {
$this->initializeIterator();
}
return current($this->results);
}
/**
* {@inheritdoc}
*/
public function key()
{
if (!$this->isIteratorInitialized()) {
$this->initializeIterator();
}
return key($this->results);
}
/**
* {@inheritdoc}
*/
public function next()
{
if (!$this->isIteratorInitialized()) {
$this->initializeIterator();
}
--$this->resultsCounter;
return next($this->results);
}
/**
* {@inheritdoc}
*/
public function rewind()
{
if (!$this->isIteratorInitialized()) {
$this->initializeIterator();
}
$this->resultsCounter = count($this->results);
return reset($this->results);
}
/**
* {@inheritdoc}
*/
public function valid()
{
if (!$this->isIteratorInitialized()) {
$this->initializeIterator();
}
return $this->resultsCounter > 0;
}
/**
* {@inheritdoc}
*/
public function count()
{
return $this->getNbResults();
}
/**
* {@inheritdoc}
*/
public function serialize()
{
$vars = get_object_vars($this);
unset($vars['query']);
return serialize($vars);
}
/**
* {@inheritdoc}
*/
public function unserialize($serialized)
{
$array = unserialize($serialized);
foreach ($array as $name => $values) {
$this->$name = $values;
}
}
/**
* @return array
*/
public function getCountColumn()
{
return $this->countColumn;
}
/**
* @param array $countColumn
*
* @return array
*/
public function setCountColumn(array $countColumn)
{
return $this->countColumn = $countColumn;
}
/**
* {@inheritdoc}
*/
public function setQuery($query)
{
$this->query = $query;
}
/**
* @return ProxyQueryInterface
*/
public function getQuery()
{
return $this->query;
}
/**
* Sets the number of results.
*
* @param int $nb
*/
protected function setNbResults($nb)
{
$this->nbResults = $nb;
}
/**
* Sets the last page number.
*
* @param int $page
*/
protected function setLastPage($page)
{
$this->lastPage = $page;
if ($this->getPage() > $page) {
$this->setPage($page);
}
}
/**
* Returns true if the properties used for iteration have been initialized.
*
* @return bool
*/
protected function isIteratorInitialized()
{
return null !== $this->results;
}
/**
* Loads data into properties used for iteration.
*/
protected function initializeIterator()
{
$this->results = $this->getResults();
$this->resultsCounter = count($this->results);
}
/**
* Empties properties used for iteration.
*/
protected function resetIterator()
{
$this->results = null;
$this->resultsCounter = 0;
}
/**
* Retrieve the object for a certain offset.
*
* @param int $offset
*
* @return object
*/
protected function retrieveObject($offset)
{
$queryForRetrieve = clone $this->getQuery();
$queryForRetrieve
->setFirstResult($offset - 1)
->setMaxResults(1);
$results = $queryForRetrieve->execute();
return $results[0];
}
}

View File

@@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Datagrid;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
interface PagerInterface
{
/**
* Initialize the Pager.
*/
public function init();
/**
* Returns the maximum number of results per page.
*
* @return int
*/
public function getMaxPerPage();
/**
* Sets the maximum number of results per page.
*
* @param int $max
*/
public function setMaxPerPage($max);
/**
* Sets the current page.
*
* @param int $page
*/
public function setPage($page);
/**
* Set query.
*
* @param ProxyQueryInterface $query
*/
public function setQuery($query);
/**
* Returns an array of results on the given page.
*
* @return array
*/
public function getResults();
/**
* Sets the maximum number of page numbers.
*
* @param int $maxPageLinks
*/
public function setMaxPageLinks($maxPageLinks);
/**
* Returns the maximum number of page numbers.
*
* @return int
*/
public function getMaxPageLinks();
}

View File

@@ -0,0 +1,102 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Datagrid;
/**
* Used by the Datagrid to build the query.
*
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
interface ProxyQueryInterface
{
/**
* @param string $name
* @param array $args
*
* @return mixed
*/
public function __call($name, $args);
/**
* @param array $params
* @param null $hydrationMode
*
* @return mixed
*/
public function execute(array $params = [], $hydrationMode = null);
/**
* @param array $parentAssociationMappings
* @param array $fieldMapping
*
* @return ProxyQueryInterface
*/
public function setSortBy($parentAssociationMappings, $fieldMapping);
/**
* @return mixed
*/
public function getSortBy();
/**
* @param mixed $sortOrder
*
* @return ProxyQueryInterface
*/
public function setSortOrder($sortOrder);
/**
* @return mixed
*/
public function getSortOrder();
/**
* @return mixed
*/
public function getSingleScalarResult();
/**
* @param int $firstResult
*
* @return ProxyQueryInterface
*/
public function setFirstResult($firstResult);
/**
* @return mixed
*/
public function getFirstResult();
/**
* @param int $maxResults
*
* @return ProxyQueryInterface
*/
public function setMaxResults($maxResults);
/**
* @return mixed
*/
public function getMaxResults();
/**
* @return mixed
*/
public function getUniqueParameterId();
/**
* @param array $associationMappings
*
* @return mixed
*/
public function entityJoin(array $associationMappings);
}

View File

@@ -0,0 +1,161 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Datagrid;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @author Lukas Kahwe Smith <smith@pooteeweet.org>
* @author Sjoerd Peters <sjoerd.peters@gmail.com>
*/
class SimplePager extends Pager
{
/**
* @var bool
*/
protected $haveToPaginate;
/**
* How many pages to look forward to create links to next pages.
*
* @var int
*/
protected $threshold;
/**
* @var int
*/
protected $thresholdCount;
/**
* The threshold parameter can be used to determine how far ahead the pager
* should fetch results.
*
* If set to 1 which is the minimal value the pager will generate a link to the next page
* If set to 2 the pager will generate links to the next two pages
* If set to 3 the pager will generate links to the next three pages
* etc.
*
* @param int $maxPerPage Number of records to display per page
* @param int $threshold
*/
public function __construct($maxPerPage = 10, $threshold = 1)
{
parent::__construct($maxPerPage);
$this->setThreshold($threshold);
}
/**
* {@inheritdoc}
*/
public function getNbResults()
{
$n = ceil(($this->getLastPage() - 1) * $this->getMaxPerPage());
if ($this->getLastPage() == $this->getPage()) {
return $n + $this->thresholdCount;
}
return $n;
}
/**
* {@inheritdoc}
*/
public function getResults($hydrationMode = null)
{
if ($this->results) {
return $this->results;
}
$this->results = $this->getQuery()->execute([], $hydrationMode);
$this->thresholdCount = count($this->results);
if (count($this->results) > $this->getMaxPerPage()) {
$this->haveToPaginate = true;
if ($this->results instanceof ArrayCollection) {
$this->results = new ArrayCollection($this->results->slice(0, $this->getMaxPerPage()));
} else {
$this->results = new ArrayCollection(array_slice($this->results, 0, $this->getMaxPerPage()));
}
} else {
$this->haveToPaginate = false;
}
return $this->results;
}
/**
* {@inheritdoc}
*/
public function haveToPaginate()
{
return $this->haveToPaginate || $this->getPage() > 1;
}
/**
* {@inheritdoc}
*
* @throws \RuntimeException the QueryBuilder is uninitialized
*/
public function init()
{
if (!$this->getQuery()) {
throw new \RuntimeException('Uninitialized QueryBuilder');
}
$this->resetIterator();
if (0 == $this->getPage() || 0 == $this->getMaxPerPage()) {
$this->setLastPage(0);
$this->getQuery()->setFirstResult(0);
$this->getQuery()->setMaxResults(0);
} else {
$offset = ($this->getPage() - 1) * $this->getMaxPerPage();
$this->getQuery()->setFirstResult($offset);
$maxOffset = $this->getThreshold() > 0
? $this->getMaxPerPage() * $this->threshold + 1 : $this->getMaxPerPage() + 1;
$this->getQuery()->setMaxResults($maxOffset);
$this->initializeIterator();
$t = (int) ceil($this->thresholdCount / $this->getMaxPerPage()) + $this->getPage() - 1;
$this->setLastPage($t);
}
}
/**
* Set how many pages to look forward to create links to next pages.
*
* @param int $threshold
*/
public function setThreshold($threshold)
{
$this->threshold = (int) $threshold;
}
/**
* @return int
*/
public function getThreshold()
{
return $this->threshold;
}
/**
* {@inheritdoc}
*/
protected function resetIterator()
{
parent::resetIterator();
$this->haveToPaginate = false;
}
}

View File

@@ -0,0 +1,116 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
abstract class AbstractSonataAdminExtension extends Extension
{
/**
* Fix template configuration.
*
* @param array $configs
* @param ContainerBuilder $container
* @param array $defaultSonataDoctrineConfig
*
* @return array
*/
protected function fixTemplatesConfiguration(array $configs, ContainerBuilder $container, array $defaultSonataDoctrineConfig = [])
{
$defaultConfig = [
'templates' => [
'types' => [
'list' => [
'array' => 'SonataAdminBundle:CRUD:list_array.html.twig',
'boolean' => 'SonataAdminBundle:CRUD:list_boolean.html.twig',
'date' => 'SonataAdminBundle:CRUD:list_date.html.twig',
'time' => 'SonataAdminBundle:CRUD:list_time.html.twig',
'datetime' => 'SonataAdminBundle:CRUD:list_datetime.html.twig',
'text' => 'SonataAdminBundle:CRUD:list_string.html.twig',
'textarea' => 'SonataAdminBundle:CRUD:list_string.html.twig',
'email' => 'SonataAdminBundle:CRUD:list_email.html.twig',
'trans' => 'SonataAdminBundle:CRUD:list_trans.html.twig',
'string' => 'SonataAdminBundle:CRUD:list_string.html.twig',
'smallint' => 'SonataAdminBundle:CRUD:list_string.html.twig',
'bigint' => 'SonataAdminBundle:CRUD:list_string.html.twig',
'integer' => 'SonataAdminBundle:CRUD:list_string.html.twig',
'decimal' => 'SonataAdminBundle:CRUD:list_string.html.twig',
'identifier' => 'SonataAdminBundle:CRUD:list_string.html.twig',
'currency' => 'SonataAdminBundle:CRUD:list_currency.html.twig',
'percent' => 'SonataAdminBundle:CRUD:list_percent.html.twig',
'choice' => 'SonataAdminBundle:CRUD:list_choice.html.twig',
'url' => 'SonataAdminBundle:CRUD:list_url.html.twig',
'html' => 'SonataAdminBundle:CRUD:list_html.html.twig',
],
'show' => [
'array' => 'SonataAdminBundle:CRUD:show_array.html.twig',
'boolean' => 'SonataAdminBundle:CRUD:show_boolean.html.twig',
'date' => 'SonataAdminBundle:CRUD:show_date.html.twig',
'time' => 'SonataAdminBundle:CRUD:show_time.html.twig',
'datetime' => 'SonataAdminBundle:CRUD:show_datetime.html.twig',
'text' => 'SonataAdminBundle:CRUD:base_show_field.html.twig',
'email' => 'SonataAdminBundle:CRUD:show_email.html.twig',
'trans' => 'SonataAdminBundle:CRUD:show_trans.html.twig',
'string' => 'SonataAdminBundle:CRUD:base_show_field.html.twig',
'smallint' => 'SonataAdminBundle:CRUD:base_show_field.html.twig',
'bigint' => 'SonataAdminBundle:CRUD:base_show_field.html.twig',
'integer' => 'SonataAdminBundle:CRUD:base_show_field.html.twig',
'decimal' => 'SonataAdminBundle:CRUD:base_show_field.html.twig',
'currency' => 'SonataAdminBundle:CRUD:show_currency.html.twig',
'percent' => 'SonataAdminBundle:CRUD:show_percent.html.twig',
'choice' => 'SonataAdminBundle:CRUD:show_choice.html.twig',
'url' => 'SonataAdminBundle:CRUD:show_url.html.twig',
'html' => 'SonataAdminBundle:CRUD:show_html.html.twig',
],
],
],
];
// let's add some magic, only overwrite template if the SonataIntlBundle is enabled
$bundles = $container->getParameter('kernel.bundles');
if (isset($bundles['SonataIntlBundle'])) {
$defaultConfig['templates']['types']['list'] = array_merge($defaultConfig['templates']['types']['list'], [
'date' => 'SonataIntlBundle:CRUD:list_date.html.twig',
'datetime' => 'SonataIntlBundle:CRUD:list_datetime.html.twig',
'smallint' => 'SonataIntlBundle:CRUD:list_decimal.html.twig',
'bigint' => 'SonataIntlBundle:CRUD:list_decimal.html.twig',
'integer' => 'SonataIntlBundle:CRUD:list_decimal.html.twig',
'decimal' => 'SonataIntlBundle:CRUD:list_decimal.html.twig',
'currency' => 'SonataIntlBundle:CRUD:list_currency.html.twig',
'percent' => 'SonataIntlBundle:CRUD:list_percent.html.twig',
]);
$defaultConfig['templates']['types']['show'] = array_merge($defaultConfig['templates']['types']['show'], [
'date' => 'SonataIntlBundle:CRUD:show_date.html.twig',
'datetime' => 'SonataIntlBundle:CRUD:show_datetime.html.twig',
'smallint' => 'SonataIntlBundle:CRUD:show_decimal.html.twig',
'bigint' => 'SonataIntlBundle:CRUD:show_decimal.html.twig',
'integer' => 'SonataIntlBundle:CRUD:show_decimal.html.twig',
'decimal' => 'SonataIntlBundle:CRUD:show_decimal.html.twig',
'currency' => 'SonataIntlBundle:CRUD:show_currency.html.twig',
'percent' => 'SonataIntlBundle:CRUD:show_percent.html.twig',
]);
}
if (!empty($defaultSonataDoctrineConfig)) {
$defaultConfig = array_merge_recursive($defaultConfig, $defaultSonataDoctrineConfig);
}
array_unshift($configs, $defaultConfig);
return $configs;
}
}

View File

@@ -0,0 +1,421 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\DependencyInjection\Compiler;
use Doctrine\Common\Inflector\Inflector;
use Sonata\AdminBundle\Datagrid\Pager;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\Reference;
/**
* Add all dependencies to the Admin class, this avoid to write too many lines
* in the configuration files.
*
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class AddDependencyCallsCompilerPass implements CompilerPassInterface
{
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
// check if translator service exist
if (!$container->has('translator')) {
throw new \RuntimeException('The "translator" service is not yet enabled.
It\'s required by SonataAdmin to display all labels properly.
To learn how to enable the translator service please visit:
http://symfony.com/doc/current/translation.html#configuration
');
}
$parameterBag = $container->getParameterBag();
$groupDefaults = $admins = $classes = [];
$pool = $container->getDefinition('sonata.admin.pool');
foreach ($container->findTaggedServiceIds('sonata.admin') as $id => $tags) {
foreach ($tags as $attributes) {
$definition = $container->getDefinition($id);
$parentDefinition = null;
// NEXT_MAJOR: Remove check for DefinitionDecorator instance when dropping Symfony <3.3 support
if ($definition instanceof ChildDefinition || $definition instanceof DefinitionDecorator) {
$parentDefinition = $container->getDefinition($definition->getParent());
}
$this->replaceDefaultArguments([
0 => $id,
2 => 'SonataAdminBundle:CRUD',
], $definition, $parentDefinition);
$this->applyConfigurationFromAttribute($definition, $attributes);
$this->applyDefaults($container, $id, $attributes);
$arguments = $parentDefinition ?
array_merge($parentDefinition->getArguments(), $definition->getArguments()) :
$definition->getArguments();
$admins[] = $id;
if (!isset($classes[$arguments[1]])) {
$classes[$arguments[1]] = [];
}
$classes[$arguments[1]][] = $id;
$showInDashboard = (bool) (isset($attributes['show_in_dashboard']) ? $parameterBag->resolveValue($attributes['show_in_dashboard']) : true);
if (!$showInDashboard) {
continue;
}
$resolvedGroupName = isset($attributes['group']) ? $parameterBag->resolveValue($attributes['group']) : 'default';
$labelCatalogue = isset($attributes['label_catalogue']) ? $attributes['label_catalogue'] : 'SonataAdminBundle';
$icon = isset($attributes['icon']) ? $attributes['icon'] : '<i class="fa fa-folder"></i>';
$onTop = isset($attributes['on_top']) ? $attributes['on_top'] : false;
$keepOpen = isset($attributes['keep_open']) ? $attributes['keep_open'] : false;
if (!isset($groupDefaults[$resolvedGroupName])) {
$groupDefaults[$resolvedGroupName] = [
'label' => $resolvedGroupName,
'label_catalogue' => $labelCatalogue,
'icon' => $icon,
'roles' => [],
'on_top' => false,
'keep_open' => false,
];
}
$groupDefaults[$resolvedGroupName]['items'][] = [
'admin' => $id,
'label' => !empty($attributes['label']) ? $attributes['label'] : '',
'route' => '',
'route_params' => [],
'route_absolute' => false,
];
if (isset($groupDefaults[$resolvedGroupName]['on_top']) && $groupDefaults[$resolvedGroupName]['on_top']
|| $onTop && (count($groupDefaults[$resolvedGroupName]['items']) > 1)) {
throw new \RuntimeException('You can\'t use "on_top" option with multiple same name groups.');
}
$groupDefaults[$resolvedGroupName]['on_top'] = $onTop;
$groupDefaults[$resolvedGroupName]['keep_open'] = $keepOpen;
}
}
$dashboardGroupsSettings = $container->getParameter('sonata.admin.configuration.dashboard_groups');
if (!empty($dashboardGroupsSettings)) {
$groups = $dashboardGroupsSettings;
foreach ($dashboardGroupsSettings as $groupName => $group) {
$resolvedGroupName = $parameterBag->resolveValue($groupName);
if (!isset($groupDefaults[$resolvedGroupName])) {
$groupDefaults[$resolvedGroupName] = [
'items' => [],
'label' => $resolvedGroupName,
'roles' => [],
'on_top' => false,
'keep_open' => false,
];
}
if (empty($group['items'])) {
$groups[$resolvedGroupName]['items'] = $groupDefaults[$resolvedGroupName]['items'];
}
if (empty($group['label'])) {
$groups[$resolvedGroupName]['label'] = $groupDefaults[$resolvedGroupName]['label'];
}
if (empty($group['label_catalogue'])) {
$groups[$resolvedGroupName]['label_catalogue'] = 'SonataAdminBundle';
}
if (empty($group['icon'])) {
$groups[$resolvedGroupName]['icon'] = $groupDefaults[$resolvedGroupName]['icon'];
}
if (!empty($group['item_adds'])) {
$groups[$resolvedGroupName]['items'] = array_merge($groups[$resolvedGroupName]['items'], $group['item_adds']);
}
if (empty($group['roles'])) {
$groups[$resolvedGroupName]['roles'] = $groupDefaults[$resolvedGroupName]['roles'];
}
if (isset($groups[$resolvedGroupName]['on_top']) && !empty($group['on_top']) && $group['on_top']
&& (count($groups[$resolvedGroupName]['items']) > 1)) {
throw new \RuntimeException('You can\'t use "on_top" option with multiple same name groups.');
}
if (empty($group['on_top'])) {
$groups[$resolvedGroupName]['on_top'] = $groupDefaults[$resolvedGroupName]['on_top'];
}
if (empty($group['keep_open'])) {
$groups[$resolvedGroupName]['keep_open'] = $groupDefaults[$resolvedGroupName]['keep_open'];
}
}
} elseif ($container->getParameter('sonata.admin.configuration.sort_admins')) {
$groups = $groupDefaults;
$elementSort = function (&$element) {
usort(
$element['items'],
function ($a, $b) {
$a = !empty($a['label']) ? $a['label'] : $a['admin'];
$b = !empty($b['label']) ? $b['label'] : $b['admin'];
if ($a === $b) {
return 0;
}
return $a < $b ? -1 : 1;
}
);
};
/*
* 1) sort the groups by their index
* 2) sort the elements within each group by label/admin
*/
ksort($groups);
array_walk($groups, $elementSort);
} else {
$groups = $groupDefaults;
}
$pool->addMethodCall('setAdminServiceIds', [$admins]);
$pool->addMethodCall('setAdminGroups', [$groups]);
$pool->addMethodCall('setAdminClasses', [$classes]);
$routeLoader = $container->getDefinition('sonata.admin.route_loader');
$routeLoader->replaceArgument(1, $admins);
}
/**
* This method read the attribute keys and configure admin class to use the related dependency.
*
* @param Definition $definition
* @param array $attributes
*/
public function applyConfigurationFromAttribute(Definition $definition, array $attributes)
{
$keys = [
'model_manager',
'form_contractor',
'show_builder',
'list_builder',
'datagrid_builder',
'translator',
'configuration_pool',
'router',
'validator',
'security_handler',
'menu_factory',
'route_builder',
'label_translator_strategy',
];
foreach ($keys as $key) {
$method = 'set'.Inflector::classify($key);
if (!isset($attributes[$key]) || $definition->hasMethodCall($method)) {
continue;
}
$definition->addMethodCall($method, [new Reference($attributes[$key])]);
}
}
/**
* Apply the default values required by the AdminInterface to the Admin service definition.
*
* @param ContainerBuilder $container
* @param string $serviceId
* @param array $attributes
*
* @return Definition
*/
public function applyDefaults(ContainerBuilder $container, $serviceId, array $attributes = [])
{
$definition = $container->getDefinition($serviceId);
$settings = $container->getParameter('sonata.admin.configuration.admin_services');
if (method_exists($definition, 'setShared')) { // Symfony 2.8+
$definition->setShared(false);
} else { // For Symfony <2.8 compatibility
$definition->setScope(ContainerInterface::SCOPE_PROTOTYPE);
}
$manager_type = $attributes['manager_type'];
$overwriteAdminConfiguration = isset($settings[$serviceId]) ? $settings[$serviceId] : [];
$defaultAddServices = [
'model_manager' => sprintf('sonata.admin.manager.%s', $manager_type),
'form_contractor' => sprintf('sonata.admin.builder.%s_form', $manager_type),
'show_builder' => sprintf('sonata.admin.builder.%s_show', $manager_type),
'list_builder' => sprintf('sonata.admin.builder.%s_list', $manager_type),
'datagrid_builder' => sprintf('sonata.admin.builder.%s_datagrid', $manager_type),
'translator' => 'translator',
'configuration_pool' => 'sonata.admin.pool',
'route_generator' => 'sonata.admin.route.default_generator',
'validator' => 'validator',
'security_handler' => 'sonata.admin.security.handler',
'menu_factory' => 'knp_menu.factory',
'route_builder' => 'sonata.admin.route.path_info'.
(($manager_type == 'doctrine_phpcr') ? '_slashes' : ''),
'label_translator_strategy' => 'sonata.admin.label.strategy.native',
];
$definition->addMethodCall('setManagerType', [$manager_type]);
foreach ($defaultAddServices as $attr => $addServiceId) {
$method = 'set'.Inflector::classify($attr);
if (isset($overwriteAdminConfiguration[$attr]) || !$definition->hasMethodCall($method)) {
$args = [new Reference(isset($overwriteAdminConfiguration[$attr]) ? $overwriteAdminConfiguration[$attr] : $addServiceId)];
if ('translator' === $attr) {
$args[] = false;
}
$definition->addMethodCall($method, $args);
}
}
if (isset($overwriteAdminConfiguration['pager_type'])) {
$pagerType = $overwriteAdminConfiguration['pager_type'];
} elseif (isset($attributes['pager_type'])) {
$pagerType = $attributes['pager_type'];
} else {
$pagerType = Pager::TYPE_DEFAULT;
}
$definition->addMethodCall('setPagerType', [$pagerType]);
if (isset($overwriteAdminConfiguration['label'])) {
$label = $overwriteAdminConfiguration['label'];
} elseif (isset($attributes['label'])) {
$label = $attributes['label'];
} else {
$label = '-';
}
$definition->addMethodCall('setLabel', [$label]);
if (isset($attributes['persist_filters'])) {
$persistFilters = (bool) $attributes['persist_filters'];
} else {
$persistFilters = (bool) $container->getParameter('sonata.admin.configuration.filters.persist');
}
$definition->addMethodCall('setPersistFilters', [$persistFilters]);
if (isset($overwriteAdminConfiguration['show_mosaic_button'])) {
$showMosaicButton = $overwriteAdminConfiguration['show_mosaic_button'];
} elseif (isset($attributes['show_mosaic_button'])) {
$showMosaicButton = $attributes['show_mosaic_button'];
} else {
$showMosaicButton = $container->getParameter('sonata.admin.configuration.show.mosaic.button');
}
$definition->addMethodCall('showMosaicButton', [$showMosaicButton]);
$this->fixTemplates($container, $definition, isset($overwriteAdminConfiguration['templates']) ? $overwriteAdminConfiguration['templates'] : ['view' => []]);
if ($container->hasParameter('sonata.admin.configuration.security.information') && !$definition->hasMethodCall('setSecurityInformation')) {
$definition->addMethodCall('setSecurityInformation', ['%sonata.admin.configuration.security.information%']);
}
$definition->addMethodCall('initialize');
return $definition;
}
/**
* @param ContainerBuilder $container
* @param Definition $definition
* @param array $overwrittenTemplates
*/
public function fixTemplates(ContainerBuilder $container, Definition $definition, array $overwrittenTemplates = [])
{
$definedTemplates = $container->getParameter('sonata.admin.configuration.templates');
$methods = [];
$pos = 0;
foreach ($definition->getMethodCalls() as $method) {
if ($method[0] == 'setTemplates') {
$definedTemplates = array_merge($definedTemplates, $method[1][0]);
continue;
}
if ($method[0] == 'setTemplate') {
$definedTemplates[$method[1][0]] = $method[1][1];
continue;
}
// set template for simple pager if it is not already overwritten
if ($method[0] === 'setPagerType'
&& $method[1][0] === Pager::TYPE_SIMPLE
&& (
!isset($definedTemplates['pager_results'])
|| $definedTemplates['pager_results'] === 'SonataAdminBundle:Pager:results.html.twig'
)
) {
$definedTemplates['pager_results'] = 'SonataAdminBundle:Pager:simple_pager_results.html.twig';
}
$methods[$pos] = $method;
++$pos;
}
$definition->setMethodCalls($methods);
$definedTemplates = $overwrittenTemplates['view'] + $definedTemplates;
if ($container->getParameter('sonata.admin.configuration.templates') !== $definedTemplates) {
$definition->addMethodCall('setTemplates', [$definedTemplates]);
} else {
$definition->addMethodCall('setTemplates', ['%sonata.admin.configuration.templates%']);
}
}
/**
* Replace the empty arguments required by the Admin service definition.
*
* @param array $defaultArguments
* @param Definition $definition
* @param Definition|null $parentDefinition
*/
private function replaceDefaultArguments(array $defaultArguments, Definition $definition, Definition $parentDefinition = null)
{
$arguments = $definition->getArguments();
$parentArguments = $parentDefinition ? $parentDefinition->getArguments() : [];
foreach ($defaultArguments as $index => $value) {
$declaredInParent = $parentDefinition && array_key_exists($index, $parentArguments);
if (strlen($declaredInParent ? $parentArguments[$index] : $arguments[$index]) == 0) {
$arguments[$declaredInParent ? sprintf('index_%s', $index) : $index] = $value;
}
}
$definition->setArguments($arguments);
}
}

View File

@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class AddFilterTypeCompilerPass implements CompilerPassInterface
{
/**
* @param ContainerBuilder $container
*/
public function process(ContainerBuilder $container)
{
$definition = $container->getDefinition('sonata.admin.builder.filter.factory');
$types = [];
foreach ($container->findTaggedServiceIds('sonata.admin.filter.type') as $id => $attributes) {
$serviceDefinition = $container->getDefinition($id);
if (method_exists($definition, 'setShared')) { // Symfony 2.8+
$serviceDefinition->setShared(false);
} else { // For Symfony <2.8 compatibility
$serviceDefinition->setScope(ContainerInterface::SCOPE_PROTOTYPE);
}
$types[$serviceDefinition->getClass()] = $id;
// NEXT_MAJOR: Remove the alias when dropping support for symfony 2.x
foreach ($attributes as $eachTag) {
$types[$eachTag['alias']] = $id;
}
}
$definition->replaceArgument(1, $types);
}
}

View File

@@ -0,0 +1,244 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class ExtensionCompilerPass implements CompilerPassInterface
{
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
$universalExtensions = [];
$targets = [];
foreach ($container->findTaggedServiceIds('sonata.admin.extension') as $id => $tags) {
foreach ($tags as $attributes) {
$target = false;
if (isset($attributes['target'])) {
$target = $attributes['target'];
}
if (isset($attributes['global']) && $attributes['global']) {
$universalExtensions[$id] = $attributes;
}
if (!$target || !$container->hasDefinition($target)) {
continue;
}
$this->addExtension($targets, $target, $id, $attributes);
}
}
$extensionConfig = $container->getParameter('sonata.admin.extension.map');
$extensionMap = $this->flattenExtensionConfiguration($extensionConfig);
foreach ($container->findTaggedServiceIds('sonata.admin') as $id => $attributes) {
$admin = $container->getDefinition($id);
if (!isset($targets[$id])) {
$targets[$id] = new \SplPriorityQueue();
}
foreach ($universalExtensions as $extension => $extensionAttributes) {
$this->addExtension($targets, $id, $extension, $extensionAttributes);
}
$extensions = $this->getExtensionsForAdmin($id, $admin, $container, $extensionMap);
foreach ($extensions as $extension => $attributes) {
if (!$container->has($extension)) {
throw new \InvalidArgumentException(
sprintf('Unable to find extension service for id %s', $extension)
);
}
$this->addExtension($targets, $id, $extension, $attributes);
}
}
foreach ($targets as $target => $extensions) {
$extensions = iterator_to_array($extensions);
krsort($extensions);
$admin = $container->getDefinition($target);
foreach (array_values($extensions) as $extension) {
$admin->addMethodCall('addExtension', [$extension]);
}
}
}
/**
* @param string $id
* @param Definition $admin
* @param ContainerBuilder $container
* @param array $extensionMap
*
* @return array
*/
protected function getExtensionsForAdmin($id, Definition $admin, ContainerBuilder $container, array $extensionMap)
{
$extensions = [];
$classReflection = $subjectReflection = null;
$excludes = $extensionMap['excludes'];
unset($extensionMap['excludes']);
foreach ($extensionMap as $type => $subjects) {
foreach ($subjects as $subject => $extensionList) {
if ('admins' == $type) {
if ($id == $subject) {
$extensions = array_merge($extensions, $extensionList);
}
} else {
$class = $this->getManagedClass($admin, $container);
if (!class_exists($class)) {
continue;
}
$classReflection = new \ReflectionClass($class);
$subjectReflection = new \ReflectionClass($subject);
}
if ('instanceof' == $type) {
if ($subjectReflection->getName() == $classReflection->getName() || $classReflection->isSubclassOf($subject)) {
$extensions = array_merge($extensions, $extensionList);
}
}
if ('implements' == $type) {
if ($classReflection->implementsInterface($subject)) {
$extensions = array_merge($extensions, $extensionList);
}
}
if ('extends' == $type) {
if ($classReflection->isSubclassOf($subject)) {
$extensions = array_merge($extensions, $extensionList);
}
}
if ('uses' == $type) {
if ($this->hasTrait($classReflection, $subject)) {
$extensions = array_merge($extensions, $extensionList);
}
}
}
}
if (isset($excludes[$id])) {
$extensions = array_diff_key($extensions, $excludes[$id]);
}
return $extensions;
}
/**
* Resolves the class argument of the admin to an actual class (in case of %parameter%).
*
* @param Definition $admin
* @param ContainerBuilder $container
*
* @return string
*/
protected function getManagedClass(Definition $admin, ContainerBuilder $container)
{
return $container->getParameterBag()->resolveValue($admin->getArgument(1));
}
/**
* @param array $config
*
* @return array An array with the following structure.
*
* array(
* 'excludes' => array('<admin_id>' => array('<extension_id>' => array('priority' => <int>))),
* 'admins' => array('<admin_id>' => array('<extension_id>' => array('priority' => <int>))),
* 'implements' => array('<interface>' => array('<extension_id>' => array('priority' => <int>))),
* 'extends' => array('<class>' => array('<extension_id>' => array('priority' => <int>))),
* 'instanceof' => array('<class>' => array('<extension_id>' => array('priority' => <int>))),
* 'uses' => array('<trait>' => array('<extension_id>' => array('priority' => <int>))),
* )
*/
protected function flattenExtensionConfiguration(array $config)
{
$extensionMap = [
'excludes' => [],
'admins' => [],
'implements' => [],
'extends' => [],
'instanceof' => [],
'uses' => [],
];
foreach ($config as $extension => $options) {
$optionsMap = array_intersect_key($options, $extensionMap);
foreach ($optionsMap as $key => $value) {
foreach ($value as $source) {
if (!isset($extensionMap[$key][$source])) {
$extensionMap[$key][$source] = [];
}
$extensionMap[$key][$source][$extension]['priority'] = $options['priority'];
}
}
}
return $extensionMap;
}
/**
* @param \ReflectionClass $class
* @param $traitName
*
* @return bool
*/
protected function hasTrait(\ReflectionClass $class, $traitName)
{
if (in_array($traitName, $class->getTraitNames())) {
return true;
}
if (!$parentClass = $class->getParentClass()) {
return false;
}
return $this->hasTrait($parentClass, $traitName);
}
/**
* Add extension configuration to the targets array.
*
* @param array $targets
* @param string $target
* @param string $extension
* @param array $attributes
*/
private function addExtension(array &$targets, $target, $extension, array $attributes)
{
if (!isset($targets[$target])) {
$targets[$target] = new \SplPriorityQueue();
}
$priority = isset($attributes['priority']) ? $attributes['priority'] : 0;
$targets[$target]->insert(new Reference($extension), $priority);
}
}

View File

@@ -0,0 +1,31 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class GlobalVariablesCompilerPass implements CompilerPassInterface
{
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
$container->getDefinition('twig')
->addMethodCall('addGlobal', ['sonata_admin', new Reference('sonata.admin.twig.global')]);
}
}

View File

@@ -0,0 +1,454 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
/**
* This class contains the configuration information for the bundle.
*
* This information is solely responsible for how the different configuration
* sections are normalized, and merged.
*
* @author Michael Williams <mtotheikle@gmail.com>
*/
class Configuration implements ConfigurationInterface
{
/**
* Generates the configuration tree.
*
* @return TreeBuilder
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('sonata_admin', 'array');
$rootNode
->fixXmlConfig('option')
->fixXmlConfig('admin_service')
->fixXmlConfig('template')
->fixXmlConfig('extension')
->children()
->arrayNode('security')
->addDefaultsIfNotSet()
->fixXmlConfig('admin_permission')
->fixXmlConfig('object_permission')
->children()
->scalarNode('handler')->defaultValue('sonata.admin.security.handler.noop')->end()
->arrayNode('information')
->useAttributeAsKey('id')
->prototype('array')
->performNoDeepMerging()
->beforeNormalization()
->ifString()
->then(function ($v) {
return [$v];
})
->end()
->prototype('scalar')->end()
->end()
->end()
->arrayNode('admin_permissions')
->defaultValue(['CREATE', 'LIST', 'DELETE', 'UNDELETE', 'EXPORT', 'OPERATOR', 'MASTER'])
->prototype('scalar')->end()
->end()
->arrayNode('object_permissions')
->defaultValue(['VIEW', 'EDIT', 'DELETE', 'UNDELETE', 'OPERATOR', 'MASTER', 'OWNER'])
->prototype('scalar')->end()
->end()
->scalarNode('acl_user_manager')->defaultNull()->end()
->end()
->end()
->scalarNode('title')->defaultValue('Sonata Admin')->cannotBeEmpty()->end()
->scalarNode('title_logo')->defaultValue('bundles/sonataadmin/logo_title.png')->cannotBeEmpty()->end()
->arrayNode('global_search')
->addDefaultsIfNotSet()
->children()
->scalarNode('empty_boxes')
->defaultValue('show')
->info('Perhaps one of the three options: show, fade, hide.')
->validate()
->ifTrue(function ($v) {
return !in_array($v, ['show', 'fade', 'hide']);
})
->thenInvalid('Configuration value of "global_search.empty_boxes" must be one of show, fade or hide.')
->end()
->end()
->end()
->end()
->arrayNode('breadcrumbs')
->addDefaultsIfNotSet()
->children()
->scalarNode('child_admin_route')
->defaultValue('edit')
->info('Change the default route used to generate the link to the parent object, when in a child admin')
->end()
->end()
->end()
->arrayNode('options')
->addDefaultsIfNotSet()
->children()
->booleanNode('html5_validate')->defaultTrue()->end()
->booleanNode('sort_admins')->defaultFalse()->info('Auto order groups and admins by label or id')->end()
->booleanNode('confirm_exit')->defaultTrue()->end()
->booleanNode('use_select2')->defaultTrue()->end()
->booleanNode('use_icheck')->defaultTrue()->end()
->booleanNode('use_bootlint')->defaultFalse()->end()
->booleanNode('use_stickyforms')->defaultTrue()->end()
->integerNode('pager_links')->defaultNull()->end()
->scalarNode('form_type')->defaultValue('standard')->end()
->integerNode('dropdown_number_groups_per_colums')->defaultValue(2)->end()
->enumNode('title_mode')
->values(['single_text', 'single_image', 'both'])
->defaultValue('both')
->cannotBeEmpty()
->end()
->booleanNode('lock_protection')
->defaultFalse()
->info('Enable locking when editing an object, if the corresponding object manager supports it.')
->end()
->booleanNode('enable_jms_di_extra_autoregistration') // NEXT_MAJOR: remove this option
->defaultTrue()
->info('Enable automatic registration of annotations with JMSDiExtraBundle')
->end()
->end()
->end()
->arrayNode('dashboard')
->addDefaultsIfNotSet()
->fixXmlConfig('group')
->fixXmlConfig('block')
->children()
->arrayNode('groups')
->useAttributeAsKey('id')
->prototype('array')
->beforeNormalization()
->ifArray()
->then(function ($items) {
if (isset($items['provider'])) {
$disallowedItems = ['items', 'label'];
foreach ($disallowedItems as $item) {
if (isset($items[$item])) {
throw new \InvalidArgumentException(sprintf('The config value "%s" cannot be used alongside "provider" config value', $item));
}
}
}
return $items;
})
->end()
->fixXmlConfig('item')
->fixXmlConfig('item_add')
->children()
->scalarNode('label')->end()
->scalarNode('label_catalogue')->end()
->scalarNode('icon')->defaultValue('<i class="fa fa-folder"></i>')->end()
->scalarNode('on_top')->defaultFalse()->info('Show menu item in side dashboard menu without treeview')->end()
->scalarNode('keep_open')->defaultFalse()->info('Keep menu group always open')->end()
->scalarNode('provider')->end()
->arrayNode('items')
->beforeNormalization()
->ifArray()
->then(function ($items) {
foreach ($items as $key => $item) {
if (is_array($item)) {
if (!array_key_exists('label', $item) || !array_key_exists('route', $item)) {
throw new \InvalidArgumentException('Expected either parameters "route" and "label" for array items');
}
if (!array_key_exists('route_params', $item)) {
$items[$key]['route_params'] = [];
}
$items[$key]['admin'] = '';
} else {
$items[$key] = [
'admin' => $item,
'label' => '',
'route' => '',
'route_params' => [],
'route_absolute' => false,
];
}
}
return $items;
})
->end()
->prototype('array')
->children()
->scalarNode('admin')->end()
->scalarNode('label')->end()
->scalarNode('route')->end()
->arrayNode('roles')
->prototype('scalar')
->info('Roles which will see the route in the menu')
->defaultValue([])
->end()
->end()
->arrayNode('route_params')
->prototype('scalar')->end()
->end()
->booleanNode('route_absolute')
->info('Whether the generated url should be absolute')
->defaultFalse()
->end()
->end()
->end()
->end()
->arrayNode('item_adds')
->prototype('scalar')->end()
->end()
->arrayNode('roles')
->prototype('scalar')->defaultValue([])->end()
->end()
->end()
->end()
->end()
->arrayNode('blocks')
->defaultValue([[
'position' => 'left',
'settings' => [],
'type' => 'sonata.admin.block.admin_list',
'roles' => [],
]])
->prototype('array')
->fixXmlConfig('setting')
->children()
->scalarNode('type')->cannotBeEmpty()->end()
->arrayNode('roles')
->defaultValue([])
->prototype('scalar')->end()
->end()
->arrayNode('settings')
->useAttributeAsKey('id')
->prototype('variable')->defaultValue([])->end()
->end()
->scalarNode('position')->defaultValue('right')->end()
->scalarNode('class')->defaultValue('col-md-4')->end()
->end()
->end()
->end()
->end()
->end()
->arrayNode('admin_services')
->prototype('array')
->children()
->scalarNode('model_manager')->defaultNull()->end()
->scalarNode('form_contractor')->defaultNull()->end()
->scalarNode('show_builder')->defaultNull()->end()
->scalarNode('list_builder')->defaultNull()->end()
->scalarNode('datagrid_builder')->defaultNull()->end()
->scalarNode('translator')->defaultNull()->end()
->scalarNode('configuration_pool')->defaultNull()->end()
->scalarNode('route_generator')->defaultNull()->end()
->scalarNode('validator')->defaultNull()->end()
->scalarNode('security_handler')->defaultNull()->end()
->scalarNode('label')->defaultNull()->end()
->scalarNode('menu_factory')->defaultNull()->end()
->scalarNode('route_builder')->defaultNull()->end()
->scalarNode('label_translator_strategy')->defaultNull()->end()
->scalarNode('pager_type')->defaultNull()->end()
->arrayNode('templates')
->addDefaultsIfNotSet()
->children()
->arrayNode('form')
->prototype('scalar')->end()
->end()
->arrayNode('filter')
->prototype('scalar')->end()
->end()
->arrayNode('view')
->useAttributeAsKey('id')
->prototype('scalar')->end()
->end()
->end()
->end()
->end()
->end()
->end()
->arrayNode('templates')
->addDefaultsIfNotSet()
->children()
->scalarNode('user_block')->defaultValue('SonataAdminBundle:Core:user_block.html.twig')->cannotBeEmpty()->end()
->scalarNode('add_block')->defaultValue('SonataAdminBundle:Core:add_block.html.twig')->cannotBeEmpty()->end()
->scalarNode('layout')->defaultValue('SonataAdminBundle::standard_layout.html.twig')->cannotBeEmpty()->end()
->scalarNode('ajax')->defaultValue('SonataAdminBundle::ajax_layout.html.twig')->cannotBeEmpty()->end()
->scalarNode('dashboard')->defaultValue('SonataAdminBundle:Core:dashboard.html.twig')->cannotBeEmpty()->end()
->scalarNode('search')->defaultValue('SonataAdminBundle:Core:search.html.twig')->cannotBeEmpty()->end()
->scalarNode('list')->defaultValue('SonataAdminBundle:CRUD:list.html.twig')->cannotBeEmpty()->end()
->scalarNode('filter')->defaultValue('SonataAdminBundle:Form:filter_admin_fields.html.twig')->cannotBeEmpty()->end()
->scalarNode('show')->defaultValue('SonataAdminBundle:CRUD:show.html.twig')->cannotBeEmpty()->end()
->scalarNode('show_compare')->defaultValue('SonataAdminBundle:CRUD:show_compare.html.twig')->cannotBeEmpty()->end()
->scalarNode('edit')->defaultValue('SonataAdminBundle:CRUD:edit.html.twig')->cannotBeEmpty()->end()
->scalarNode('preview')->defaultValue('SonataAdminBundle:CRUD:preview.html.twig')->cannotBeEmpty()->end()
->scalarNode('history')->defaultValue('SonataAdminBundle:CRUD:history.html.twig')->cannotBeEmpty()->end()
->scalarNode('acl')->defaultValue('SonataAdminBundle:CRUD:acl.html.twig')->cannotBeEmpty()->end()
->scalarNode('history_revision_timestamp')->defaultValue('SonataAdminBundle:CRUD:history_revision_timestamp.html.twig')->cannotBeEmpty()->end()
->scalarNode('action')->defaultValue('SonataAdminBundle:CRUD:action.html.twig')->cannotBeEmpty()->end()
->scalarNode('select')->defaultValue('SonataAdminBundle:CRUD:list__select.html.twig')->cannotBeEmpty()->end()
->scalarNode('list_block')->defaultValue('SonataAdminBundle:Block:block_admin_list.html.twig')->cannotBeEmpty()->end()
->scalarNode('search_result_block')->defaultValue('SonataAdminBundle:Block:block_search_result.html.twig')->cannotBeEmpty()->end()
->scalarNode('short_object_description')->defaultValue('SonataAdminBundle:Helper:short-object-description.html.twig')->cannotBeEmpty()->end()
->scalarNode('delete')->defaultValue('SonataAdminBundle:CRUD:delete.html.twig')->cannotBeEmpty()->end()
->scalarNode('batch')->defaultValue('SonataAdminBundle:CRUD:list__batch.html.twig')->cannotBeEmpty()->end()
->scalarNode('batch_confirmation')->defaultValue('SonataAdminBundle:CRUD:batch_confirmation.html.twig')->cannotBeEmpty()->end()
->scalarNode('inner_list_row')->defaultValue('SonataAdminBundle:CRUD:list_inner_row.html.twig')->cannotBeEmpty()->end()
->scalarNode('outer_list_rows_mosaic')->defaultValue('SonataAdminBundle:CRUD:list_outer_rows_mosaic.html.twig')->cannotBeEmpty()->end()
->scalarNode('outer_list_rows_list')->defaultValue('SonataAdminBundle:CRUD:list_outer_rows_list.html.twig')->cannotBeEmpty()->end()
->scalarNode('outer_list_rows_tree')->defaultValue('SonataAdminBundle:CRUD:list_outer_rows_tree.html.twig')->cannotBeEmpty()->end()
->scalarNode('base_list_field')->defaultValue('SonataAdminBundle:CRUD:base_list_field.html.twig')->cannotBeEmpty()->end()
->scalarNode('pager_links')->defaultValue('SonataAdminBundle:Pager:links.html.twig')->cannotBeEmpty()->end()
->scalarNode('pager_results')->defaultValue('SonataAdminBundle:Pager:results.html.twig')->cannotBeEmpty()->end()
->scalarNode('tab_menu_template')->defaultValue('SonataAdminBundle:Core:tab_menu_template.html.twig')->cannotBeEmpty()->end()
->scalarNode('knp_menu_template')->defaultValue('SonataAdminBundle:Menu:sonata_menu.html.twig')->cannotBeEmpty()->end()
->scalarNode('action_create')->defaultValue('SonataAdminBundle:CRUD:dashboard__action_create.html.twig')->cannotBeEmpty()->end()
->scalarNode('button_acl')->defaultValue('SonataAdminBundle:Button:acl_button.html.twig')->cannotBeEmpty()->end()
->scalarNode('button_create')->defaultValue('SonataAdminBundle:Button:create_button.html.twig')->cannotBeEmpty()->end()
->scalarNode('button_edit')->defaultValue('SonataAdminBundle:Button:edit_button.html.twig')->cannotBeEmpty()->end()
->scalarNode('button_history')->defaultValue('SonataAdminBundle:Button:history_button.html.twig')->cannotBeEmpty()->end()
->scalarNode('button_list')->defaultValue('SonataAdminBundle:Button:list_button.html.twig')->cannotBeEmpty()->end()
->scalarNode('button_show')->defaultValue('SonataAdminBundle:Button:show_button.html.twig')->cannotBeEmpty()->end()
->end()
->end()
->arrayNode('assets')
->addDefaultsIfNotSet()
->children()
->arrayNode('stylesheets')
->defaultValue([
'bundles/sonatacore/vendor/bootstrap/dist/css/bootstrap.min.css',
'bundles/sonatacore/vendor/components-font-awesome/css/font-awesome.min.css',
'bundles/sonatacore/vendor/ionicons/css/ionicons.min.css',
'bundles/sonataadmin/vendor/admin-lte/dist/css/AdminLTE.min.css',
'bundles/sonataadmin/vendor/admin-lte/dist/css/skins/skin-black.min.css',
'bundles/sonataadmin/vendor/iCheck/skins/square/blue.css',
'bundles/sonatacore/vendor/eonasdan-bootstrap-datetimepicker/build/css/bootstrap-datetimepicker.min.css',
'bundles/sonataadmin/vendor/jqueryui/themes/base/jquery-ui.css',
'bundles/sonatacore/vendor/select2/select2.css',
'bundles/sonatacore/vendor/select2-bootstrap-css/select2-bootstrap.min.css',
'bundles/sonataadmin/vendor/x-editable/dist/bootstrap3-editable/css/bootstrap-editable.css',
'bundles/sonataadmin/css/styles.css',
'bundles/sonataadmin/css/layout.css',
'bundles/sonataadmin/css/tree.css',
])
->prototype('scalar')->end()
->end()
->arrayNode('javascripts')
->defaultValue([
'bundles/sonatacore/vendor/jquery/dist/jquery.min.js',
'bundles/sonataadmin/vendor/jquery.scrollTo/jquery.scrollTo.min.js',
'bundles/sonatacore/vendor/moment/min/moment.min.js',
'bundles/sonataadmin/vendor/jqueryui/ui/minified/jquery-ui.min.js',
'bundles/sonataadmin/vendor/jqueryui/ui/minified/i18n/jquery-ui-i18n.min.js',
'bundles/sonatacore/vendor/bootstrap/dist/js/bootstrap.min.js',
'bundles/sonatacore/vendor/eonasdan-bootstrap-datetimepicker/build/js/bootstrap-datetimepicker.min.js',
'bundles/sonataadmin/vendor/jquery-form/jquery.form.js',
'bundles/sonataadmin/jquery/jquery.confirmExit.js',
'bundles/sonataadmin/vendor/x-editable/dist/bootstrap3-editable/js/bootstrap-editable.min.js',
'bundles/sonatacore/vendor/select2/select2.min.js',
'bundles/sonataadmin/vendor/admin-lte/dist/js/app.min.js',
'bundles/sonataadmin/vendor/iCheck/icheck.min.js',
'bundles/sonataadmin/vendor/slimScroll/jquery.slimscroll.min.js',
'bundles/sonataadmin/vendor/waypoints/lib/jquery.waypoints.min.js',
'bundles/sonataadmin/vendor/waypoints/lib/shortcuts/sticky.min.js',
'bundles/sonataadmin/vendor/readmore-js/readmore.min.js',
'bundles/sonataadmin/vendor/masonry/dist/masonry.pkgd.min.js',
'bundles/sonataadmin/Admin.js',
'bundles/sonataadmin/treeview.js',
])
->prototype('scalar')->end()
->end()
->end()
->end()
->arrayNode('extensions')
->useAttributeAsKey('id')
->defaultValue(['admins' => [], 'excludes' => [], 'implements' => [], 'extends' => [], 'instanceof' => [], 'uses' => []])
->prototype('array')
->fixXmlConfig('admin')
->fixXmlConfig('exclude')
->fixXmlConfig('implement')
->fixXmlConfig('extend')
->fixXmlConfig('use')
->children()
->arrayNode('admins')
->prototype('scalar')->end()
->end()
->arrayNode('excludes')
->prototype('scalar')->end()
->end()
->arrayNode('implements')
->prototype('scalar')->end()
->end()
->arrayNode('extends')
->prototype('scalar')->end()
->end()
->arrayNode('instanceof')
->prototype('scalar')->end()
->end()
->arrayNode('uses')
->prototype('scalar')->end()
->validate()
->ifTrue(function ($v) {
return !empty($v) && version_compare(PHP_VERSION, '5.4.0', '<');
})
->thenInvalid('PHP >= 5.4.0 is required to use traits.')
->end()
->end()
->integerNode('priority')
->info('Positive or negative integer. The higher the priority, the earlier its executed.')
->defaultValue(0)
->end()
->end()
->end()
->end()
->scalarNode('persist_filters')->defaultFalse()->end()
->booleanNode('show_mosaic_button')
->defaultTrue()
->info('Show mosaic button on all admin screens')
->end()
// NEXT_MAJOR : remove this option
->booleanNode('translate_group_label')
->defaultFalse()
->info('Translate group label')
->end()
->end()
->end();
return $treeBuilder;
}
}

View File

@@ -0,0 +1,423 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
* @author Michael Williams <michael.williams@funsational.com>
*/
class SonataAdminExtension extends Extension implements PrependExtensionInterface
{
/**
* @param array $configs An array of configuration settings
* @param ContainerBuilder $container A ContainerBuilder instance
*/
public function load(array $configs, ContainerBuilder $container)
{
$bundles = $container->getParameter('kernel.bundles');
if (isset($bundles['SonataUserBundle'])) {
// integrate the SonataUserBundle / FOSUserBundle if the bundle exists
array_unshift($configs, [
'templates' => [
'user_block' => 'SonataUserBundle:Admin/Core:user_block.html.twig',
],
]);
}
if (isset($bundles['SonataIntlBundle'])) {
// integrate the SonataUserBundle if the bundle exists
array_unshift($configs, [
'templates' => [
'history_revision_timestamp' => 'SonataIntlBundle:CRUD:history_revision_timestamp.html.twig',
],
]);
}
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('templates.xml');
$loader->load('twig.xml');
$loader->load('core.xml');
$loader->load('form_types.xml');
$loader->load('validator.xml');
$loader->load('route.xml');
$loader->load('block.xml');
$loader->load('menu.xml');
if (isset($bundles['SonataExporterBundle'])) {
$loader->load('exporter.xml');
}
// NEXT_MAJOR : remove this block
if (method_exists('Symfony\Component\DependencyInjection\Definition', 'setDeprecated')) {
$container->getDefinition('sonata.admin.exporter')->setDeprecated(
'The service "%service_id%" is deprecated in favor of the "sonata.exporter.exporter" service'
);
}
// TODO: Go back on xml configuration when bumping requirements to SF 2.6+
$sidebarMenu = $container->getDefinition('sonata.admin.sidebar_menu');
if (method_exists($sidebarMenu, 'setFactory')) {
$sidebarMenu->setFactory([new Reference('sonata.admin.menu_builder'), 'createSidebarMenu']);
} else {
$sidebarMenu->setFactoryService('sonata.admin.menu_builder');
$sidebarMenu->setFactoryMethod('createSidebarMenu');
}
$configuration = $this->getConfiguration($configs, $container);
$config = $this->processConfiguration($configuration, $configs);
$config['options']['javascripts'] = $config['assets']['javascripts'];
$config['options']['stylesheets'] = $config['assets']['stylesheets'];
$pool = $container->getDefinition('sonata.admin.pool');
$pool->replaceArgument(1, $config['title']);
$pool->replaceArgument(2, $config['title_logo']);
$pool->replaceArgument(3, $config['options']);
if (false === $config['options']['lock_protection']) {
$container->removeDefinition('sonata.admin.lock.extension');
}
$container->setParameter('sonata.admin.configuration.global_search.empty_boxes', $config['global_search']['empty_boxes']);
$container->setParameter('sonata.admin.configuration.templates', $config['templates'] + [
'user_block' => 'SonataAdminBundle:Core:user_block.html.twig',
'add_block' => 'SonataAdminBundle:Core:add_block.html.twig',
'layout' => 'SonataAdminBundle::standard_layout.html.twig',
'ajax' => 'SonataAdminBundle::ajax_layout.html.twig',
'dashboard' => 'SonataAdminBundle:Core:dashboard.html.twig',
'list' => 'SonataAdminBundle:CRUD:list.html.twig',
'filter' => 'SonataAdminBundle:Form:filter_admin_fields.html.twig',
'show' => 'SonataAdminBundle:CRUD:show.html.twig',
'show_compare' => 'SonataAdminBundle:CRUD:show_compare.html.twig',
'edit' => 'SonataAdminBundle:CRUD:edit.html.twig',
'history' => 'SonataAdminBundle:CRUD:history.html.twig',
'history_revision_timestamp' => 'SonataAdminBundle:CRUD:history_revision_timestamp.html.twig',
'acl' => 'SonataAdminBundle:CRUD:acl.html.twig',
'action' => 'SonataAdminBundle:CRUD:action.html.twig',
'short_object_description' => 'SonataAdminBundle:Helper:short-object-description.html.twig',
'preview' => 'SonataAdminBundle:CRUD:preview.html.twig',
'list_block' => 'SonataAdminBundle:Block:block_admin_list.html.twig',
'delete' => 'SonataAdminBundle:CRUD:delete.html.twig',
'batch' => 'SonataAdminBundle:CRUD:list__batch.html.twig',
'select' => 'SonataAdminBundle:CRUD:list__select.html.twig',
'batch_confirmation' => 'SonataAdminBundle:CRUD:batch_confirmation.html.twig',
'inner_list_row' => 'SonataAdminBundle:CRUD:list_inner_row.html.twig',
'base_list_field' => 'SonataAdminBundle:CRUD:base_list_field.html.twig',
'pager_links' => 'SonataAdminBundle:Pager:links.html.twig',
'pager_results' => 'SonataAdminBundle:Pager:results.html.twig',
'tab_menu_template' => 'SonataAdminBundle:Core:tab_menu_template.html.twig',
'knp_menu_template' => 'SonataAdminBundle:Menu:sonata_menu.html.twig',
'outer_list_rows_mosaic' => 'SonataAdminBundle:CRUD:list_outer_rows_mosaic.html.twig',
'outer_list_rows_list' => 'SonataAdminBundle:CRUD:list_outer_rows_list.html.twig',
'outer_list_rows_tree' => 'SonataAdminBundle:CRUD:list_outer_rows_tree.html.twig',
]);
$container->setParameter('sonata.admin.configuration.admin_services', $config['admin_services']);
$container->setParameter('sonata.admin.configuration.dashboard_groups', $config['dashboard']['groups']);
$container->setParameter('sonata.admin.configuration.dashboard_blocks', $config['dashboard']['blocks']);
$container->setParameter('sonata.admin.configuration.sort_admins', $config['options']['sort_admins']);
$container->setParameter('sonata.admin.configuration.breadcrumbs', $config['breadcrumbs']);
if (null === $config['security']['acl_user_manager'] && isset($bundles['FOSUserBundle'])) {
$container->setParameter('sonata.admin.security.acl_user_manager', 'fos_user.user_manager');
} else {
$container->setParameter('sonata.admin.security.acl_user_manager', $config['security']['acl_user_manager']);
}
$container->setAlias('sonata.admin.security.handler', $config['security']['handler']);
switch ($config['security']['handler']) {
case 'sonata.admin.security.handler.role':
if (count($config['security']['information']) === 0) {
$config['security']['information'] = [
'EDIT' => ['EDIT'],
'LIST' => ['LIST'],
'CREATE' => ['CREATE'],
'VIEW' => ['VIEW'],
'DELETE' => ['DELETE'],
'EXPORT' => ['EXPORT'],
'ALL' => ['ALL'],
];
}
break;
case 'sonata.admin.security.handler.acl':
if (count($config['security']['information']) === 0) {
$config['security']['information'] = [
'GUEST' => ['VIEW', 'LIST'],
'STAFF' => ['EDIT', 'LIST', 'CREATE'],
'EDITOR' => ['OPERATOR', 'EXPORT'],
'ADMIN' => ['MASTER'],
];
}
break;
}
$container->setParameter('sonata.admin.configuration.security.information', $config['security']['information']);
$container->setParameter('sonata.admin.configuration.security.admin_permissions', $config['security']['admin_permissions']);
$container->setParameter('sonata.admin.configuration.security.object_permissions', $config['security']['object_permissions']);
$loader->load('security.xml');
// Set the SecurityContext for Symfony <2.6
// NEXT_MAJOR: Go back to simple xml configuration when bumping requirements to SF 2.6+
if (interface_exists('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface')) {
$tokenStorageReference = new Reference('security.token_storage');
$authorizationCheckerReference = new Reference('security.authorization_checker');
} else {
$tokenStorageReference = new Reference('security.context');
$authorizationCheckerReference = new Reference('security.context');
}
$container
->getDefinition('sonata.admin.security.handler.role')
->replaceArgument(0, $authorizationCheckerReference)
;
$container
->getDefinition('sonata.admin.security.handler.acl')
->replaceArgument(0, $tokenStorageReference)
->replaceArgument(1, $authorizationCheckerReference)
;
$container
->getDefinition('sonata.admin.menu.group_provider')
->replaceArgument(2, $authorizationCheckerReference)
;
$container->setParameter('sonata.admin.extension.map', $config['extensions']);
/*
* This is a work in progress, so for now it is hardcoded
*/
$classes = [
'email' => '',
'textarea' => '',
'text' => '',
'choice' => '',
'integer' => '',
'datetime' => 'sonata-medium-date',
'date' => 'sonata-medium-date',
// SF3+
'Symfony\Component\Form\Extension\Core\Type\ChoiceType' => '',
'Symfony\Component\Form\Extension\Core\Type\DateType' => 'sonata-medium-date',
'Symfony\Component\Form\Extension\Core\Type\DateTimeType' => 'sonata-medium-date',
'Symfony\Component\Form\Extension\Core\Type\EmailType' => '',
'Symfony\Component\Form\Extension\Core\Type\IntegerType' => '',
'Symfony\Component\Form\Extension\Core\Type\TextareaType' => '',
'Symfony\Component\Form\Extension\Core\Type\TextType' => '',
];
$container->getDefinition('sonata.admin.form.extension.field')
->replaceArgument(0, $classes)
->replaceArgument(1, $config['options']);
// remove non used service
if (!isset($bundles['JMSTranslationBundle'])) {
$container->removeDefinition('sonata.admin.translator.extractor.jms_translator_bundle');
}
//remove non-Mopa compatibility layer
if (isset($bundles['MopaBootstrapBundle'])) {
$container->removeDefinition('sonata.admin.form.extension.field.mopa');
}
// set filter persistence
$container->setParameter('sonata.admin.configuration.filters.persist', $config['persist_filters']);
$container->setParameter('sonata.admin.configuration.show.mosaic.button', $config['show_mosaic_button']);
$container->setParameter('sonata.admin.configuration.translate_group_label', $config['translate_group_label']);
if (\PHP_VERSION_ID < 70000) {
$this->configureClassesToCompile();
}
$this->replacePropertyAccessor($container);
}
/**
* Allow an extension to prepend the extension configurations.
*
* NEXT_MAJOR: remove all code that deals with JMSDiExtraBundle
*
* @param ContainerBuilder $container
*/
public function prepend(ContainerBuilder $container)
{
$bundles = $container->getParameter('kernel.bundles');
if (!isset($bundles['JMSDiExtraBundle'])) {
return;
}
$configs = $container->getExtensionConfig($this->getAlias());
$config = $this->processConfiguration(new Configuration(), $configs);
if (!$config['options']['enable_jms_di_extra_autoregistration']) {
return;
}
$sonataAdminPattern = 'Sonata\AdminBundle\Annotation';
$annotationPatternsConfigured = false;
$diExtraConfigs = $container->getExtensionConfig('jms_di_extra');
foreach ($diExtraConfigs as $diExtraConfig) {
if (isset($diExtraConfig['annotation_patterns'])) {
// don't add our own pattern if user has already done so
if (array_search($sonataAdminPattern, $diExtraConfig['annotation_patterns']) !== false) {
return;
}
$annotationPatternsConfigured = true;
break;
}
}
@trigger_error(
'Automatic registration of annotations is deprecated since 3.14, to be removed in 4.0.',
E_USER_DEPRECATED
);
if ($annotationPatternsConfigured) {
$annotationPatterns = [$sonataAdminPattern];
} else {
// get annotation_patterns default from DiExtraBundle configuration
$diExtraConfigDefinition = new \JMS\DiExtraBundle\DependencyInjection\Configuration();
// FIXME: this will break if DiExtraBundle adds any mandatory configuration
$diExtraConfig = $this->processConfiguration($diExtraConfigDefinition, []);
$annotationPatterns = $diExtraConfig['annotation_patterns'];
$annotationPatterns[] = $sonataAdminPattern;
}
$container->prependExtensionConfig(
'jms_di_extra',
[
'annotation_patterns' => $annotationPatterns,
]
);
}
public function configureClassesToCompile()
{
$this->addClassesToCompile([
'Sonata\\AdminBundle\\Admin\\AbstractAdmin',
'Sonata\\AdminBundle\\Admin\\AbstractAdminExtension',
'Sonata\\AdminBundle\\Admin\\AdminExtensionInterface',
'Sonata\\AdminBundle\\Admin\\AdminHelper',
'Sonata\\AdminBundle\\Admin\\AdminInterface',
'Sonata\\AdminBundle\\Admin\\BaseFieldDescription',
'Sonata\\AdminBundle\\Admin\\FieldDescriptionCollection',
'Sonata\\AdminBundle\\Admin\\FieldDescriptionInterface',
'Sonata\\AdminBundle\\Admin\\Pool',
'Sonata\\AdminBundle\\Block\\AdminListBlockService',
'Sonata\\AdminBundle\\Builder\\DatagridBuilderInterface',
'Sonata\\AdminBundle\\Builder\\FormContractorInterface',
'Sonata\\AdminBundle\\Builder\\ListBuilderInterface',
'Sonata\\AdminBundle\\Builder\\RouteBuilderInterface',
'Sonata\\AdminBundle\\Builder\\ShowBuilderInterface',
'Sonata\\AdminBundle\\Datagrid\\Datagrid',
'Sonata\\AdminBundle\\Datagrid\\DatagridInterface',
'Sonata\\AdminBundle\\Datagrid\\DatagridMapper',
'Sonata\\AdminBundle\\Datagrid\\ListMapper',
'Sonata\\AdminBundle\\Datagrid\\Pager',
'Sonata\\AdminBundle\\Datagrid\\PagerInterface',
'Sonata\\AdminBundle\\Datagrid\\ProxyQueryInterface',
'Sonata\\AdminBundle\\Exception\\ModelManagerException',
'Sonata\\AdminBundle\\Exception\\NoValueException',
'Sonata\\AdminBundle\\Filter\\Filter',
'Sonata\\AdminBundle\\Filter\\FilterFactory',
'Sonata\\AdminBundle\\Filter\\FilterFactoryInterface',
'Sonata\\AdminBundle\\Filter\\FilterInterface',
'Sonata\\AdminBundle\\Form\\DataTransformer\\ArrayToModelTransformer',
'Sonata\\AdminBundle\\Form\\DataTransformer\\ModelsToArrayTransformer',
'Sonata\\AdminBundle\\Form\\DataTransformer\\ModelToIdTransformer',
'Sonata\\AdminBundle\\Form\\EventListener\\MergeCollectionListener',
'Sonata\\AdminBundle\\Form\\Extension\\Field\\Type\\FormTypeFieldExtension',
'Sonata\\AdminBundle\\Form\\FormMapper',
'Sonata\\AdminBundle\\Form\\Type\\AdminType',
'Sonata\\AdminBundle\\Form\\Type\\Filter\\ChoiceType',
'Sonata\\AdminBundle\\Form\\Type\\Filter\\DateRangeType',
'Sonata\\AdminBundle\\Form\\Type\\Filter\\DateTimeRangeType',
'Sonata\\AdminBundle\\Form\\Type\\Filter\\DateTimeType',
'Sonata\\AdminBundle\\Form\\Type\\Filter\\DateType',
'Sonata\\AdminBundle\\Form\\Type\\Filter\\DefaultType',
'Sonata\\AdminBundle\\Form\\Type\\Filter\\NumberType',
'Sonata\\AdminBundle\\Form\\Type\\ModelReferenceType',
'Sonata\\AdminBundle\\Form\\Type\\ModelType',
'Sonata\\AdminBundle\\Form\\Type\\ModelListType',
'Sonata\\AdminBundle\\Guesser\\TypeGuesserChain',
'Sonata\\AdminBundle\\Guesser\\TypeGuesserInterface',
'Sonata\\AdminBundle\\Model\\AuditManager',
'Sonata\\AdminBundle\\Model\\AuditManagerInterface',
'Sonata\\AdminBundle\\Model\\AuditReaderInterface',
'Sonata\\AdminBundle\\Model\\ModelManagerInterface',
'Sonata\\AdminBundle\\Route\\AdminPoolLoader',
'Sonata\\AdminBundle\\Route\\DefaultRouteGenerator',
'Sonata\\AdminBundle\\Route\\PathInfoBuilder',
'Sonata\\AdminBundle\\Route\\QueryStringBuilder',
'Sonata\\AdminBundle\\Route\\RouteCollection',
'Sonata\\AdminBundle\\Route\\RouteGeneratorInterface',
'Sonata\\AdminBundle\\Security\\Acl\\Permission\\AdminPermissionMap',
'Sonata\\AdminBundle\\Security\\Acl\\Permission\\MaskBuilder',
'Sonata\\AdminBundle\\Security\\Handler\\AclSecurityHandler',
'Sonata\\AdminBundle\\Security\\Handler\\AclSecurityHandlerInterface',
'Sonata\\AdminBundle\\Security\\Handler\\NoopSecurityHandler',
'Sonata\\AdminBundle\\Security\\Handler\\RoleSecurityHandler',
'Sonata\\AdminBundle\\Security\\Handler\\SecurityHandlerInterface',
'Sonata\\AdminBundle\\Show\\ShowMapper',
'Sonata\\AdminBundle\\Translator\\BCLabelTranslatorStrategy',
'Sonata\\AdminBundle\\Translator\\FormLabelTranslatorStrategy',
'Sonata\\AdminBundle\\Translator\\LabelTranslatorStrategyInterface',
'Sonata\\AdminBundle\\Translator\\NativeLabelTranslatorStrategy',
'Sonata\\AdminBundle\\Translator\\NoopLabelTranslatorStrategy',
'Sonata\\AdminBundle\\Translator\\UnderscoreLabelTranslatorStrategy',
'Sonata\\AdminBundle\\Twig\\Extension\\SonataAdminExtension',
'Sonata\\AdminBundle\\Util\\AdminAclManipulator',
'Sonata\\AdminBundle\\Util\\AdminAclManipulatorInterface',
'Sonata\\AdminBundle\\Util\\FormBuilderIterator',
'Sonata\\AdminBundle\\Util\\FormViewIterator',
'Sonata\\AdminBundle\\Util\\ObjectAclManipulator',
'Sonata\\AdminBundle\\Util\\ObjectAclManipulatorInterface',
]);
}
/**
* {@inheritdoc}
*/
public function getNamespace()
{
return 'https://sonata-project.org/schema/dic/admin';
}
private function replacePropertyAccessor(ContainerBuilder $container)
{
if (!$container->has('form.property_accessor')) {
return;
}
$pool = $container->getDefinition('sonata.admin.pool');
$pool->replaceArgument(4, new Reference('form.property_accessor'));
$modelChoice = $container->getDefinition('sonata.admin.form.type.model_choice');
$modelChoice->replaceArgument(0, new Reference('form.property_accessor'));
}
}

View File

@@ -0,0 +1,125 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Event;
use Sonata\AdminBundle\Admin\AbstractAdminExtension;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class AdminEventExtension extends AbstractAdminExtension
{
protected $eventDispatcher;
/**
* @param EventDispatcherInterface $eventDispatcher
*/
public function __construct(EventDispatcherInterface $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
}
/**
* {@inheritdoc}
*/
public function configureFormFields(FormMapper $form)
{
$this->eventDispatcher->dispatch('sonata.admin.event.configure.form', new ConfigureEvent($form->getAdmin(), $form, ConfigureEvent::TYPE_FORM));
}
/**
* {@inheritdoc}
*/
public function configureListFields(ListMapper $list)
{
$this->eventDispatcher->dispatch('sonata.admin.event.configure.list', new ConfigureEvent($list->getAdmin(), $list, ConfigureEvent::TYPE_LIST));
}
/**
* {@inheritdoc}
*/
public function configureDatagridFilters(DatagridMapper $filter)
{
$this->eventDispatcher->dispatch('sonata.admin.event.configure.datagrid', new ConfigureEvent($filter->getAdmin(), $filter, ConfigureEvent::TYPE_DATAGRID));
}
/**
* {@inheritdoc}
*/
public function configureShowFields(ShowMapper $show)
{
$this->eventDispatcher->dispatch('sonata.admin.event.configure.show', new ConfigureEvent($show->getAdmin(), $show, ConfigureEvent::TYPE_SHOW));
}
/**
* {@inheritdoc}
*/
public function configureQuery(AdminInterface $admin, ProxyQueryInterface $query, $context = 'list')
{
$this->eventDispatcher->dispatch('sonata.admin.event.configure.query', new ConfigureQueryEvent($admin, $query, $context));
}
/**
* {@inheritdoc}
*/
public function preUpdate(AdminInterface $admin, $object)
{
$this->eventDispatcher->dispatch('sonata.admin.event.persistence.pre_update', new PersistenceEvent($admin, $object, PersistenceEvent::TYPE_PRE_UPDATE));
}
/**
* {@inheritdoc}
*/
public function postUpdate(AdminInterface $admin, $object)
{
$this->eventDispatcher->dispatch('sonata.admin.event.persistence.post_update', new PersistenceEvent($admin, $object, PersistenceEvent::TYPE_POST_UPDATE));
}
/**
* {@inheritdoc}
*/
public function prePersist(AdminInterface $admin, $object)
{
$this->eventDispatcher->dispatch('sonata.admin.event.persistence.pre_persist', new PersistenceEvent($admin, $object, PersistenceEvent::TYPE_PRE_PERSIST));
}
/**
* {@inheritdoc}
*/
public function postPersist(AdminInterface $admin, $object)
{
$this->eventDispatcher->dispatch('sonata.admin.event.persistence.post_persist', new PersistenceEvent($admin, $object, PersistenceEvent::TYPE_POST_PERSIST));
}
/**
* {@inheritdoc}
*/
public function preRemove(AdminInterface $admin, $object)
{
$this->eventDispatcher->dispatch('sonata.admin.event.persistence.pre_remove', new PersistenceEvent($admin, $object, PersistenceEvent::TYPE_PRE_REMOVE));
}
/**
* {@inheritdoc}
*/
public function postRemove(AdminInterface $admin, $object)
{
$this->eventDispatcher->dispatch('sonata.admin.event.persistence.post_remove', new PersistenceEvent($admin, $object, PersistenceEvent::TYPE_POST_REMOVE));
}
}

View File

@@ -0,0 +1,88 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Event;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Mapper\BaseMapper;
use Symfony\Component\EventDispatcher\Event;
/**
* This event is sent by hook:
* - configureFormFields
* - configureListFields
* - configureDatagridFilters
* - configureShowFields.
*
* You can register the listener to the event dispatcher by using:
* - sonata.admin.event.configure.[form|list|datagrid|show]
* - sonata.admin.event.configure.[admin_code].[form|list|datagrid|show] (not implemented yet)
*
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class ConfigureEvent extends Event
{
const TYPE_SHOW = 'show';
const TYPE_DATAGRID = 'datagrid';
const TYPE_FORM = 'form';
const TYPE_LIST = 'list';
/**
* @var AdminInterface
*/
protected $admin;
/**
* @var BaseMapper
*/
protected $mapper;
/**
* @var string
*/
protected $type;
/**
* @param AdminInterface $admin
* @param BaseMapper $mapper
* @param string $type
*/
public function __construct(AdminInterface $admin, BaseMapper $mapper, $type)
{
$this->admin = $admin;
$this->mapper = $mapper;
$this->type = $type;
}
/**
* @return mixed
*/
public function getType()
{
return $this->type;
}
/**
* @return AdminInterface
*/
public function getAdmin()
{
return $this->admin;
}
/**
* @return BaseMapper
*/
public function getMapper()
{
return $this->mapper;
}
}

View File

@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Event;
use Knp\Menu\FactoryInterface;
use Knp\Menu\ItemInterface;
use Symfony\Component\EventDispatcher\Event;
/**
* Menu builder event. Used for extending the menus.
*
* @author Martin Hasoň <martin.hason@gmail.com>
*/
class ConfigureMenuEvent extends Event
{
const SIDEBAR = 'sonata.admin.event.configure.menu.sidebar';
/**
* @var FactoryInterface
*/
private $factory;
/**
* @var ItemInterface
*/
private $menu;
/**
* @param FactoryInterface $factory
* @param ItemInterface $menu
*/
public function __construct(FactoryInterface $factory, ItemInterface $menu)
{
$this->factory = $factory;
$this->menu = $menu;
}
/**
* @return FactoryInterface
*/
public function getFactory()
{
return $this->factory;
}
/**
* @return ItemInterface
*/
public function getMenu()
{
return $this->menu;
}
}

View File

@@ -0,0 +1,80 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Event;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
use Symfony\Component\EventDispatcher\Event;
/**
* This event is sent by hook:
* - configureQuery.
*
* You can register the listener to the event dispatcher by using:
* - sonata.admin.event.configure.query
* - sonata.admin.event.configure.[admin_code].query (not implemented yet)
*
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class ConfigureQueryEvent extends Event
{
/**
* @var AdminInterface
*/
protected $admin;
/**
* @var ProxyQueryInterface
*/
protected $proxyQuery;
/**
* @var string
*/
protected $context;
/**
* @param AdminInterface $admin
* @param ProxyQueryInterface $proxyQuery
* @param string $context
*/
public function __construct(AdminInterface $admin, ProxyQueryInterface $proxyQuery, $context)
{
$this->admin = $admin;
$this->proxyQuery = $proxyQuery;
$this->context = $context;
}
/**
* @return AdminInterface
*/
public function getAdmin()
{
return $this->admin;
}
/**
* @return string
*/
public function getContext()
{
return $this->context;
}
/**
* @return ProxyQueryInterface
*/
public function getProxyQuery()
{
return $this->proxyQuery;
}
}

View File

@@ -0,0 +1,88 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Event;
use Sonata\AdminBundle\Admin\AdminInterface;
use Symfony\Component\EventDispatcher\Event;
/**
* This event is sent by hook:
* - preUpdate | postUpdate
* - prePersist | postPersist
* - preRemove | postRemove.
*
* You can register the listener to the event dispatcher by using:
* - sonata.admin.event.persistence.[pre|post]_[persist|update|remove)
* - sonata.admin.event.persistence.[admin_code].[pre|post]_[persist|update|remove) (not implemented yet)
*
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class PersistenceEvent extends Event
{
const TYPE_PRE_UPDATE = 'pre_update';
const TYPE_POST_UPDATE = 'post_update';
const TYPE_PRE_PERSIST = 'pre_persist';
const TYPE_POST_PERSIST = 'post_persist';
const TYPE_PRE_REMOVE = 'pre_remove';
const TYPE_POST_REMOVE = 'post_remove';
/**
* @var AdminInterface
*/
protected $admin;
/**
* @var object
*/
protected $object;
/**
* @var string
*/
protected $type;
/**
* @param AdminInterface $admin
* @param object $object
* @param string $type
*/
public function __construct(AdminInterface $admin, $object, $type)
{
$this->admin = $admin;
$this->object = $object;
$this->type = $type;
}
/**
* @return AdminInterface
*/
public function getAdmin()
{
return $this->admin;
}
/**
* @return object
*/
public function getObject()
{
return $this->object;
}
/**
* @return mixed
*/
public function getType()
{
return $this->type;
}
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Exception;
/**
* @author Emmanuel Vella <vella.emmanuel@gmail.com>
*/
class LockException extends \Exception
{
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Exception;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class ModelManagerException extends \Exception
{
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Exception;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class NoValueException extends \Exception
{
}

View File

@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Export;
use Sonata\CoreBundle\Exporter\Exporter as BaseExporter;
@trigger_error(
'The '.__NAMESPACE__.'\Exporter class is deprecated since version 3.14 and will be removed in 4.0.'.
' Use Exporter\Exporter instead',
E_USER_DEPRECATED
);
/**
* NEXT_MAJOR: remove this class.
*
* @deprecated
*/
class Exporter extends BaseExporter
{
}

View File

@@ -0,0 +1,277 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Filter;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
abstract class Filter implements FilterInterface
{
/**
* @var string|null
*/
protected $name = null;
/**
* @var mixed|null
*/
protected $value = null;
/**
* @var array
*/
protected $options = [];
/**
* @var string
*/
protected $condition;
/**
* {@inheritdoc}
*/
public function initialize($name, array $options = [])
{
$this->name = $name;
$this->setOptions($options);
}
/**
* {@inheritdoc}
*/
public function getName()
{
return $this->name;
}
/**
* {@inheritdoc}
*/
public function getFormName()
{
/*
Symfony default form class sadly can't handle
form element with dots in its name (when data
get bound, the default dataMapper is a PropertyPathMapper).
So use this trick to avoid any issue.
*/
return str_replace('.', '__', $this->name);
}
/**
* {@inheritdoc}
*/
public function getOption($name, $default = null)
{
if (array_key_exists($name, $this->options)) {
return $this->options[$name];
}
return $default;
}
/**
* {@inheritdoc}
*/
public function setOption($name, $value)
{
$this->options[$name] = $value;
}
/**
* {@inheritdoc}
*/
public function getFieldType()
{
// NEXT_MAJOR: Remove ternary and keep 'Symfony\Component\Form\Extension\Core\Type\TextType'
// (when requirement of Symfony is >= 2.8)
return $this->getOption('field_type', method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')
? 'Symfony\Component\Form\Extension\Core\Type\TextType'
: 'text'
);
}
/**
* {@inheritdoc}
*/
public function getFieldOptions()
{
return $this->getOption('field_options', ['required' => false]);
}
/**
* {@inheritdoc}
*/
public function getFieldOption($name, $default = null)
{
if (isset($this->options['field_options'][$name]) && is_array($this->options['field_options'])) {
return $this->options['field_options'][$name];
}
return $default;
}
/**
* {@inheritdoc}
*/
public function setFieldOption($name, $value)
{
$this->options['field_options'][$name] = $value;
}
/**
* {@inheritdoc}
*/
public function getLabel()
{
return $this->getOption('label');
}
/**
* {@inheritdoc}
*/
public function setLabel($label)
{
$this->setOption('label', $label);
}
/**
* {@inheritdoc}
*/
public function getFieldName()
{
$fieldName = $this->getOption('field_name');
if (!$fieldName) {
throw new \RuntimeException(sprintf('The option `field_name` must be set for field: `%s`', $this->getName()));
}
return $fieldName;
}
/**
* {@inheritdoc}
*/
public function getParentAssociationMappings()
{
return $this->getOption('parent_association_mappings', []);
}
/**
* {@inheritdoc}
*/
public function getFieldMapping()
{
$fieldMapping = $this->getOption('field_mapping');
if (!$fieldMapping) {
throw new \RuntimeException(sprintf('The option `field_mapping` must be set for field: `%s`', $this->getName()));
}
return $fieldMapping;
}
/**
* {@inheritdoc}
*/
public function getAssociationMapping()
{
$associationMapping = $this->getOption('association_mapping');
if (!$associationMapping) {
throw new \RuntimeException(sprintf('The option `association_mapping` must be set for field: `%s`', $this->getName()));
}
return $associationMapping;
}
/**
* Set options.
*
* @param array $options
*/
public function setOptions(array $options)
{
$this->options = array_merge(
['show_filter' => null, 'advanced_filter' => true],
$this->getDefaultOptions(),
$options
);
}
/**
* Get options.
*
* @return array
*/
public function getOptions()
{
return $this->options;
}
/**
* Set value.
*
* @param mixed $value
*/
public function setValue($value)
{
$this->value = $value;
}
/**
* Get value.
*
* @return mixed
*/
public function getValue()
{
return $this->value;
}
/**
* {@inheritdoc}
*/
public function isActive()
{
$values = $this->getValue();
return isset($values['value'])
&& false !== $values['value']
&& '' !== $values['value'];
}
/**
* {@inheritdoc}
*/
public function setCondition($condition)
{
$this->condition = $condition;
}
/**
* {@inheritdoc}
*/
public function getCondition()
{
return $this->condition;
}
/**
* {@inheritdoc}
*/
public function getTranslationDomain()
{
return $this->getOption('translation_domain');
}
}

View File

@@ -0,0 +1,68 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Filter;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class FilterFactory implements FilterFactoryInterface
{
/**
* @var ContainerInterface
*/
protected $container;
/**
* @var string[]
*/
protected $types;
/**
* @param ContainerInterface $container
* @param string[] $types
*/
public function __construct(ContainerInterface $container, array $types = [])
{
$this->container = $container;
$this->types = $types;
}
/**
* {@inheritdoc}
*/
public function create($name, $type, array $options = [])
{
if (!$type) {
throw new \RuntimeException('The type must be defined');
}
$id = isset($this->types[$type]) ? $this->types[$type] : false;
if ($id) {
$filter = $this->container->get($id);
} elseif (class_exists($type)) {
$filter = new $type();
} else {
throw new \RuntimeException(sprintf('No attached service to type named `%s`', $type));
}
if (!$filter instanceof FilterInterface) {
throw new \RuntimeException(sprintf('The service `%s` must implement `FilterInterface`', $type));
}
$filter->initialize($name, $options);
return $filter;
}
}

View File

@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Filter;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
interface FilterFactoryInterface
{
/**
* @param string $name
* @param string $type
* @param array $options
*
* @return mixed
*/
public function create($name, $type, array $options = []);
}

View File

@@ -0,0 +1,170 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Filter;
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
interface FilterInterface
{
const CONDITION_OR = 'OR';
const CONDITION_AND = 'AND';
/**
* Apply the filter to the QueryBuilder instance.
*
* @param ProxyQueryInterface $queryBuilder
* @param string $alias
* @param string $field
* @param string $value
*/
public function filter(ProxyQueryInterface $queryBuilder, $alias, $field, $value);
/**
* @param mixed $query
* @param mixed $value
*/
public function apply($query, $value);
/**
* Returns the filter name.
*
* @return string
*/
public function getName();
/**
* Returns the filter form name.
*
* @return string
*/
public function getFormName();
/**
* Returns the label name.
*
* @return string|bool
*/
public function getLabel();
/**
* @param string $label
*/
public function setLabel($label);
/**
* @return array
*/
public function getDefaultOptions();
/**
* @param string $name
* @param null $default
*
* @return mixed
*/
public function getOption($name, $default = null);
/**
* @param string $name
* @param mixed $value
*/
public function setOption($name, $value);
/**
* @param string $name
* @param array $options
*/
public function initialize($name, array $options = []);
/**
* @return string
*/
public function getFieldName();
/**
* @return array of mappings
*/
public function getParentAssociationMappings();
/**
* @return array field mapping
*/
public function getFieldMapping();
/**
* @return array association mapping
*/
public function getAssociationMapping();
/**
* @return array
*/
public function getFieldOptions();
/**
* Get field option.
*
* @param string $name
* @param null $default
*
* @return mixed
*/
public function getFieldOption($name, $default = null);
/**
* Set field option.
*
* @param string $name
* @param mixed $value
*/
public function setFieldOption($name, $value);
/**
* @return string
*/
public function getFieldType();
/**
* Returns the main widget used to render the filter.
*
* @return array
*/
public function getRenderSettings();
/**
* Returns true if filter is active.
*
* @return bool
*/
public function isActive();
/**
* Set the condition to use with the left side of the query : OR or AND.
*
* @param string $condition
*/
public function setCondition($condition);
/**
* @return string
*/
public function getCondition();
/**
* @return string
*/
public function getTranslationDomain();
}

View File

@@ -0,0 +1,300 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\ChoiceList;
use Doctrine\Common\Util\ClassUtils;
use Sonata\AdminBundle\Model\ModelManagerInterface;
use Symfony\Component\Form\Exception\InvalidArgumentException;
use Symfony\Component\Form\Exception\RuntimeException;
use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyAccess\PropertyPath;
@trigger_error(
'The '.__CLASS__.' class is deprecated since 3.24 and will be removed in 4.0.'
.' Use '.__NAMESPACE__.'\ModelChoiceLoader instead.',
E_USER_DEPRECATED
);
/**
* NEXT_MAJOR: Remove this class.
*
* @deprecated Since 3.24, to be removed in 4.0. Use Sonata\AdminBundle\ModelChoiceLoader instead
*
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class ModelChoiceList extends SimpleChoiceList
{
/**
* @var ModelManagerInterface
*/
private $modelManager;
/**
* @var string
*/
private $class;
/**
* The entities from which the user can choose.
*
* This array is either indexed by ID (if the ID is a single field)
* or by key in the choices array (if the ID consists of multiple fields)
*
* This property is initialized by initializeChoices(). It should only
* be accessed through getEntity() and getEntities().
*
* @var mixed
*/
private $entities = [];
/**
* Contains the query builder that builds the query for fetching the
* entities.
*
* This property should only be accessed through queryBuilder.
*
* @var \Doctrine\ORM\QueryBuilder
*/
private $query;
/**
* The fields of which the identifier of the underlying class consists.
*
* This property should only be accessed through identifier.
*
* @var array
*/
private $identifier = [];
/**
* A cache for \ReflectionProperty instances for the underlying class.
*
* This property should only be accessed through getReflProperty().
*
* @var array
*/
private $reflProperties = [];
/**
* @var PropertyPath
*/
private $propertyPath;
/**
* @var PropertyAccessorInterface
*/
private $propertyAccessor;
/**
* @param ModelManagerInterface $modelManager
* @param string $class
* @param null $property
* @param null $query
* @param array $choices
*/
public function __construct(ModelManagerInterface $modelManager, $class, $property = null, $query = null, $choices = [], PropertyAccessorInterface $propertyAccessor = null)
{
$this->modelManager = $modelManager;
$this->class = $class;
$this->query = $query;
$this->identifier = $this->modelManager->getIdentifierFieldNames($this->class);
// The property option defines, which property (path) is used for
// displaying entities as strings
if ($property) {
$this->propertyPath = new PropertyPath($property);
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
}
parent::__construct($this->load($choices));
}
/**
* @return array
*/
public function getIdentifier()
{
return $this->identifier;
}
/**
* Returns the according entities for the choices.
*
* If the choices were not initialized, they are initialized now. This
* is an expensive operation, except if the entities were passed in the
* "choices" option.
*
* @return array An array of entities
*/
public function getEntities()
{
return $this->entities;
}
/**
* Returns the entity for the given key.
*
* If the underlying entities have composite identifiers, the choices
* are initialized. The key is expected to be the index in the choices
* array in this case.
*
* If they have single identifiers, they are either fetched from the
* internal entity cache (if filled) or loaded from the database.
*
* @param string $key The choice key (for entities with composite
* identifiers) or entity ID (for entities with single
* identifiers)
*
* @return object The matching entity
*/
public function getEntity($key)
{
if (count($this->identifier) > 1) {
// $key is a collection index
$entities = $this->getEntities();
return isset($entities[$key]) ? $entities[$key] : null;
} elseif ($this->entities) {
return isset($this->entities[$key]) ? $this->entities[$key] : null;
}
return $this->modelManager->find($this->class, $key);
}
/**
* Returns the values of the identifier fields of an entity.
*
* Doctrine must know about this entity, that is, the entity must already
* be persisted or added to the identity map before. Otherwise an
* exception is thrown.
*
* @param object $entity The entity for which to get the identifier
*
* @throws InvalidArgumentException If the entity does not exist in Doctrine's
* identity map
*
* @return array
*/
public function getIdentifierValues($entity)
{
try {
return $this->modelManager->getIdentifierValues($entity);
} catch (\Exception $e) {
throw new InvalidArgumentException(sprintf('Unable to retrieve the identifier values for entity %s', ClassUtils::getClass($entity)), 0, $e);
}
}
/**
* @return ModelManagerInterface
*/
public function getModelManager()
{
return $this->modelManager;
}
/**
* @return string
*/
public function getClass()
{
return $this->class;
}
/**
* Initializes the choices and returns them.
*
* The choices are generated from the entities. If the entities have a
* composite identifier, the choices are indexed using ascending integers.
* Otherwise the identifiers are used as indices.
*
* If the entities were passed in the "choices" option, this method
* does not have any significant overhead. Otherwise, if a query builder
* was passed in the "query" option, this builder is now used to construct
* a query which is executed. In the last case, all entities for the
* underlying class are fetched from the repository.
*
* If the option "property" was passed, the property path in that option
* is used as option values. Otherwise this method tries to convert
* objects to strings using __toString().
*
* @param $choices
*
* @return array An array of choices
*/
protected function load($choices)
{
if (is_array($choices) && count($choices) > 0) {
$entities = $choices;
} elseif ($this->query) {
$entities = $this->modelManager->executeQuery($this->query);
} else {
$entities = $this->modelManager->findBy($this->class);
}
if (null === $entities) {
return [];
}
$choices = [];
$this->entities = [];
foreach ($entities as $key => $entity) {
if ($this->propertyPath) {
// If the property option was given, use it
$value = $this->propertyAccessor->getValue($entity, $this->propertyPath);
} else {
// Otherwise expect a __toString() method in the entity
try {
$value = (string) $entity;
} catch (\Exception $e) {
throw new RuntimeException(sprintf('Unable to convert the entity "%s" to string, provide '
.'"property" option or implement "__toString()" method in your entity.', ClassUtils::getClass($entity)), 0, $e);
}
}
if (count($this->identifier) > 1) {
// When the identifier consists of multiple field, use
// naturally ordered keys to refer to the choices
$choices[$key] = $value;
$this->entities[$key] = $entity;
} else {
// When the identifier is a single field, index choices by
// entity ID for performance reasons
$id = current($this->getIdentifierValues($entity));
$choices[$id] = $value;
$this->entities[$id] = $entity;
}
}
return $choices;
}
/**
* Returns the \ReflectionProperty instance for a property of the
* underlying class.
*
* @param string $property The name of the property
*
* @return \ReflectionProperty The reflection instance
*/
private function getReflProperty($property)
{
if (!isset($this->reflProperties[$property])) {
$this->reflProperties[$property] = new \ReflectionProperty($this->class, $property);
$this->reflProperties[$property]->setAccessible(true);
}
return $this->reflProperties[$property];
}
}

View File

@@ -0,0 +1,168 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\ChoiceList;
use Doctrine\Common\Util\ClassUtils;
use Sonata\AdminBundle\Model\ModelManagerInterface;
use Sonata\CoreBundle\Model\Adapter\AdapterInterface;
use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\Exception\RuntimeException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyAccess\PropertyPath;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class ModelChoiceLoader implements ChoiceLoaderInterface
{
public $identifier;
/**
* @var \Sonata\AdminBundle\Model\ModelManagerInterface
*/
private $modelManager;
/**
* @var string
*/
private $class;
private $property;
private $query;
private $choices;
/**
* @var PropertyPath
*/
private $propertyPath;
/**
* @var PropertyAccessorInterface
*/
private $propertyAccessor;
private $choiceList;
/**
* @param ModelManagerInterface $modelManager
* @param string $class
* @param null $property
* @param null $query
* @param array $choices
* @param PropertyAccessorInterface|null $propertyAccessor
*/
public function __construct(ModelManagerInterface $modelManager, $class, $property = null, $query = null, $choices = [], PropertyAccessorInterface $propertyAccessor = null)
{
$this->modelManager = $modelManager;
$this->class = $class;
$this->property = $property;
$this->query = $query;
$this->choices = $choices;
$this->identifier = $this->modelManager->getIdentifierFieldNames($this->class);
// The property option defines, which property (path) is used for
// displaying entities as strings
if ($property) {
$this->propertyPath = new PropertyPath($property);
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
}
}
/**
* {@inheritdoc}
*/
public function loadChoiceList($value = null)
{
if (!$this->choiceList) {
if ($this->query) {
$entities = $this->modelManager->executeQuery($this->query);
} elseif (is_array($this->choices) && count($this->choices) > 0) {
$entities = $this->choices;
} else {
$entities = $this->modelManager->findBy($this->class);
}
$choices = [];
foreach ($entities as $key => $entity) {
if ($this->propertyPath) {
// If the property option was given, use it
$valueObject = $this->propertyAccessor->getValue($entity, $this->propertyPath);
} else {
// Otherwise expect a __toString() method in the entity
try {
$valueObject = (string) $entity;
} catch (\Exception $e) {
throw new RuntimeException(sprintf('Unable to convert the entity "%s" to string, provide "property" option or implement "__toString()" method in your entity.', ClassUtils::getClass($entity)), 0, $e);
}
}
$id = implode(AdapterInterface::ID_SEPARATOR, $this->getIdentifierValues($entity));
if (!array_key_exists($valueObject, $choices)) {
$choices[$valueObject] = [];
}
$choices[$valueObject][] = $id;
}
$finalChoices = [];
foreach ($choices as $valueObject => $idx) {
if (count($idx) > 1) { // avoid issue with identical values ...
foreach ($idx as $id) {
$finalChoices[sprintf('%s (id: %s)', $valueObject, $id)] = $id;
}
} else {
$finalChoices[$valueObject] = current($idx);
}
}
$this->choiceList = new ArrayChoiceList($finalChoices, $value);
}
return $this->choiceList;
}
/**
* {@inheritdoc}
*/
public function loadChoicesForValues(array $values, $value = null)
{
return $this->loadChoiceList($value)->getChoicesForValues($values);
}
/**
* {@inheritdoc}
*/
public function loadValuesForChoices(array $choices, $value = null)
{
return $this->loadChoiceList($value)->getValuesForChoices($choices);
}
/**
* @param object $entity
*
* @return array
*/
private function getIdentifierValues($entity)
{
try {
return $this->modelManager->getIdentifierValues($entity);
} catch (\Exception $e) {
throw new \InvalidArgumentException(sprintf('Unable to retrieve the identifier values for entity %s', ClassUtils::getClass($entity)), 0, $e);
}
}
}

View File

@@ -0,0 +1,69 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\DataTransformer;
use Sonata\AdminBundle\Model\ModelManagerInterface;
use Symfony\Component\Form\DataTransformerInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class ArrayToModelTransformer implements DataTransformerInterface
{
/**
* @var ModelManagerInterface
*/
protected $modelManager;
/**
* @var string
*/
protected $className;
/**
* @param ModelManagerInterface $modelManager
* @param string $className
*/
public function __construct(ModelManagerInterface $modelManager, $className)
{
$this->modelManager = $modelManager;
$this->className = $className;
}
/**
* {@inheritdoc}
*/
public function reverseTransform($array)
{
// when the object is created the form return an array
// one the object is persisted, the edit $array is the user instance
if ($array instanceof $this->className) {
return $array;
}
$instance = new $this->className();
if (!is_array($array)) {
return $instance;
}
return $this->modelManager->modelReverseTransform($this->className, $array);
}
/**
* {@inheritdoc}
*/
public function transform($value)
{
return $value;
}
}

View File

@@ -0,0 +1,115 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\DataTransformer;
use Sonata\AdminBundle\Form\ChoiceList\ModelChoiceList;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
/**
* NEXT_MAJOR: remove this class when dropping Symfony < 2.7 support.
*
* This class should be used with Symfony <2.7 only and will be deprecated with 3.0.
*
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class LegacyModelsToArrayTransformer implements DataTransformerInterface
{
/**
* @var ModelChoiceList
*/
protected $choiceList;
/**
* @param ModelChoiceList $choiceList
*/
public function __construct(ModelChoiceList $choiceList)
{
if (interface_exists('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface')) {
@trigger_error(
'The '.__CLASS__.' class is deprecated since 3.11, to be removed in 4.0. '.
'Use Sonata\AdminBundle\Form\DataTransformer\ModelsToArrayTransformer instead.',
E_USER_DEPRECATED
);
}
$this->choiceList = $choiceList;
}
/**
* {@inheritdoc}
*/
public function transform($collection)
{
if (null === $collection) {
return [];
}
$array = [];
if (count($this->choiceList->getIdentifier()) > 1) {
// load all choices
$availableEntities = $this->choiceList->getEntities();
foreach ($collection as $entity) {
// identify choices by their collection key
$key = array_search($entity, $availableEntities);
$array[] = $key;
}
} else {
foreach ($collection as $entity) {
$array[] = current($this->choiceList->getIdentifierValues($entity));
}
}
return $array;
}
/**
* {@inheritdoc}
*/
public function reverseTransform($keys)
{
$collection = $this->choiceList->getModelManager()->getModelCollectionInstance(
$this->choiceList->getClass()
);
if (!$collection instanceof \ArrayAccess) {
throw new UnexpectedTypeException($collection, '\ArrayAccess');
}
if ('' === $keys || null === $keys) {
return $collection;
}
if (!is_array($keys)) {
throw new UnexpectedTypeException($keys, 'array');
}
$notFound = [];
// optimize this into a SELECT WHERE IN query
foreach ($keys as $key) {
if ($entity = $this->choiceList->getEntity($key)) {
$collection[] = $entity;
} else {
$notFound[] = $key;
}
}
if (count($notFound) > 0) {
throw new TransformationFailedException(sprintf('The entities with keys "%s" could not be found', implode('", "', $notFound)));
}
return $collection;
}
}

View File

@@ -0,0 +1,157 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\DataTransformer;
use Doctrine\Common\Util\ClassUtils;
use Sonata\AdminBundle\Model\ModelManagerInterface;
use Symfony\Component\Form\DataTransformerInterface;
/**
* Transform object to ID and property label.
*
* @author Andrej Hudec <pulzarraider@gmail.com>
*/
class ModelToIdPropertyTransformer implements DataTransformerInterface
{
/**
* @var ModelManagerInterface
*/
protected $modelManager;
/**
* @var string
*/
protected $className;
/**
* @var string
*/
protected $property;
/**
* @var bool
*/
protected $multiple;
/**
* @var callable|null
*/
protected $toStringCallback;
/**
* @param ModelManagerInterface $modelManager
* @param string $className
* @param string $property
* @param bool $multiple
* @param null $toStringCallback
*/
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)) {
if ($this->multiple) {
return $collection;
}
return;
}
if (!$this->multiple) {
return $this->modelManager->find($this->className, $value);
}
if (!is_array($value)) {
throw new \UnexpectedValueException(sprintf('Value should be array, %s given.', gettype($value)));
}
foreach ($value as $key => $id) {
if ($key === '_labels') {
continue;
}
$collection->add($this->modelManager->find($this->className, $id));
}
return $collection;
}
/**
* {@inheritdoc}
*/
public function transform($entityOrCollection)
{
$result = [];
if (!$entityOrCollection) {
return $result;
}
if ($this->multiple) {
$isArray = is_array($entityOrCollection);
if (!$isArray && substr(get_class($entityOrCollection), -1 * strlen($this->className)) == $this->className) {
throw new \InvalidArgumentException('A multiple selection must be passed a collection not a single value. Make sure that form option "multiple=false" is set for many-to-one relation and "multiple=true" is set for many-to-many or one-to-many relations.');
} elseif ($isArray || ($entityOrCollection instanceof \ArrayAccess)) {
$collection = $entityOrCollection;
} else {
throw new \InvalidArgumentException('A multiple selection must be passed a collection not a single value. Make sure that form option "multiple=false" is set for many-to-one relation and "multiple=true" is set for many-to-many or one-to-many relations.');
}
} else {
if (substr(get_class($entityOrCollection), -1 * strlen($this->className)) == $this->className) {
$collection = [$entityOrCollection];
} elseif ($entityOrCollection instanceof \ArrayAccess) {
throw new \InvalidArgumentException('A single selection must be passed a single value not a collection. Make sure that form option "multiple=false" is set for many-to-one relation and "multiple=true" is set for many-to-many or one-to-many relations.');
} else {
$collection = [$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[] = $id;
$result['_labels'][] = $label;
}
return $result;
}
}

View File

@@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\DataTransformer;
use Sonata\AdminBundle\Model\ModelManagerInterface;
use Symfony\Component\Form\DataTransformerInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class ModelToIdTransformer implements DataTransformerInterface
{
/**
* @var ModelManagerInterface
*/
protected $modelManager;
/**
* @var string
*/
protected $className;
/**
* @param ModelManagerInterface $modelManager
* @param string $className
*/
public function __construct(ModelManagerInterface $modelManager, $className)
{
$this->modelManager = $modelManager;
$this->className = $className;
}
/**
* {@inheritdoc}
*/
public function reverseTransform($newId)
{
if (empty($newId) && !in_array($newId, ['0', 0], true)) {
return;
}
return $this->modelManager->find($this->className, $newId);
}
/**
* {@inheritdoc}
*/
public function transform($entity)
{
if (empty($entity)) {
return;
}
return $this->modelManager->getNormalizedIdentifier($entity);
}
}

View File

@@ -0,0 +1,217 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\DataTransformer;
use Doctrine\Common\Util\ClassUtils;
use Sonata\AdminBundle\Form\ChoiceList\ModelChoiceList;
use Sonata\AdminBundle\Form\ChoiceList\ModelChoiceLoader;
use Sonata\AdminBundle\Model\ModelManagerInterface;
use Sonata\CoreBundle\Model\Adapter\AdapterInterface;
use Symfony\Component\Form\ChoiceList\LazyChoiceList;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\RuntimeException;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class ModelsToArrayTransformer implements DataTransformerInterface
{
/**
* @var ModelManagerInterface
*/
protected $modelManager;
/**
* @var string
*/
protected $class;
/**
* @var ModelChoiceList
*
* @deprecated since 3.12, to be removed in 4.0
* NEXT_MAJOR: remove this property
*/
protected $choiceList;
/**
* ModelsToArrayTransformer constructor.
*
* @param ModelChoiceList|LazyChoiceList|ModelChoiceLoader $choiceList
* @param ModelManagerInterface $modelManager
* @param $class
*
* @throws RuntimeException
*/
public function __construct($choiceList, $modelManager, $class = null)
{
/*
NEXT_MAJOR: Remove condition , magic methods, legacyConstructor() method, $choiceList property and argument
__construct() signature should be : public function __construct(ModelManager $modelManager, $class)
*/
$args = func_get_args();
if (func_num_args() == 3) {
$this->legacyConstructor($args);
} else {
$this->modelManager = $args[0];
$this->class = $args[1];
}
}
/**
* @internal
*/
public function __get($name)
{
if ('choiceList' === $name) {
$this->triggerDeprecation();
}
return $this->$name;
}
/**
* @internal
*/
public function __set($name, $value)
{
if ('choiceList' === $name) {
$this->triggerDeprecation();
}
$this->$name = $value;
}
/**
* @internal
*/
public function __isset($name)
{
if ('choiceList' === $name) {
$this->triggerDeprecation();
}
return isset($this->$name);
}
/**
* @internal
*/
public function __unset($name)
{
if ('choiceList' === $name) {
$this->triggerDeprecation();
}
unset($this->$name);
}
/**
* {@inheritdoc}
*/
public function transform($collection)
{
if (null === $collection) {
return [];
}
$array = [];
foreach ($collection as $key => $entity) {
$id = implode(AdapterInterface::ID_SEPARATOR, $this->getIdentifierValues($entity));
$array[] = $id;
}
return $array;
}
/**
* {@inheritdoc}
*/
public function reverseTransform($keys)
{
if (!is_array($keys)) {
throw new UnexpectedTypeException($keys, 'array');
}
$collection = $this->modelManager->getModelCollectionInstance($this->class);
$notFound = [];
// optimize this into a SELECT WHERE IN query
foreach ($keys as $key) {
if ($entity = $this->modelManager->find($this->class, $key)) {
$collection[] = $entity;
} else {
$notFound[] = $key;
}
}
if (count($notFound) > 0) {
throw new TransformationFailedException(sprintf('The entities with keys "%s" could not be found', implode('", "', $notFound)));
}
return $collection;
}
/**
* Simulates the old constructor for BC.
*
* @param array $args
*
* @throws RuntimeException
*/
private function legacyConstructor($args)
{
$choiceList = $args[0];
if (!$choiceList instanceof ModelChoiceList
&& !$choiceList instanceof ModelChoiceLoader
&& !$choiceList instanceof LazyChoiceList) {
throw new RuntimeException('First param passed to ModelsToArrayTransformer should be instance of
ModelChoiceLoader or ModelChoiceList or LazyChoiceList');
}
$this->choiceList = $choiceList;
$this->modelManager = $args[1];
$this->class = $args[2];
}
/**
* @param object $entity
*
* @return array
*/
private function getIdentifierValues($entity)
{
try {
return $this->modelManager->getIdentifierValues($entity);
} catch (\Exception $e) {
throw new \InvalidArgumentException(sprintf('Unable to retrieve the identifier values for entity %s', ClassUtils::getClass($entity)), 0, $e);
}
}
/**
* @internal
*/
private function triggerDeprecation()
{
@trigger_error(sprintf(
'Using the "%s::$choiceList" property is deprecated since version 3.12 and will be removed in 4.0.',
__CLASS__),
E_USER_DEPRECATED)
;
}
}

View File

@@ -0,0 +1,79 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\EventListener;
use Sonata\AdminBundle\Model\ModelManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class MergeCollectionListener implements EventSubscriberInterface
{
/**
* @var ModelManagerInterface
*/
protected $modelManager;
/**
* @param ModelManagerInterface $modelManager
*/
public function __construct(ModelManagerInterface $modelManager)
{
$this->modelManager = $modelManager;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return [
FormEvents::SUBMIT => ['onBind', 10],
];
}
/**
* @param FormEvent $event
*/
public function onBind(FormEvent $event)
{
$collection = $event->getForm()->getData();
$data = $event->getData();
// looks like there is no way to remove other listeners
$event->stopPropagation();
if (!$collection) {
$collection = $data;
} elseif (count($data) === 0) {
$this->modelManager->collectionClear($collection);
} else {
// merge $data into $collection
foreach ($collection as $entity) {
if (!$this->modelManager->collectionHasElement($data, $entity)) {
$this->modelManager->collectionRemoveElement($collection, $entity);
} else {
$this->modelManager->collectionRemoveElement($data, $entity);
}
}
foreach ($data as $entity) {
$this->modelManager->collectionAddElement($collection, $entity);
}
}
$event->setData($collection);
}
}

View File

@@ -0,0 +1,71 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\Extension;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
/**
* @author Amine Zaghdoudi <amine.zaghdoudi@ekino.com>
*/
class ChoiceTypeExtension extends AbstractTypeExtension
{
/**
* NEXT_MAJOR: Remove method, when bumping requirements to SF 2.7+.
*
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$this->configureOptions($resolver);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$optionalOptions = ['sortable'];
if (method_exists($resolver, 'setDefined')) {
$resolver->setDefined($optionalOptions);
} else {
// To keep Symfony <2.6 support
$resolver->setOptional($optionalOptions);
}
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['sortable'] = array_key_exists('sortable', $options) && $options['sortable'];
}
/**
* {@inheritdoc}
*/
public function getExtendedType()
{
/*
* NEXT_MAJOR: Remove when dropping Symfony <2.8 support. It should
* simply be return 'Symfony\Component\Form\Extension\Core\Type\ChoiceType';
*/
return method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')
? 'Symfony\Component\Form\Extension\Core\Type\ChoiceType'
: 'choice';
}
}

View File

@@ -0,0 +1,263 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\Extension\Field\Type;
use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
use Sonata\AdminBundle\Exception\NoValueException;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class FormTypeFieldExtension extends AbstractTypeExtension
{
/**
* @var array
*/
protected $defaultClasses = [];
/**
* @var array
*/
protected $options;
/**
* @param array $defaultClasses
* @param array $options
*/
public function __construct(array $defaultClasses, array $options)
{
$this->defaultClasses = $defaultClasses;
$this->options = $options;
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$sonataAdmin = [
'name' => null,
'admin' => null,
'value' => null,
'edit' => 'standard',
'inline' => 'natural',
'field_description' => null,
'block_name' => false,
'options' => $this->options,
];
$builder->setAttribute('sonata_admin_enabled', false);
$builder->setAttribute('sonata_help', false);
if ($options['sonata_field_description'] instanceof FieldDescriptionInterface) {
$fieldDescription = $options['sonata_field_description'];
$sonataAdmin['admin'] = $fieldDescription->getAdmin();
$sonataAdmin['field_description'] = $fieldDescription;
$sonataAdmin['name'] = $fieldDescription->getName();
$sonataAdmin['edit'] = $fieldDescription->getOption('edit', 'standard');
$sonataAdmin['inline'] = $fieldDescription->getOption('inline', 'natural');
$sonataAdmin['block_name'] = $fieldDescription->getOption('block_name', false);
$sonataAdmin['class'] = $this->getClass($builder);
$builder->setAttribute('sonata_admin_enabled', true);
}
$builder->setAttribute('sonata_admin', $sonataAdmin);
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$sonataAdmin = $form->getConfig()->getAttribute('sonata_admin');
$sonataAdminHelp = isset($options['sonata_help']) ? $options['sonata_help'] : null;
/*
* We have a child, so we need to upgrade block prefix
*/
if ($view->parent && $view->parent->vars['sonata_admin_enabled'] && !$sonataAdmin['admin']) {
$blockPrefixes = $view->vars['block_prefixes'];
$baseName = str_replace('.', '_', $view->parent->vars['sonata_admin_code']);
$baseType = $blockPrefixes[count($blockPrefixes) - 2];
$blockSuffix = preg_replace('#^_([a-z0-9]{14})_(.++)$#', '$2', array_pop($blockPrefixes));
$blockPrefixes[] = sprintf('%s_%s', $baseName, $baseType);
$blockPrefixes[] = sprintf('%s_%s_%s_%s', $baseName, $baseType, $view->parent->vars['name'], $view->vars['name']);
$blockPrefixes[] = sprintf('%s_%s_%s_%s', $baseName, $baseType, $view->parent->vars['name'], $blockSuffix);
$view->vars['block_prefixes'] = $blockPrefixes;
$view->vars['sonata_admin_enabled'] = true;
$view->vars['sonata_admin'] = [
'admin' => false,
'field_description' => false,
'name' => false,
'edit' => 'standard',
'inline' => 'natural',
'block_name' => false,
'class' => false,
'options' => $this->options,
];
$view->vars['sonata_help'] = $sonataAdminHelp;
$view->vars['sonata_admin_code'] = $view->parent->vars['sonata_admin_code'];
return;
}
// avoid to add extra information not required by non admin field
if ($sonataAdmin && $form->getConfig()->getAttribute('sonata_admin_enabled', true)) {
$sonataAdmin['value'] = $form->getData();
// add a new block types, so the Admin Form element can be tweaked based on the admin code
$blockPrefixes = $view->vars['block_prefixes'];
$baseName = str_replace('.', '_', $sonataAdmin['admin']->getCode());
$baseType = $blockPrefixes[count($blockPrefixes) - 2];
$blockSuffix = preg_replace('#^_([a-z0-9]{14})_(.++)$#', '$2', array_pop($blockPrefixes));
$blockPrefixes[] = sprintf('%s_%s', $baseName, $baseType);
$blockPrefixes[] = sprintf('%s_%s_%s', $baseName, $sonataAdmin['name'], $baseType);
$blockPrefixes[] = sprintf('%s_%s_%s_%s', $baseName, $sonataAdmin['name'], $baseType, $blockSuffix);
if (isset($sonataAdmin['block_name']) && $sonataAdmin['block_name'] !== false) {
$blockPrefixes[] = $sonataAdmin['block_name'];
}
$view->vars['block_prefixes'] = $blockPrefixes;
$view->vars['sonata_admin_enabled'] = true;
$view->vars['sonata_admin'] = $sonataAdmin;
$view->vars['sonata_admin_code'] = $sonataAdmin['admin']->getCode();
$attr = $view->vars['attr'];
if (!isset($attr['class']) && isset($sonataAdmin['class'])) {
$attr['class'] = $sonataAdmin['class'];
}
$view->vars['attr'] = $attr;
} else {
$view->vars['sonata_admin_enabled'] = false;
}
$view->vars['sonata_help'] = $sonataAdminHelp;
$view->vars['sonata_admin'] = $sonataAdmin;
}
/**
* {@inheritdoc}
*/
public function getExtendedType()
{
return
method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix') ?
'Symfony\Component\Form\Extension\Core\Type\FormType' :
'form';
}
/**
* NEXT_MAJOR: Remove method, when bumping requirements to SF 2.7+.
*
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$this->configureOptions($resolver);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'sonata_admin' => null,
'sonata_field_description' => null,
// be compatible with mopa if not installed, avoid generating an exception for invalid option
'label_render' => true,
'sonata_help' => null,
]);
}
/**
* return the value related to FieldDescription, if the associated object does no
* exists => a temporary one is created.
*
* @param object $object
* @param FieldDescriptionInterface $fieldDescription
*
* @return mixed
*/
public function getValueFromFieldDescription($object, FieldDescriptionInterface $fieldDescription)
{
$value = null;
if (!$object) {
return $value;
}
try {
$value = $fieldDescription->getValue($object);
} catch (NoValueException $e) {
if ($fieldDescription->getAssociationAdmin()) {
$value = $fieldDescription->getAssociationAdmin()->getNewInstance();
}
}
return $value;
}
/**
* @param FormBuilderInterface $formBuilder
*
* @return string
*/
protected function getClass(FormBuilderInterface $formBuilder)
{
foreach ($this->getTypes($formBuilder) as $type) {
if (!method_exists($type, 'getName')) { // SF3.0+
$name = get_class($type);
} else {
$name = $type->getName();
}
if (isset($this->defaultClasses[$name])) {
return $this->defaultClasses[$name];
}
}
return '';
}
/**
* @param FormBuilderInterface $formBuilder
*
* @return array
*/
protected function getTypes(FormBuilderInterface $formBuilder)
{
$types = [];
for ($type = $formBuilder->getType(); null !== $type; $type = $type->getParent()) {
array_unshift($types, $type->getInnerType());
}
return $types;
}
}

View File

@@ -0,0 +1,73 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\Extension\Field\Type;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
/**
* This class is built to allow AdminInterface to work properly
* if the MopaBootstrapBundle is not installed.
*
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class MopaCompatibilityTypeFieldExtension extends AbstractTypeExtension
{
/**
* NEXT_MAJOR: Remove method, when bumping requirements to SF 2.7+.
*
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$this->configureOptions($resolver);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'horizontal_label_class' => '',
'horizontal_label_offset_class' => '',
'horizontal_input_wrapper_class' => '',
]);
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['horizontal_label_class'] = $options['horizontal_label_class'];
$view->vars['horizontal_label_offset_class'] = $options['horizontal_label_offset_class'];
$view->vars['horizontal_input_wrapper_class'] = $options['horizontal_input_wrapper_class'];
}
/**
* {@inheritdoc}
*/
public function getExtendedType()
{
/*
* NEXT_MAJOR: Remove when dropping Symfony <2.8 support. It should
* simply be return 'Symfony\Component\Form\Extension\Core\Type\FormType';
*/
return method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')
? 'Symfony\Component\Form\Extension\Core\Type\FormType'
: 'form';
}
}

View File

@@ -0,0 +1,336 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Builder\FormContractorInterface;
use Sonata\AdminBundle\Mapper\BaseGroupedMapper;
use Symfony\Component\Form\FormBuilderInterface;
/**
* This class is use to simulate the Form API.
*
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class FormMapper extends BaseGroupedMapper
{
/**
* @var FormBuilderInterface
*/
protected $formBuilder;
/**
* @param FormContractorInterface $formContractor
* @param FormBuilderInterface $formBuilder
* @param AdminInterface $admin
*/
public function __construct(FormContractorInterface $formContractor, FormBuilderInterface $formBuilder, AdminInterface $admin)
{
parent::__construct($formContractor, $admin);
$this->formBuilder = $formBuilder;
}
/**
* {@inheritdoc}
*/
public function reorder(array $keys)
{
$this->admin->reorderFormGroup($this->getCurrentGroupName(), $keys);
return $this;
}
/**
* @param string $name
* @param string $type
* @param array $options
* @param array $fieldDescriptionOptions
*
* @return $this
*/
public function add($name, $type = null, array $options = [], array $fieldDescriptionOptions = [])
{
if ($this->apply !== null && !$this->apply) {
return $this;
}
if ($name instanceof FormBuilderInterface) {
$fieldName = $name->getName();
} else {
$fieldName = $name;
}
// "Dot" notation is not allowed as form name, but can be used as property path to access nested data.
if (!$name instanceof FormBuilderInterface && !isset($options['property_path'])) {
$options['property_path'] = $fieldName;
// fix the form name
$fieldName = $this->sanitizeFieldName($fieldName);
}
// change `collection` to `sonata_type_native_collection` form type to
// avoid BC break problems
if ($type === 'collection' || $type === 'Symfony\Component\Form\Extension\Core\Type\CollectionType') {
// the field name is used to preserve Symfony <2.8 compatibility, the FQCN should be used instead
$type = 'sonata_type_native_collection';
}
$label = $fieldName;
$group = $this->addFieldToCurrentGroup($label);
// Try to autodetect type
if ($name instanceof FormBuilderInterface && null === $type) {
$fieldDescriptionOptions['type'] = get_class($name->getType()->getInnerType());
}
if (!isset($fieldDescriptionOptions['type']) && is_string($type)) {
$fieldDescriptionOptions['type'] = $type;
}
if ($group['translation_domain'] && !isset($fieldDescriptionOptions['translation_domain'])) {
$fieldDescriptionOptions['translation_domain'] = $group['translation_domain'];
}
$fieldDescription = $this->admin->getModelManager()->getNewFieldDescriptionInstance(
$this->admin->getClass(),
$name instanceof FormBuilderInterface ? $name->getName() : $name,
$fieldDescriptionOptions
);
// Note that the builder var is actually the formContractor:
$this->builder->fixFieldDescription($this->admin, $fieldDescription, $fieldDescriptionOptions);
if ($fieldName != $name) {
$fieldDescription->setName($fieldName);
}
$this->admin->addFormFieldDescription($fieldName, $fieldDescription);
if ($name instanceof FormBuilderInterface) {
$this->formBuilder->add($name);
} else {
// Note that the builder var is actually the formContractor:
$options = array_replace_recursive($this->builder->getDefaultOptions($type, $fieldDescription), $options);
// be compatible with mopa if not installed, avoid generating an exception for invalid option
// force the default to false ...
if (!isset($options['label_render'])) {
$options['label_render'] = false;
}
if (!isset($options['label'])) {
$options['label'] = $this->admin->getLabelTranslatorStrategy()->getLabel($fieldDescription->getName(), 'form', 'label');
}
$help = null;
if (isset($options['help'])) {
$help = $options['help'];
unset($options['help']);
}
$this->formBuilder->add($fieldDescription->getName(), $type, $options);
if (null !== $help) {
$this->admin->getFormFieldDescription($fieldDescription->getName())->setHelp($help);
}
}
return $this;
}
/**
* {@inheritdoc}
*/
public function get($name)
{
$name = $this->sanitizeFieldName($name);
return $this->formBuilder->get($name);
}
/**
* {@inheritdoc}
*/
public function has($key)
{
$key = $this->sanitizeFieldName($key);
return $this->formBuilder->has($key);
}
/**
* {@inheritdoc}
*/
final public function keys()
{
return array_keys($this->formBuilder->all());
}
/**
* {@inheritdoc}
*/
public function remove($key)
{
$key = $this->sanitizeFieldName($key);
$this->admin->removeFormFieldDescription($key);
$this->admin->removeFieldFromFormGroup($key);
$this->formBuilder->remove($key);
return $this;
}
/**
* Removes a group.
*
* @param string $group The group to delete
* @param string $tab The tab the group belongs to, defaults to 'default'
* @param bool $deleteEmptyTab Whether or not the Tab should be deleted, when the deleted group leaves the tab empty after deletion
*
* @return $this
*/
public function removeGroup($group, $tab = 'default', $deleteEmptyTab = false)
{
$groups = $this->getGroups();
// When the default tab is used, the tabname is not prepended to the index in the group array
if ($tab !== 'default') {
$group = $tab.'.'.$group;
}
if (isset($groups[$group])) {
foreach ($groups[$group]['fields'] as $field) {
$this->remove($field);
}
}
unset($groups[$group]);
$tabs = $this->getTabs();
$key = array_search($group, $tabs[$tab]['groups']);
if (false !== $key) {
unset($tabs[$tab]['groups'][$key]);
}
if ($deleteEmptyTab && count($tabs[$tab]['groups']) == 0) {
unset($tabs[$tab]);
}
$this->setTabs($tabs);
$this->setGroups($groups);
return $this;
}
/**
* @return FormBuilderInterface
*/
public function getFormBuilder()
{
return $this->formBuilder;
}
/**
* @param string $name
* @param mixed $type
* @param array $options
*
* @return FormBuilderInterface
*/
public function create($name, $type = null, array $options = [])
{
return $this->formBuilder->create($name, $type, $options);
}
/**
* @param array $helps
*
* @return FormMapper
*/
public function setHelps(array $helps = [])
{
foreach ($helps as $name => $help) {
$this->addHelp($name, $help);
}
return $this;
}
/**
* @param $name
* @param $help
*
* @return FormMapper
*/
public function addHelp($name, $help)
{
if ($this->admin->hasFormFieldDescription($name)) {
$this->admin->getFormFieldDescription($name)->setHelp($help);
}
return $this;
}
/**
* Symfony default form class sadly can't handle
* form element with dots in its name (when data
* get bound, the default dataMapper is a PropertyPathMapper).
* So use this trick to avoid any issue.
*
* @param string $fieldName
*
* @return string
*/
protected function sanitizeFieldName($fieldName)
{
return str_replace(['__', '.'], ['____', '__'], $fieldName);
}
/**
* {@inheritdoc}
*/
protected function getGroups()
{
return $this->admin->getFormGroups();
}
/**
* {@inheritdoc}
*/
protected function setGroups(array $groups)
{
$this->admin->setFormGroups($groups);
}
/**
* {@inheritdoc}
*/
protected function getTabs()
{
return $this->admin->getFormTabs();
}
/**
* {@inheritdoc}
*/
protected function setTabs(array $tabs)
{
$this->admin->setFormTabs($tabs);
}
/**
* {@inheritdoc}
*/
protected function getName()
{
return 'form';
}
}

View File

@@ -0,0 +1,105 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* This type define an ACL matrix.
*
* @author Samuel Roze <samuel@sroze.io>
* @author Baptiste Meyer <baptiste@les-tilleuls.coop>
*/
class AclMatrixType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$aclValueType = $options['acl_value'] instanceof UserInterface ? 'user' : 'role';
$aclValueData = $options['acl_value'] instanceof UserInterface ? $options['acl_value']->getUsername() : $options['acl_value'];
$builder->add(
$aclValueType,
// NEXT_MAJOR: remove when dropping Symfony <2.8 support
method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')
? 'Symfony\Component\Form\Extension\Core\Type\HiddenType'
: 'hidden',
['data' => $aclValueData]
);
foreach ($options['permissions'] as $permission => $attributes) {
$builder->add(
$permission,
// NEXT_MAJOR: remove when dropping Symfony <2.8 support
method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')
? 'Symfony\Component\Form\Extension\Core\Type\CheckboxType'
: 'checkbox',
$attributes
);
}
}
/**
* NEXT_MAJOR: Remove method, when bumping requirements to SF 2.7+.
*
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$this->configureOptions($resolver);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setRequired([
'permissions',
'acl_value',
]);
if (method_exists($resolver, 'setDefined')) {
$resolver->setAllowedTypes('permissions', 'array');
$resolver->setAllowedTypes('acl_value', ['string', '\Symfony\Component\Security\Core\User\UserInterface']);
} else {
$resolver->setAllowedTypes([
'permissions' => 'array',
'acl_value' => ['string', '\Symfony\Component\Security\Core\User\UserInterface'],
]);
}
}
/**
* NEXT_MAJOR: Remove when dropping Symfony <2.8 support.
*
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'sonata_type_acl_matrix';
}
}

View File

@@ -0,0 +1,182 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\Type;
use Doctrine\Common\Collections\Collection;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
use Sonata\AdminBundle\Form\DataTransformer\ArrayToModelTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
use Symfony\Component\PropertyAccess\PropertyAccessor;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class AdminType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$admin = clone $this->getAdmin($options);
if ($admin->hasParentFieldDescription()) {
$admin->getParentFieldDescription()->setAssociationAdmin($admin);
}
if ($options['delete'] && $admin->hasAccess('delete')) {
if (!array_key_exists('translation_domain', $options['delete_options']['type_options'])) {
$options['delete_options']['type_options']['translation_domain'] = $admin->getTranslationDomain();
}
$builder->add('_delete', $options['delete_options']['type'], $options['delete_options']['type_options']);
}
// hack to make sure the subject is correctly set
// https://github.com/sonata-project/SonataAdminBundle/pull/2076
if ($builder->getData() === null) {
$p = new PropertyAccessor(false, true);
try {
$parentSubject = $admin->getParentFieldDescription()->getAdmin()->getSubject();
if ($parentSubject !== null && $parentSubject !== false) {
// for PropertyAccessor < 2.5
// NEXT_MAJOR: remove this code for old PropertyAccessor after dropping support for Symfony 2.3
if (!method_exists($p, 'isReadable')) {
$subjectCollection = $p->getValue(
$parentSubject,
$this->getFieldDescription($options)->getFieldName()
);
if ($subjectCollection instanceof Collection) {
$subject = $subjectCollection->get(trim($options['property_path'], '[]'));
}
} else {
// for PropertyAccessor >= 2.5
$subject = $p->getValue(
$parentSubject,
$options['property_path']
);
}
$builder->setData($subject);
}
} catch (NoSuchIndexException $e) {
// no object here
}
}
$admin->setSubject($builder->getData());
$admin->defineFormBuilder($builder);
$builder->addModelTransformer(new ArrayToModelTransformer($admin->getModelManager(), $admin->getClass()));
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['btn_add'] = $options['btn_add'];
$view->vars['btn_list'] = $options['btn_list'];
$view->vars['btn_delete'] = $options['btn_delete'];
$view->vars['btn_catalogue'] = $options['btn_catalogue'];
}
/**
* NEXT_MAJOR: Remove method, when bumping requirements to SF 2.7+.
*
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$this->configureOptions($resolver);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'delete' => function (Options $options) {
return $options['btn_delete'] !== false;
},
'delete_options' => [
// NEXT_MAJOR: Remove ternary (when requirement of Symfony is >= 2.8)
'type' => method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')
? 'Symfony\Component\Form\Extension\Core\Type\CheckboxType'
: 'checkbox',
'type_options' => [
'required' => false,
'mapped' => false,
],
],
'auto_initialize' => false,
'btn_add' => 'link_add',
'btn_list' => 'link_list',
'btn_delete' => 'link_delete',
'btn_catalogue' => 'SonataAdminBundle',
]);
}
/**
* NEXT_MAJOR: Remove when dropping Symfony <2.8 support.
*
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'sonata_type_admin';
}
/**
* @param array $options
*
* @return FieldDescriptionInterface
*
* @throws \RuntimeException
*/
protected function getFieldDescription(array $options)
{
if (!isset($options['sonata_field_description'])) {
throw new \RuntimeException('Please provide a valid `sonata_field_description` option');
}
return $options['sonata_field_description'];
}
/**
* @param array $options
*
* @return AdminInterface
*/
protected function getAdmin(array $options)
{
return $this->getFieldDescription($options)->getAssociationAdmin();
}
}

View File

@@ -0,0 +1,104 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class ChoiceFieldMaskType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$sanitizedMap = [];
foreach ($options['map'] as $value => $fieldNames) {
foreach ($fieldNames as $fieldName) {
$sanitizedMap[$value][] =
str_replace(['__', '.'], ['____', '__'], $fieldName);
}
}
$allFieldNames = call_user_func_array('array_merge', $sanitizedMap);
$allFieldNames = array_unique($allFieldNames);
$view->vars['all_fields'] = $allFieldNames;
$view->vars['map'] = $sanitizedMap;
$options['expanded'] = false;
parent::buildView($view, $form, $options);
}
/**
* NEXT_MAJOR: Remove method, when bumping requirements to SF 2.7+.
*
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$this->configureOptions($resolver);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
// NEXT_MAJOR: Remove conditional parent call when bumping requirements to SF 2.7+
if (method_exists('Symfony\Component\Form\AbstractType', 'configureOptions')) {
parent::configureOptions($resolver);
} else {
parent::setDefaultOptions($resolver);
}
$resolver->setDefaults([
'map' => [],
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
{
// NEXT_MAJOR: Remove ternary (when requirement of Symfony is >= 2.8)
return method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')
? 'Symfony\Component\Form\Extension\Core\Type\ChoiceType'
: 'choice';
}
/**
* NEXT_MAJOR: Remove when dropping Symfony <2.8 support.
*
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'sonata_type_choice_field_mask';
}
}

View File

@@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Sonata Project 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\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 <pulzarraider@gmail.com>
*/
class CollectionType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function getParent()
{
return
method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix') ?
'Symfony\Component\Form\Extension\Core\Type\CollectionType' :
'collection';
}
/**
* NEXT_MAJOR: Remove when dropping Symfony <2.8 support.
*
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'sonata_type_native_collection';
}
}

View File

@@ -0,0 +1,128 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\Type\Filter;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Translation\TranslatorInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class ChoiceType extends AbstractType
{
const TYPE_CONTAINS = 1;
const TYPE_NOT_CONTAINS = 2;
const TYPE_EQUAL = 3;
/**
* NEXT_MAJOR: remove this property.
*
* @deprecated since 3.5, to be removed with 4.0
*
* @var TranslatorInterface
*/
protected $translator;
/**
* @param TranslatorInterface $translator
*/
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* NEXT_MAJOR: Remove when dropping Symfony <2.8 support.
*
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'sonata_type_filter_choice';
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$choices = [
'label_type_contains' => self::TYPE_CONTAINS,
'label_type_not_contains' => self::TYPE_NOT_CONTAINS,
'label_type_equals' => self::TYPE_EQUAL,
];
$operatorChoices = [];
// NEXT_MAJOR: Remove first check (when requirement of Symfony is >= 2.8)
if ($options['operator_type'] !== 'hidden' && $options['operator_type'] !== 'Symfony\Component\Form\Extension\Core\Type\HiddenType') {
// NEXT_MAJOR: Remove (when requirement of Symfony is >= 2.7)
if (!method_exists('Symfony\Component\Form\AbstractType', 'configureOptions')) {
$choices = array_flip($choices);
foreach ($choices as $key => $value) {
$choices[$key] = $this->translator->trans($value, [], 'SonataAdminBundle');
}
} else {
$operatorChoices['choice_translation_domain'] = 'SonataAdminBundle';
// NEXT_MAJOR: Remove (when requirement of Symfony is >= 3.0)
if (method_exists('Symfony\Component\Form\FormTypeInterface', 'setDefaultOptions')) {
$operatorChoices['choices_as_values'] = true;
}
}
$operatorChoices['choices'] = $choices;
}
$builder
->add('type', $options['operator_type'], array_merge(['required' => false], $options['operator_options'], $operatorChoices))
->add('value', $options['field_type'], array_merge(['required' => false], $options['field_options']))
;
}
/**
* NEXT_MAJOR: Remove method, when bumping requirements to SF 2.7+.
*
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$this->configureOptions($resolver);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'field_type' => 'choice',
'field_options' => [],
'operator_type' => method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')
? 'Symfony\Component\Form\Extension\Core\Type\ChoiceType'
: 'choice', // NEXT_MAJOR: Remove ternary (when requirement of Symfony is >= 2.8)
'operator_options' => [],
]);
}
}

View File

@@ -0,0 +1,122 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\Type\Filter;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Translation\TranslatorInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class DateRangeType extends AbstractType
{
const TYPE_BETWEEN = 1;
const TYPE_NOT_BETWEEN = 2;
/**
* NEXT_MAJOR: remove this property.
*
* @deprecated since 3.5, to be removed with 4.0
*
* @var TranslatorInterface
*/
protected $translator;
/**
* @param TranslatorInterface $translator
*/
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* NEXT_MAJOR: Remove when dropping Symfony <2.8 support.
*
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'sonata_type_filter_date_range';
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$choices = [
'label_date_type_between' => self::TYPE_BETWEEN,
'label_date_type_not_between' => self::TYPE_NOT_BETWEEN,
];
$choiceOptions = [
'required' => false,
];
// NEXT_MAJOR: Remove (when requirement of Symfony is >= 2.7)
if (!method_exists('Symfony\Component\Form\AbstractType', 'configureOptions')) {
$choices = array_flip($choices);
foreach ($choices as $key => $value) {
$choices[$key] = $this->translator->trans($value, [], 'SonataAdminBundle');
}
} else {
$choiceOptions['choice_translation_domain'] = 'SonataAdminBundle';
// NEXT_MAJOR: Remove (when requirement of Symfony is >= 3.0)
if (method_exists('Symfony\Component\Form\FormTypeInterface', 'setDefaultOptions')) {
$choiceOptions['choices_as_values'] = true;
}
}
$choiceOptions['choices'] = $choices;
$builder
// NEXT_MAJOR: Remove ternary (when requirement of Symfony is >= 2.8)
->add('type', method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')
? 'Symfony\Component\Form\Extension\Core\Type\ChoiceType'
: 'choice', $choiceOptions)
->add('value', $options['field_type'], $options['field_options'])
;
}
// NEXT_MAJOR: Remove method, when bumping requirements to SF 2.7+
/**
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$this->configureOptions($resolver);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'field_type' => 'sonata_type_date_range',
'field_options' => ['format' => 'yyyy-MM-dd'],
]);
}
}

View File

@@ -0,0 +1,122 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\Type\Filter;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Translation\TranslatorInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class DateTimeRangeType extends AbstractType
{
const TYPE_BETWEEN = 1;
const TYPE_NOT_BETWEEN = 2;
/**
* NEXT_MAJOR: remove this property.
*
* @deprecated since 3.5, to be removed with 4.0
*
* @var TranslatorInterface
*/
protected $translator;
/**
* @param TranslatorInterface $translator
*/
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* NEXT_MAJOR: Remove when dropping Symfony <2.8 support.
*
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'sonata_type_filter_datetime_range';
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$choices = [
'label_date_type_between' => self::TYPE_BETWEEN,
'label_date_type_not_between' => self::TYPE_NOT_BETWEEN,
];
$choiceOptions = [
'required' => false,
];
// NEXT_MAJOR: Remove (when requirement of Symfony is >= 2.7)
if (!method_exists('Symfony\Component\Form\AbstractType', 'configureOptions')) {
$choices = array_flip($choices);
foreach ($choices as $key => $value) {
$choices[$key] = $this->translator->trans($value, [], 'SonataAdminBundle');
}
} else {
$choiceOptions['choice_translation_domain'] = 'SonataAdminBundle';
// NEXT_MAJOR: Remove (when requirement of Symfony is >= 3.0)
if (method_exists('Symfony\Component\Form\FormTypeInterface', 'setDefaultOptions')) {
$choiceOptions['choices_as_values'] = true;
}
}
$choiceOptions['choices'] = $choices;
$builder
// NEXT_MAJOR: Remove ternary (when requirement of Symfony is >= 2.8)
->add('type', method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')
? 'Symfony\Component\Form\Extension\Core\Type\ChoiceType'
: 'choice', $choiceOptions)
->add('value', $options['field_type'], $options['field_options'])
;
}
/**
* NEXT_MAJOR: Remove method, when bumping requirements to SF 2.7+.
*
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$this->configureOptions($resolver);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'field_type' => 'sonata_type_datetime_range',
'field_options' => ['date_format' => 'yyyy-MM-dd'],
]);
}
}

View File

@@ -0,0 +1,138 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\Type\Filter;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Translation\TranslatorInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class DateTimeType extends AbstractType
{
const TYPE_GREATER_EQUAL = 1;
const TYPE_GREATER_THAN = 2;
const TYPE_EQUAL = 3;
const TYPE_LESS_EQUAL = 4;
const TYPE_LESS_THAN = 5;
const TYPE_NULL = 6;
const TYPE_NOT_NULL = 7;
/**
* NEXT_MAJOR: remove this property.
*
* @deprecated since 3.5, to be removed with 4.0
*
* @var TranslatorInterface
*/
protected $translator;
/**
* @param TranslatorInterface $translator
*/
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* NEXT_MAJOR: Remove when dropping Symfony <2.8 support.
*
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'sonata_type_filter_datetime';
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$choices = [
'label_date_type_equal' => self::TYPE_EQUAL,
'label_date_type_greater_equal' => self::TYPE_GREATER_EQUAL,
'label_date_type_greater_than' => self::TYPE_GREATER_THAN,
'label_date_type_less_equal' => self::TYPE_LESS_EQUAL,
'label_date_type_less_than' => self::TYPE_LESS_THAN,
'label_date_type_null' => self::TYPE_NULL,
'label_date_type_not_null' => self::TYPE_NOT_NULL,
];
$choiceOptions = [
'required' => false,
];
// NEXT_MAJOR: Remove (when requirement of Symfony is >= 2.7)
if (!method_exists('Symfony\Component\Form\AbstractType', 'configureOptions')) {
$choices = array_flip($choices);
foreach ($choices as $key => $value) {
$choices[$key] = $this->translator->trans($value, [], 'SonataAdminBundle');
}
} else {
$choiceOptions['choice_translation_domain'] = 'SonataAdminBundle';
// NEXT_MAJOR: Remove (when requirement of Symfony is >= 3.0)
if (method_exists('Symfony\Component\Form\FormTypeInterface', 'setDefaultOptions')) {
$choiceOptions['choices_as_values'] = true;
}
}
$choiceOptions['choices'] = $choices;
$builder
// NEXT_MAJOR: Remove ternary (when requirement of Symfony is >= 2.8)
->add('type', method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')
? 'Symfony\Component\Form\Extension\Core\Type\ChoiceType'
: 'choice', $choiceOptions)
->add('value', $options['field_type'], array_merge(['required' => false], $options['field_options']))
;
}
/**
* NEXT_MAJOR: Remove method, when bumping requirements to SF 2.7+.
*
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$this->configureOptions($resolver);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'field_type' => 'datetime',
'field_options' => ['date_format' => 'yyyy-MM-dd'],
]);
}
}

View File

@@ -0,0 +1,138 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\Type\Filter;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Optionsresolver\OptionsResolverInterface;
use Symfony\Component\Translation\TranslatorInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class DateType extends AbstractType
{
const TYPE_GREATER_EQUAL = 1;
const TYPE_GREATER_THAN = 2;
const TYPE_EQUAL = 3;
const TYPE_LESS_EQUAL = 4;
const TYPE_LESS_THAN = 5;
const TYPE_NULL = 6;
const TYPE_NOT_NULL = 7;
/**
* NEXT_MAJOR: remove this property.
*
* @deprecated since 3.5, to be removed with 4.0
*
* @var TranslatorInterface
*/
protected $translator;
/**
* @param TranslatorInterface $translator
*/
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* NEXT_MAJOR: Remove when dropping Symfony <2.8 support.
*
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'sonata_type_filter_date';
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$choices = [
'label_date_type_equal' => self::TYPE_EQUAL,
'label_date_type_greater_equal' => self::TYPE_GREATER_EQUAL,
'label_date_type_greater_than' => self::TYPE_GREATER_THAN,
'label_date_type_less_equal' => self::TYPE_LESS_EQUAL,
'label_date_type_less_than' => self::TYPE_LESS_THAN,
'label_date_type_null' => self::TYPE_NULL,
'label_date_type_not_null' => self::TYPE_NOT_NULL,
];
$choiceOptions = [
'required' => false,
];
// NEXT_MAJOR: Remove (when requirement of Symfony is >= 2.7)
if (!method_exists('Symfony\Component\Form\AbstractType', 'configureOptions')) {
$choices = array_flip($choices);
foreach ($choices as $key => $value) {
$choices[$key] = $this->translator->trans($value, [], 'SonataAdminBundle');
}
} else {
$choiceOptions['choice_translation_domain'] = 'SonataAdminBundle';
// NEXT_MAJOR: Remove (when requirement of Symfony is >= 3.0)
if (method_exists('Symfony\Component\Form\FormTypeInterface', 'setDefaultOptions')) {
$choiceOptions['choices_as_values'] = true;
}
}
$choiceOptions['choices'] = $choices;
$builder
// NEXT_MAJOR: Remove ternary (when requirement of Symfony is >= 2.8)
->add('type', method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')
? 'Symfony\Component\Form\Extension\Core\Type\ChoiceType'
: 'choice', $choiceOptions)
->add('value', $options['field_type'], array_merge(['required' => false], $options['field_options']))
;
}
/**
* NEXT_MAJOR: Remove method, when bumping requirements to SF 2.7+.
*
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$this->configureOptions($resolver);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'field_type' => 'date',
'field_options' => ['date_format' => 'yyyy-MM-dd'],
]);
}
}

View File

@@ -0,0 +1,79 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\Type\Filter;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class DefaultType extends AbstractType
{
/**
* NEXT_MAJOR: Remove when dropping Symfony <2.8 support.
*
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'sonata_type_filter_default';
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('type', $options['operator_type'], array_merge(['required' => false], $options['operator_options']))
->add('value', $options['field_type'], array_merge(['required' => false], $options['field_options']))
;
}
/**
* NEXT_MAJOR: Remove method, when bumping requirements to SF 2.7+.
*
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$this->configureOptions($resolver);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'operator_type' => method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')
? 'Symfony\Component\Form\Extension\Core\Type\HiddenType'
: 'hidden', // NEXT_MAJOR: Remove ternary (when requirement of Symfony is >= 2.8)
'operator_options' => [],
'field_type' => method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')
? 'Symfony\Component\Form\Extension\Core\Type\TextType'
: 'text', // NEXT_MAJOR: Remove ternary (when requirement of Symfony is >= 2.8)
'field_options' => [],
]);
}
}

View File

@@ -0,0 +1,133 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\Type\Filter;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Translation\TranslatorInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class NumberType extends AbstractType
{
const TYPE_GREATER_EQUAL = 1;
const TYPE_GREATER_THAN = 2;
const TYPE_EQUAL = 3;
const TYPE_LESS_EQUAL = 4;
const TYPE_LESS_THAN = 5;
/**
* NEXT_MAJOR: remove this property.
*
* @deprecated since 3.5, to be removed with 4.0
*
* @var TranslatorInterface
*/
protected $translator;
/**
* @param TranslatorInterface $translator
*/
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* NEXT_MAJOR: Remove when dropping Symfony <2.8 support.
*
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'sonata_type_filter_number';
}
/**
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$choices = [
'label_type_equal' => self::TYPE_EQUAL,
'label_type_greater_equal' => self::TYPE_GREATER_EQUAL,
'label_type_greater_than' => self::TYPE_GREATER_THAN,
'label_type_less_equal' => self::TYPE_LESS_EQUAL,
'label_type_less_than' => self::TYPE_LESS_THAN,
];
$choiceOptions = [
'required' => false,
];
// NEXT_MAJOR: Remove (when requirement of Symfony is >= 2.7)
if (!method_exists('Symfony\Component\Form\AbstractType', 'configureOptions')) {
$choices = array_flip($choices);
foreach ($choices as $key => $value) {
$choices[$key] = $this->translator->trans($value, [], 'SonataAdminBundle');
}
} else {
$choiceOptions['choice_translation_domain'] = 'SonataAdminBundle';
// NEXT_MAJOR: Remove (when requirement of Symfony is >= 3.0)
if (method_exists('Symfony\Component\Form\FormTypeInterface', 'setDefaultOptions')) {
$choiceOptions['choices_as_values'] = true;
}
}
$choiceOptions['choices'] = $choices;
$builder
// NEXT_MAJOR: Remove ternary (when requirement of Symfony is >= 2.8)
->add('type', method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')
? 'Symfony\Component\Form\Extension\Core\Type\ChoiceType'
: 'choice', $choiceOptions)
->add('value', $options['field_type'], array_merge(['required' => false], $options['field_options']))
;
}
/**
* NEXT_MAJOR: Remove method, when bumping requirements to SF 2.7+.
*
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$this->configureOptions($resolver);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'field_type' => 'number',
'field_options' => [],
]);
}
}

View File

@@ -0,0 +1,183 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\Type;
use Sonata\AdminBundle\Form\DataTransformer\ModelToIdPropertyTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
/**
* This type defines a standard text field with autocomplete feature.
*
* @author Andrej Hudec <pulzarraider@gmail.com>
* @author Florent Denis <dflorent.pokap@gmail.com>
*/
class ModelAutocompleteType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer(new ModelToIdPropertyTransformer($options['model_manager'], $options['class'], $options['property'], $options['multiple'], $options['to_string_callback']), true);
$builder->setAttribute('property', $options['property']);
$builder->setAttribute('callback', $options['callback']);
$builder->setAttribute('minimum_input_length', $options['minimum_input_length']);
$builder->setAttribute('items_per_page', $options['items_per_page']);
$builder->setAttribute('req_param_name_page_number', $options['req_param_name_page_number']);
$builder->setAttribute(
'disabled',
$options['disabled']
// NEXT_MAJOR: Remove this when bumping Symfony constraint to 2.8+
|| (array_key_exists('read_only', $options) && $options['read_only'])
);
$builder->setAttribute('to_string_callback', $options['to_string_callback']);
$builder->setAttribute('target_admin_access_action', $options['target_admin_access_action']);
if ($options['multiple']) {
$resizeListener = new ResizeFormListener(
// NEXT_MAJOR: Remove ternary and keep 'Symfony\Component\Form\Extension\Core\Type\HiddenType'
// (when requirement of Symfony is >= 2.8)
method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')
? 'Symfony\Component\Form\Extension\Core\Type\HiddenType'
: 'hidden',
[],
true,
true,
true
);
$builder->addEventSubscriber($resizeListener);
}
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['admin_code'] = $options['admin_code'];
$view->vars['placeholder'] = $options['placeholder'];
$view->vars['multiple'] = $options['multiple'];
$view->vars['minimum_input_length'] = $options['minimum_input_length'];
$view->vars['items_per_page'] = $options['items_per_page'];
$view->vars['width'] = $options['width'];
// ajax parameters
$view->vars['url'] = $options['url'];
$view->vars['route'] = $options['route'];
$view->vars['req_params'] = $options['req_params'];
$view->vars['req_param_name_search'] = $options['req_param_name_search'];
$view->vars['req_param_name_page_number'] = $options['req_param_name_page_number'];
$view->vars['req_param_name_items_per_page'] = $options['req_param_name_items_per_page'];
$view->vars['quiet_millis'] = $options['quiet_millis'];
$view->vars['cache'] = $options['cache'];
// CSS classes
$view->vars['container_css_class'] = $options['container_css_class'];
$view->vars['dropdown_css_class'] = $options['dropdown_css_class'];
$view->vars['dropdown_item_css_class'] = $options['dropdown_item_css_class'];
$view->vars['dropdown_auto_width'] = $options['dropdown_auto_width'];
// template
$view->vars['template'] = $options['template'];
$view->vars['context'] = $options['context'];
}
/**
* NEXT_MAJOR: Remove method, when bumping requirements to SF 2.7+.
*
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$this->configureOptions($resolver);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$compound = function (Options $options) {
return $options['multiple'];
};
$resolver->setDefaults([
'attr' => [],
'compound' => $compound,
'model_manager' => null,
'class' => null,
'admin_code' => null,
'callback' => null,
'multiple' => false,
'width' => '',
'context' => '',
'placeholder' => '',
'minimum_input_length' => 3, //minimum 3 chars should be typed to load ajax data
'items_per_page' => 10, //number of items per page
'quiet_millis' => 100,
'cache' => false,
'to_string_callback' => null,
// ajax parameters
'url' => '',
'route' => ['name' => 'sonata_admin_retrieve_autocomplete_items', 'parameters' => []],
'req_params' => [],
'req_param_name_search' => 'q',
'req_param_name_page_number' => '_page',
'req_param_name_items_per_page' => '_per_page',
// security
'target_admin_access_action' => 'list',
// CSS classes
'container_css_class' => '',
'dropdown_css_class' => '',
'dropdown_item_css_class' => '',
'dropdown_auto_width' => false,
'template' => 'SonataAdminBundle:Form/Type:sonata_type_model_autocomplete.html.twig',
]);
$resolver->setRequired(['property']);
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'sonata_type_model_autocomplete';
}
/**
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
}

View File

@@ -0,0 +1,86 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\Type;
use Sonata\AdminBundle\Form\DataTransformer\ModelToIdTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
/**
* This type define a standard hidden field, that stored id to a object.
*
* @author Andrej Hudec <pulzarraider@gmail.com>
*/
class ModelHiddenType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->addViewTransformer(new ModelToIdTransformer($options['model_manager'], $options['class']), true)
;
}
/**
* NEXT_MAJOR: Remove method, when bumping requirements to SF 2.7+.
*
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$this->configureOptions($resolver);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'model_manager' => null,
'class' => null,
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
{
// NEXT_MAJOR: Remove ternary (when requirement of Symfony is >= 2.8)
return method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')
? 'Symfony\Component\Form\Extension\Core\Type\HiddenType'
: 'hidden';
}
/**
* NEXT_MAJOR: Remove when dropping Symfony <2.8 support.
*
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'sonata_type_model_hidden';
}
}

View File

@@ -0,0 +1,122 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\Type;
use Sonata\AdminBundle\Form\DataTransformer\ModelToIdTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
/**
* This type can be used to select one associated model from a list.
*
* The associated model must be in a single-valued association relationship (e.g many-to-one)
* with the model currently edited in the parent form.
* The associated model must have an admin class registered.
*
* The selected model's identifier is rendered in an hidden input.
*
* When a model is selected, a short description is displayed by the widget.
* This description can be customized by overriding the associated admin's
* `short_object_description` template and/or overriding it's `toString` method.
*
* The widget also provides three action buttons:
* - a button to open the associated admin list view in a dialog,
* in order to select an associated model.
* - a button to open the associated admin create form in a dialog,
* in order to create and select an associated model.
* - a button to unlink the associated model, if any.
*/
class ModelListType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->resetViewTransformers()
->addViewTransformer(new ModelToIdTransformer($options['model_manager'], $options['class']));
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
if (isset($view->vars['sonata_admin'])) {
// set the correct edit mode
$view->vars['sonata_admin']['edit'] = 'list';
}
$view->vars['btn_add'] = $options['btn_add'];
$view->vars['btn_list'] = $options['btn_list'];
$view->vars['btn_delete'] = $options['btn_delete'];
$view->vars['btn_catalogue'] = $options['btn_catalogue'];
}
/**
* NEXT_MAJOR: Remove method, when bumping requirements to SF 2.7+.
*
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$this->configureOptions($resolver);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'model_manager' => null,
'class' => null,
'btn_add' => 'link_add',
'btn_list' => 'link_list',
'btn_delete' => 'link_delete',
'btn_catalogue' => 'SonataAdminBundle',
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
{
// NEXT_MAJOR: Remove ternary (when requirement of Symfony is >= 2.8)
return method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')
? 'Symfony\Component\Form\Extension\Core\Type\TextType'
: 'text';
}
/**
* NEXT_MAJOR: Remove when dropping Symfony <2.8 support.
*
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'sonata_type_model_list';
}
}

View File

@@ -0,0 +1,83 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\Type;
use Sonata\AdminBundle\Form\DataTransformer\ModelToIdTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
/**
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class ModelReferenceType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addModelTransformer(new ModelToIdTransformer($options['model_manager'], $options['class']));
}
/**
* NEXT_MAJOR: Remove method, when bumping requirements to SF 2.7+.
*
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$this->configureOptions($resolver);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'compound' => false,
'model_manager' => null,
'class' => null,
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
{
// NEXT_MAJOR: Remove ternary (when requirement of Symfony is >= 2.8)
return method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')
? 'Symfony\Component\Form\Extension\Core\Type\TextType'
: 'text';
}
/**
* NEXT_MAJOR: Remove when dropping Symfony <2.8 support.
*
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'sonata_type_model_reference';
}
}

View File

@@ -0,0 +1,199 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\Type;
use Sonata\AdminBundle\Form\ChoiceList\ModelChoiceList;
use Sonata\AdminBundle\Form\ChoiceList\ModelChoiceLoader;
use Sonata\AdminBundle\Form\DataTransformer\LegacyModelsToArrayTransformer;
use Sonata\AdminBundle\Form\DataTransformer\ModelsToArrayTransformer;
use Sonata\AdminBundle\Form\DataTransformer\ModelToIdTransformer;
use Sonata\AdminBundle\Form\EventListener\MergeCollectionListener;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
/**
* This type define a standard select input with a + sign to add new associated object.
*
* @author Thomas Rabaix <thomas.rabaix@sonata-project.org>
*/
class ModelType extends AbstractType
{
/**
* @var PropertyAccessorInterface
*/
protected $propertyAccessor;
public function __construct(PropertyAccessorInterface $propertyAccessor)
{
$this->propertyAccessor = $propertyAccessor;
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($options['multiple']) {
if (array_key_exists('choice_loader', $options) && $options['choice_loader'] !== null) { // SF2.7+
$builder->addViewTransformer(new ModelsToArrayTransformer(
$options['model_manager'],
$options['class']), true);
} else {
$builder->addViewTransformer(new LegacyModelsToArrayTransformer($options['choice_list']), true);
}
$builder
->addEventSubscriber(new MergeCollectionListener($options['model_manager']))
;
} else {
$builder
->addViewTransformer(new ModelToIdTransformer($options['model_manager'], $options['class']), true)
;
}
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['btn_add'] = $options['btn_add'];
$view->vars['btn_list'] = $options['btn_list'];
$view->vars['btn_delete'] = $options['btn_delete'];
$view->vars['btn_catalogue'] = $options['btn_catalogue'];
}
/**
* NEXT_MAJOR: Remove method, when bumping requirements to SF 2.7+.
*
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$this->configureOptions($resolver);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$options = [];
$propertyAccessor = $this->propertyAccessor;
if (interface_exists('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface')) { // SF2.7+
$options['choice_loader'] = function (Options $options, $previousValue) use ($propertyAccessor) {
if ($previousValue && count($choices = $previousValue->getChoices())) {
return $choices;
}
return new ModelChoiceLoader(
$options['model_manager'],
$options['class'],
$options['property'],
$options['query'],
$options['choices'],
$propertyAccessor
);
};
// NEXT_MAJOR: Remove this when dropping support for SF 2.8
if (method_exists('Symfony\Component\Form\FormTypeInterface', 'setDefaultOptions')) {
$options['choices_as_values'] = true;
}
} else {
$options['choice_list'] = function (Options $options, $previousValue) use ($propertyAccessor) {
if ($previousValue && count($choices = $previousValue->getChoices())) {
return $choices;
}
return new ModelChoiceList(
$options['model_manager'],
$options['class'],
$options['property'],
$options['query'],
$options['choices'],
$propertyAccessor
);
};
}
$resolver->setDefaults(array_merge($options, [
'compound' => function (Options $options) {
if (isset($options['multiple']) && $options['multiple']) {
if (isset($options['expanded']) && $options['expanded']) {
//checkboxes
return true;
}
//select tag (with multiple attribute)
return false;
}
if (isset($options['expanded']) && $options['expanded']) {
//radio buttons
return true;
}
//select tag
return false;
},
'template' => 'choice',
'multiple' => false,
'expanded' => false,
'model_manager' => null,
'class' => null,
'property' => null,
'query' => null,
'choices' => [],
'preferred_choices' => [],
'btn_add' => 'link_add',
'btn_list' => 'link_list',
'btn_delete' => 'link_delete',
'btn_catalogue' => 'SonataAdminBundle',
]));
}
/**
* {@inheritdoc}
*/
public function getParent()
{
// NEXT_MAJOR: Remove ternary (when requirement of Symfony is >= 2.8)
return method_exists('Symfony\Component\Form\AbstractType', 'getBlockPrefix')
? 'Symfony\Component\Form\Extension\Core\Type\ChoiceType'
: 'choice';
}
/**
* NEXT_MAJOR: Remove when dropping Symfony <2.8 support.
*
* {@inheritdoc}
*/
public function getName()
{
return $this->getBlockPrefix();
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'sonata_type_model';
}
}

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the Sonata Project 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\AdminBundle\Form\Type;
@trigger_error(
'The '.__NAMESPACE__.'\ModelTypeList class is deprecated since version 3.5 and will be removed in 4.0.'
.' Use '.__NAMESPACE__.'\ModelListType instead.',
E_USER_DEPRECATED
);
/**
* This type is used to render an hidden input text and 3 links
* - an add form modal
* - a list modal to select the targeted entities
* - a clear selection link.
*
* NEXT_MAJOR: remove this class.
*
* @deprecated since version 3.5, to be removed in 4.0. Use ModelListType instead
*/
class ModelTypeList extends ModelListType
{
}

Some files were not shown because too many files have changed in this diff Show More