diff --git a/Action/Action.php b/Action/Action.php index e89ec8aed..09b6d87e7 100644 --- a/Action/Action.php +++ b/Action/Action.php @@ -3,6 +3,7 @@ namespace LAG\AdminBundle\Action; use LAG\AdminBundle\Action\Configuration\ActionConfiguration; +use LAG\AdminBundle\Admin\AdminInterface; use LAG\AdminBundle\Admin\Filter; use LAG\AdminBundle\Field\Field; @@ -151,4 +152,15 @@ public function getConfiguration() { return $this->configuration; } + + /** + * Return true if the pagination is required for this action. Only action with a "multiple" load strategy require + * pagination. + * + * @return bool + */ + public function isPaginationRequired() + { + return $this->configuration->getParameter('load_strategy') === AdminInterface::LOAD_STRATEGY_MULTIPLE; + } } diff --git a/Action/ActionInterface.php b/Action/ActionInterface.php index 61757b89c..6fd0ec764 100644 --- a/Action/ActionInterface.php +++ b/Action/ActionInterface.php @@ -65,4 +65,11 @@ public function setFields($fields); * @param Field $field */ public function addField(Field $field); + + /** + * Return true if the Action requires pagination. + * + * @return bool + */ + public function isPaginationRequired(); } diff --git a/Action/Configuration/ActionConfiguration.php b/Action/Configuration/ActionConfiguration.php index 0160571d6..31d1e5607 100644 --- a/Action/Configuration/ActionConfiguration.php +++ b/Action/Configuration/ActionConfiguration.php @@ -100,7 +100,8 @@ public function configureOptions(OptionsResolver $resolver) 'xls' ]); - // entity will be retrived with this order. It should be an array of field/order mapping + // retrieved entities are sorted according to this option it should be an array indexed by the field name + // with its order as a value $resolver ->setDefault('order', []) ->setAllowedTypes('order', 'array'); diff --git a/Admin/Admin.php b/Admin/Admin.php index c9f1767fb..374ac16ff 100644 --- a/Admin/Admin.php +++ b/Admin/Admin.php @@ -196,6 +196,11 @@ public function create() ->entities ->add($entity); + // inform the user that the entity is created + $this + ->messageHandler + ->handleSuccess($this->generateMessageTranslationKey('created')); + return $entity; } @@ -212,16 +217,16 @@ public function save() ->dataProvider ->save($entity); } - // inform user everything went fine + // inform the user that the entity is saved $this ->messageHandler - ->handleSuccess('lag.admin.'.$this->name.'.saved'); + ->handleSuccess($this->generateMessageTranslationKey('saved')); $success = true; } catch (Exception $e) { $this ->messageHandler ->handleError( - 'lag.admin.saved_errors', + $this->generateMessageTranslationKey('lag.admin.saved_errors'), "An error has occurred while saving an entity : {$e->getMessage()}, stackTrace: {$e->getTraceAsString()}" ); $success = false; @@ -242,16 +247,16 @@ public function remove() ->dataProvider ->remove($entity); } - // inform user everything went fine + // inform the user that the entity is removed $this ->messageHandler - ->handleSuccess('lag.admin.'.$this->name.'.deleted'); + ->handleSuccess($this->generateMessageTranslationKey('deleted')); $success = true; } catch (Exception $e) { $this ->messageHandler ->handleError( - 'lag.admin.deleted_errors', + $this->generateMessageTranslationKey('lag.admin.deleted_errors'), "An error has occurred while deleting an entity : {$e->getMessage()}, stackTrace: {$e->getTraceAsString()} " ); $success = false; @@ -272,10 +277,10 @@ public function generateRouteName($actionName) { if (!array_key_exists($actionName, $this->getConfiguration()->getParameter('actions'))) { throw new Exception( - sprintf('Invalid action name %s for admin %s (available action are: %s)', - $actionName, - $this->getName(), - implode(', ', $this->getActionNames())) + sprintf('Invalid action name %s for admin %s (available action are: %s)', + $actionName, + $this->getName(), + implode(', ', $this->getActionNames())) ); } // get routing name pattern @@ -298,13 +303,16 @@ public function generateRouteName($actionName) */ public function load(array $criteria, $orderBy = [], $limit = 25, $offset = 1) { - $pager = $this + $actionConfiguration = $this + ->getCurrentAction() + ->getConfiguration(); + $pager = $actionConfiguration->getParameter('pager'); + $requirePagination = $this ->getCurrentAction() - ->getConfiguration() - ->getParameter('pager'); + ->isPaginationRequired(); - if ($pager == 'pagerfanta') { - // adapter to pager fanta + if ($pager == 'pagerfanta' && $requirePagination) { + // adapter to pagerfanta $adapter = new PagerFantaAdminAdapter($this->dataProvider, $criteria, $orderBy); // create pager $this->pager = new Pagerfanta($adapter); @@ -315,6 +323,10 @@ public function load(array $criteria, $orderBy = [], $limit = 25, $offset = 1) ->pager ->getCurrentPageResults(); } else { + // if the current action should retrieve only one entity, the offset should be zero + if ($actionConfiguration->getParameter('load_strategy') !== AdminInterface::LOAD_STRATEGY_MULTIPLE) { + $offset = 0; + } $entities = $this ->dataProvider ->findBy($criteria, $orderBy, $limit, $offset); @@ -480,7 +492,7 @@ public function isCurrentActionDefined() } /** - * Return admin configuration object + * Return admin configuration object. * * @return AdminConfiguration */ @@ -488,4 +500,19 @@ public function getConfiguration() { return $this->configuration; } + + /** + * Return a translation key for a message according to the Admin's translation pattern. + * + * @param string $message + * @return string + */ + protected function generateMessageTranslationKey($message) + { + return $this->getTranslationKey( + $this->configuration->getParameter('translation_pattern'), + $message, + $this->name + ); + } } diff --git a/Admin/Behaviors/AdminTrait.php b/Admin/Behaviors/AdminTrait.php index 0aa3c102c..17f6fe72f 100644 --- a/Admin/Behaviors/AdminTrait.php +++ b/Admin/Behaviors/AdminTrait.php @@ -9,6 +9,7 @@ trait AdminTrait use EntityLabelTrait { getEntityLabel as parentEntityLabel; } + use TranslationKeyTrait; /** * @var Pagerfanta diff --git a/Application/Configuration/ApplicationConfiguration.php b/Application/Configuration/ApplicationConfiguration.php index 233cf6a3f..463805bd6 100644 --- a/Application/Configuration/ApplicationConfiguration.php +++ b/Application/Configuration/ApplicationConfiguration.php @@ -40,7 +40,7 @@ public function configureOptions(OptionsResolver $resolver) // main base template // as bundles are not loaded when reading the configuration, the kernel locateResources will always failed. - // So we must not check resource existance here. + // So we must not check resource existence here. $resolver->setDefault('base_template', 'LAGAdminBundle::admin.layout.html.twig'); $resolver->setAllowedTypes('base_template', 'string'); @@ -64,13 +64,38 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setAllowedTypes('string_length_truncate', 'string'); // routing configuration (route name pattern and url name pattern) + $this->setRoutingOptions($resolver); + + // translation configuration + $this->setTranslationOptions($resolver); + + // maximum number of elements displayed + $resolver->setDefault('max_per_page', 25); + $resolver->setAllowedTypes('max_per_page', 'integer'); + + // admin field type mapping + $this->setFieldsOptions($resolver); + } + + /** + * @param OptionsResolver $resolver + */ + protected function setRoutingOptions(OptionsResolver $resolver) + { $resolver->setDefault('routing', [ 'url_pattern' => '/{admin}/{action}', - 'name_pattern' => 'lag.admin.{admin}', + 'name_pattern' => 'lag.admin.{admin}.{action}', ]); $resolver->setAllowedTypes('routing', 'array'); $resolver->setNormalizer('routing', function(Options $options, $value) { + if (!array_key_exists('url_pattern', $value)) { + $value['url_pattern'] = '/{admin}/{action}'; + } + if (!array_key_exists('name_pattern', $value)) { + $value['name_pattern'] = 'lag.admin.{admin}.{action}'; + } + // url pattern should contain {admin} and {action} token $urlPattern = $value['url_pattern']; @@ -87,11 +112,19 @@ public function configureOptions(OptionsResolver $resolver) if (strstr($namePattern, '{admin}') === false) { throw new InvalidOptionsException('Admin routing configuration pattern name should contains the {admin} placeholder'); } + if (strstr($namePattern, '{action}') === false) { + throw new InvalidOptionsException('Admin routing configuration pattern name should contains the {action} placeholder'); + } return $value; }); + } - // translation configuration + /** + * @param OptionsResolver $resolver + */ + protected function setTranslationOptions(OptionsResolver $resolver) + { $resolver->setDefault('translation', [ 'enabled' => true, 'pattern' => 'lag.admin.{key}' @@ -99,6 +132,10 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setAllowedTypes('translation', 'array'); $resolver->setNormalizer('translation', function(Options $options, $value) { + if (!array_key_exists('enabled', $value)) { + throw new InvalidOptionsException('Admin translation enabled parameter should be defined'); + } + if (!is_bool($value['enabled'])) { throw new InvalidOptionsException('Admin translation enabled parameter should be a boolean'); } @@ -113,12 +150,13 @@ public function configureOptions(OptionsResolver $resolver) return $value; }); + } - // maximum number of elements displayed - $resolver->setDefault('max_per_page', 25); - $resolver->setAllowedTypes('max_per_page', 'integer'); - - // admin field type mapping + /** + * @param OptionsResolver $resolver + */ + protected function setFieldsOptions(OptionsResolver $resolver) + { $defaultMapping = [ Field::TYPE_STRING => StringField::class, Field::TYPE_ARRAY => Field\ArrayField::class, diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 69c55829e..8bfb13623 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -37,6 +37,9 @@ public function getAdminsConfigurationNode() $node = $builder->root('admins'); $node + // useAttributeAsKey() method will preserve keys when multiple configurations files are used and then avoid + // admin not found by configuration override + ->useAttributeAsKey('name') ->prototype('array') ->children() ->scalarNode('entity')->end() diff --git a/Form/Type/DateTimePickerType.php b/Form/Type/DateTimePickerType.php index d20427a65..6c26843f4 100644 --- a/Form/Type/DateTimePickerType.php +++ b/Form/Type/DateTimePickerType.php @@ -4,6 +4,7 @@ use LAG\AdminBundle\Application\Configuration\ApplicationConfiguration; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\OptionsResolver\OptionsResolver; class DateTimePickerType extends AbstractType @@ -24,9 +25,12 @@ public function configureOptions(OptionsResolver $resolver) ]); } + /** + * @return string + */ public function getParent() { - return 'datetime'; + return DateTimeType::class; } public function getName() diff --git a/README.md b/README.md index 439a9bac8..a650d5cc1 100644 --- a/README.md +++ b/README.md @@ -1,183 +1,211 @@ -# AdminBundle +[![Build Status](https://travis-ci.org/larriereguichet/AdminBundle.svg?branch=master)](https://travis-ci.org/larriereguichet/AdminBundle) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/larriereguichet/AdminBundle/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/larriereguichet/AdminBundle/?branch=master) +[![SensioLabsInsight](https://insight.sensiolabs.com/projects/c8e28654-44c7-46f3-9450-497e37bda3d0/mini.png)](https://insight.sensiolabs.com/projects/c8e28654-44c7-46f3-9450-497e37bda3d0) + -AdminBundle allows you to create flexible and robust management application, using a simple configuration, -for your Symfony application. +# AdminBundle -Development and Documentation are in progress +The AdminBundle let you creates a **flexible** and **robust backoffice** on any Symfony application, with simple `yml` configuration. -Testing : -branch dev [![Build Status](https://travis-ci.org/larriereguichet/AdminBundle.svg?branch=dev)](https://travis-ci.org/larriereguichet/AdminBundle) -coverage [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/larriereguichet/AdminBundle/badges/quality-score.png?b=dev)](https://scrutinizer-ci.com/g/larriereguichet/AdminBundle/?branch=dev) +It eases the handling of CRUD views by configuring `Admin` objects on top of your Doctrine entities. Each `Admin` has one or many `Action`. +By default, the four actions of a classical CRUD are available (`create`, `edit`, `delete` and `list`) and the creation of new actions is painless. -## Features +If you require *more flexibility*, you can easily override any part of the process (repositories, controllers, views...). +The purpose of the bundle is to provide an Admin interface with default configuration, and allows the user to add his +specific need where he wants, and allow to implements any specific needs without any hassles. -Current version: 0.2 -* Provides dynamics CRUD for your entities (not generated) -* Simple configuration in yml (look alike symfony1 generators.yml files) -* Basic permissions -* Built-in pager (using PagerFanta) -* List export in multiple formats : html, pdf, xls, csv, json (using EE/DataExporter) -* Fully customizable (use your own controllers, managers or templates) +## Features +Version 0.4 : +* Dynamic CRUD for your entities (no code generation) +* Simple configuration in yml (look alike symfony1 generators.yml syntax) +* List with pagination, sorting and batch remove (filters are coming) +* Full translated +* Main and left menu integration +* Fully customizable (use your own controllers, data providers or templates) +* Bootstrap integration (can be disabled or override) -# Installation +## Installation ``` -php composer.phar require lag/adminbundle + composer require lag/adminbundle ``` -# Getting started +AdminBundle rely on KnpMenuBundle to handle menus and on WhiteOctoberPagerfantaBundle to handle list pagination. If you +want to use those features, both bundles should be enabled in addition to AdminBundle. + +```php -## Configuring your application + class AppKernel extends Kernel { + ... -AdminBundle configuration is made in config.yml file. However, if you require a lot of configuration, it is maybe better -to store it in a separate file. + new LAG\AdminBundle\LAGAdminBundle(), + new Knp\Bundle\MenuBundle\KnpMenuBundle(), + new WhiteOctober\PagerfantaBundle\WhiteOctoberPagerfantaBundle(), -__1) Create app/config/admins.yml file__ + ... +``` -To manage easily your admin configuration, you can put it in a separate configuration file (for example, app/config/admins.yml) -This is optional, you can write your configuration directly in your config.yml. If you choose to have a separate file, you have to -import it in your config.yml (@see [Configuration Organization](http://symfony.com/doc/current/cookbook/configuration/configuration_organization.html#advanced-techniques)). +## Configuration -__2) Add your admins.yml file into app/config/config.yml__ +As the configuration can be huge depending on your needs, we recommend to put the AdminBundle's configuration in a +separate file. ```yml - # app/config/config.yml - imports: - - { resource: admins.yml } - ... + app/config/config.yml + imports: + - { resource: admin.yml } ``` -__3) Configure your application parameters__ - -AdminBundle comes with a built-in main layout to handle page parameters (like title...). You have to configure at least your -application title, which is the content title markup by default for all your pages. - ```yml - # config.yml - blue_bear_admin: - application: - title: MyLittleTauntaun Admin - description: My little tauntaun is so nice !!! - + app/config/admin.yml + + lag_admin: + application: + title: My Little TaunTaun application + knp_menu: + twig: + template: 'LAGAdminBundle:Menu:bootstrap_menu.html.twig' ``` -__4) Configuring your CRUD views__ - -Now you have to configure the entities you want to have an admin. To achieve this, you have to add an entry in the admins -configuration. -"my_entity" is just a key. It has to be unique for each entities you have configured. -"MyBundle\Entity\MyEntity" is the namespace path to the entity -"my_form_type" is the form you want to be displayed on edition and creation for your entity. In future version, you will -be allow to set this parameter to null to let AdminBundle generates a form for you. For now you should expose your form -type as a service, and "my_form_type" is the service id. -To expose form type as service, rendez-vous [here](http://symfony.com/doc/current/cookbook/form/create_custom_field_type.html#creating-your-field-type-as-a-service) +## Admin configuration +An `Admin` is based on an Doctrine entity and a Symfony form class (for create and edit actions). Both should be provided +to enable an `Admin`. + ```yml - blue_bear_admin: + app/config/admin.yml + + lag_admin: admins: - my_entity: - entity: MyBundle\Entity\MyEntity - form: my_form_type - + planet: + entity: UniverseBundle\Entity\Planet + form: UniverseBundle\Form\Type\PlanetType ``` -__5) Import AdminBundle routing__ - -You have to import AdminBundle routing (or using yours) in your app/config/routing.yml file. +AdminBundle use a data provider to retrieve and save entities. If you do not provide one, the default one will be used. +It assumes that you have Doctrine repository implementing the `LAG\AdminBundle\Repository\RepositoryInterface`. It will +add the save and delete method to your repository. -```yml - # app/config/routing.yml - # LAG AdminBundle - blue_bear_admin: - resource: . - type: extra - # optional prefix - prefix: /admin - -``` +Fortunately, the AdminBundle provide the `LAG\AdminBundle\Repository\DoctrineRepository` abstract repository class which +implements those methods for you. All you have to do is to extend this class with your repository + +```php + + namespace UniverseBundle\Repository; -That's it. You should have admin application and you should be able to manage your entities. + use LAG\AdminBundle\Repository\DoctrineRepository; + class PlanetRepository extends DoctrineRepository { + + ... +``` + +Your admin is now ready! +> As new routes will be generated, you may need to clear the Symfony's cache. -__Configuration Reference__ +## Configuration Reference ```yml -blue_bear_admin: - application: - # application configuration - title: MyLittleTaunTaun Admin - description: My little tauntaun is so nice !!! - # main layout - layout: MyBundle::layout.html.twig - # form block fields template - block_template: MyBundle:Form:fields.html.twig - # number of entities per page before displaying a pager (for all admin by default) - max_per_page: 25 - # use default bootstrap integration (add bootstrap css and js to the layout) - bootstrap: true - # routing options - routing: - # admin routing name pattern - name_pattern: 'lag.admin.{admin}.{action}' - # admin url pattern (admin is the unique key that you have configured in admins configuration section) - url_pattern: '{admin}/{action}' - # list of managed admins - admins: - # entities (pencil is an example) - tauntaun: - # orm entity class - entity: MyBundle\Entity\TaunTaun - # associated form type - form: tauntaun_type - # if you want full customization, use your controller to handle actions - controller: MyBundle:MyController - # entity manager call for CRUD actions - manager: - # service id of your custom manager - name: my_bundle.manager.tauntaun - # only save action is supported right now. More are coming. myCustomSaveMethod should be a method of - # your manager @my_bundle.manager.tauntaun - save: myCustomSaveMethod - # number of entities per page before displaying a pager (just for current admin) + lag_admin: + application: + title: My Little TaunTaun application + description: My Little TaunTaun application using Admin Bundle + locale: en + # Use the css framework Bootstrap integration (default: true) + bootstrap: true + # Your base template (default: LAGAdminBundle::admin.layout.html.twig) + base_template: 'MyLittleTaunTaunBundle::layout.html.twig' + # Form block template + block_template: 'MyLittleTaunTaunBundle:Form:fields.html.twig' + # Admins routing configuration + routing: + name_pattern: 'tauntaun.{admin}.{action}' + url_pattern: 'tauntaun/{admin}/{action}' + # Use extra configuration helper (default: true) + enable_extra_configuration: true + # Global date format (can be override for each admin, or field) + date_format: 'd/M/Y' + # In list view, strings will be truncated after 200 characters and will be suffixed by ... + string_length: 200 + string_length_truncate: '...' + # Translation configuration + translation: + # Default: true + enabled: true + pattern: app.{key} + # In list view, only 25 items per page max_per_page: 25 - # available actions (for current admin). Actions should always be list, create, edit and delete. More - # customizations will be available in future versions - actions: - list: - # title of the list page - title: Pencils list - # roles allowed to the list action - permissions: [ROLE_ADMIN, ROLE_USER] - # available export for list action - export: [html, pdf, xls, csv, json] - # this fields will be displayed in the list table - fields: - # your entity should have those fields accessible (public property, getters...) - # for example, getId(). @see Symfony\Component\PropertyAccess\PropertyAccessor - id: ~ - name: ~ - label: ~ - # displayed only 50 first characters in list action for this field (more options are coming to - # customize field display - description: {length: 50} - # in this example, riders is related object to current entity (via ManyToOne, a rider can have - # many tauntaun in this case, your entity rider should have an accessible property label) - rider: ~ - # default configuration for those actions + fields_mapping: + # You can override or create new field (it should be declared in services.yml, see the dedicated chapter) + my_custom_field: MyLittleTaunTaunBundle\Fields\MyCustomField + my_custom_string: MyLittleTaunTaunBundle\Fields\MyString + admins: + planet: + # Generic action create: ~ edit: ~ delete: ~ - # short configuration for this entity - rider: - entity: MyOtherBundle\ORM\Rider - form: rider_type - + list: + fields: + id: ~ + name: + type: link + options: + length: 40 + # According to global routing pattern + route: tauntaun.planet.edit + parameters: {id: ~} + category: ~ + galaxy: ~ + publicationStatus: ~ + publicationDate: {type: date, options: {format: d/m/Y}} + updatedAt: {type: date, options: {format: '\L\e d/m/Y à h:i:s'}} + # Your Doctrine entity (required) + entity: MyLittleTaunTaunBundle\Entity\Planet + # Your Symfony form type (required; used in create and edit action) + form: MyLittleTaunTaunBundle\Form\PlanetType + actions: + # Custom actions + death_star: + title: Destroy a planet + fields: + permissions: [ROLE_DARK_SITH] + # Planets will be retrieved sorted by size and by population + order: + size: getSize + population: ~ + route: app.planets.destroy + route_parameters: {id: ~} + icon: fa fa-planet + load_strategy: unique + # Allowed options are pagerfanta and false + pager: false + criteria: {id: ~} + menu: + top: + items: + destroy_another: + title: Destroy an other planet + route: destroy.again + # Used batch action in list view + batch: true + # Global routing override + routing_url_pattern: custom/planet/{admin}/{action} + routing_name_pattern: tauntaun.{admin}.{action} + # Your custom controller (can extends CRUDController to ease Admin management) + controller: MyLittleTaunTaunBundle:MyController + max_per_page: 5 + # Should implements DataProviderInterface + data_provider: 'my.custom.data_provider.service' + # Translations pattern override + translation_pattern: {key} + # Short configuration reference + tauntaun: + entity: MyLittleTaunTaunBundle\Entity\TaunTaun + form: MyLittleTaunTaunBundle\Entity\TaunTaunType + actions: ~ ``` - -## Documentation - -Full documentation can be found [here](https://github.com/larriereguichet/AdminBundle/tree/master/Resources/docs/summary.md) diff --git a/Resources/config/services.yml b/Resources/config/services.yml index a7469e310..b628814f3 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -46,6 +46,7 @@ services: - '@router' - '@translator' - '@lag.admin.configuration_factory' + - '@twig' # handler lag.admin.message_handler: diff --git a/Resources/views/CRUD/edit.html.twig b/Resources/views/CRUD/edit.html.twig index 3f14f7f21..3f3c39bd4 100644 --- a/Resources/views/CRUD/edit.html.twig +++ b/Resources/views/CRUD/edit.html.twig @@ -45,7 +45,3 @@ {% endblock %} - -{% block javascripts %} - {{ tinymce_init() }} -{% endblock %} diff --git a/Resources/views/admin.layout.html.twig b/Resources/views/admin.layout.html.twig index aa6e9820c..fb82e9775 100644 --- a/Resources/views/admin.layout.html.twig +++ b/Resources/views/admin.layout.html.twig @@ -17,58 +17,63 @@ {% block stylesheets %}{% endblock %}
-{% import "LAGAdminBundle:CRUD:macros.html.twig" as AdminBundle %} -{% if admin is not defined %} - {% set admin = null %} -{% endif %} +{% block body %} + {% import "LAGAdminBundle:CRUD:macros.html.twig" as AdminBundle %} -