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

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="sonata.admin.block.admin_list" class="Sonata\AdminBundle\Block\AdminListBlockService">
<tag name="sonata.block"/>
<argument>sonata.admin.block.admin_list</argument>
<argument type="service" id="templating"/>
<argument type="service" id="sonata.admin.pool"/>
</service>
<service id="sonata.admin.block.search_result" class="Sonata\AdminBundle\Block\AdminSearchBlockService">
<tag name="sonata.block"/>
<argument>sonata.admin.block.search_result</argument>
<argument type="service" id="templating"/>
<argument type="service" id="sonata.admin.pool"/>
<argument type="service" id="sonata.admin.search.handler"/>
</service>
<service id="sonata.admin.block.stats" class="Sonata\AdminBundle\Block\AdminStatsBlockService">
<tag name="sonata.block"/>
<argument>sonata.admin.block.stats</argument>
<argument type="service" id="templating"/>
<argument type="service" id="sonata.admin.pool"/>
</service>
</services>
</container>

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="sonata.admin.pool" class="Sonata\AdminBundle\Admin\Pool">
<argument type="service" id="service_container"/>
<argument/>
<argument/>
<argument type="collection"/>
<argument type="service" id="property_accessor"/>
<call method="setTemplates">
<argument>%sonata.admin.configuration.templates%</argument>
</call>
</service>
<service id="sonata.admin.route_loader" class="Sonata\AdminBundle\Route\AdminPoolLoader">
<argument type="service" id="sonata.admin.pool"/>
<argument/>
<argument type="service" id="service_container"/>
<tag name="routing.loader"/>
</service>
<service id="sonata.admin.helper" class="Sonata\AdminBundle\Admin\AdminHelper">
<argument type="service" id="sonata.admin.pool"/>
</service>
<service id="sonata.admin.builder.filter.factory" class="Sonata\AdminBundle\Filter\FilterFactory">
<argument type="service" id="service_container"/>
<argument/>
</service>
<service id="sonata.admin.breadcrumbs_builder" class="Sonata\AdminBundle\Admin\BreadcrumbsBuilder">
<argument>%sonata.admin.configuration.breadcrumbs%</argument>
</service>
<!-- Services used to format the label, default is sonata.admin.label.strategy.noop -->
<service id="sonata.admin.label.strategy.bc" class="Sonata\AdminBundle\Translator\BCLabelTranslatorStrategy"/>
<service id="sonata.admin.label.strategy.native" class="Sonata\AdminBundle\Translator\NativeLabelTranslatorStrategy"/>
<service id="sonata.admin.label.strategy.noop" class="Sonata\AdminBundle\Translator\NoopLabelTranslatorStrategy"/>
<service id="sonata.admin.label.strategy.underscore" class="Sonata\AdminBundle\Translator\UnderscoreLabelTranslatorStrategy"/>
<service id="sonata.admin.label.strategy.form_component" class="Sonata\AdminBundle\Translator\FormLabelTranslatorStrategy"/>
<!-- Translation extractor -->
<service id="sonata.admin.translator.extractor.jms_translator_bundle" class="Sonata\AdminBundle\Translator\Extractor\JMSTranslatorBundle\AdminExtractor">
<tag name="jms_translation.extractor" alias="sonata_admin"/>
<argument type="service" id="sonata.admin.pool"/>
<argument type="service" id="logger" on-invalid="ignore"/>
<call method="setBreadcrumbsBuilder">
<argument type="service" id="sonata.admin.breadcrumbs_builder"/>
</call>
</service>
<!-- controller as services -->
<service id="sonata.admin.controller.admin" class="Sonata\AdminBundle\Controller\HelperController">
<argument type="service" id="twig"/>
<argument type="service" id="sonata.admin.pool"/>
<argument type="service" id="sonata.admin.helper"/>
<argument type="service" id="validator"/>
</service>
<!-- audit manager -->
<service id="sonata.admin.audit.manager" class="Sonata\AdminBundle\Model\AuditManager">
<argument type="service" id="service_container"/>
</service>
<!-- NEXT_MAJOR : remove this service -->
<service id="sonata.admin.exporter" class="Sonata\AdminBundle\Export\Exporter"/>
<service id="sonata.admin.search.handler" class="Sonata\AdminBundle\Search\SearchHandler">
<argument type="service" id="sonata.admin.pool"/>
</service>
<!-- event -->
<service id="sonata.admin.event.extension" class="Sonata\AdminBundle\Event\AdminEventExtension">
<argument type="service" id="event_dispatcher"/>
<tag name="sonata.admin.extension" global="true"/>
</service>
<!-- lock -->
<service id="sonata.admin.lock.extension" class="Sonata\AdminBundle\Admin\Extension\LockExtension">
<tag name="sonata.admin.extension" global="true"/>
</service>
<!-- twig -->
<service id="sonata.admin.twig.global" class="Sonata\AdminBundle\Twig\GlobalVariables">
<argument type="service" id="sonata.admin.pool"/>
</service>
</services>
</container>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="sonata.admin.admin_exporter" class="Sonata\AdminBundle\Bridge\Exporter\AdminExporter">
<argument type="service" id="sonata.exporter.exporter"/>
</service>
</services>
</container>

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<!-- Form Widget-->
<service id="sonata.admin.form.type.admin" class="Sonata\AdminBundle\Form\Type\AdminType">
<tag name="form.type" alias="sonata_type_admin"/>
</service>
<service id="sonata.admin.form.type.model_choice" class="Sonata\AdminBundle\Form\Type\ModelType">
<argument type="service" id="property_accessor"/>
<tag name="form.type" alias="sonata_type_model"/>
</service>
<service id="sonata.admin.form.type.model_list" class="Sonata\AdminBundle\Form\Type\ModelListType">
<tag name="form.type" alias="sonata_type_model_list"/>
</service>
<service id="sonata.admin.form.type.model_reference" class="Sonata\AdminBundle\Form\Type\ModelReferenceType">
<tag name="form.type" alias="sonata_type_model_reference"/>
</service>
<service id="sonata.admin.form.type.model_hidden" class="Sonata\AdminBundle\Form\Type\ModelHiddenType">
<tag name="form.type" alias="sonata_type_model_hidden"/>
</service>
<service id="sonata.admin.form.type.model_autocomplete" class="Sonata\AdminBundle\Form\Type\ModelAutocompleteType">
<tag name="form.type" alias="sonata_type_model_autocomplete"/>
</service>
<service id="sonata.admin.form.type.collection" class="Sonata\AdminBundle\Form\Type\CollectionType">
<tag name="form.type" alias="sonata_type_native_collection"/>
</service>
<service id="sonata.admin.doctrine_orm.form.type.choice_field_mask" class="Sonata\AdminBundle\Form\Type\ChoiceFieldMaskType">
<tag name="form.type" alias="sonata_type_choice_field_mask"/>
</service>
<!-- Form Extension -->
<service id="sonata.admin.form.extension.field" class="Sonata\AdminBundle\Form\Extension\Field\Type\FormTypeFieldExtension">
<tag name="form.type_extension" alias="form" extended-type="Symfony\Component\Form\Extension\Core\Type\FormType"/>
<argument/>
<argument/>
</service>
<service id="sonata.admin.form.extension.field.mopa" class="Sonata\AdminBundle\Form\Extension\Field\Type\MopaCompatibilityTypeFieldExtension">
<tag name="form.type_extension" alias="form" extended-type="Symfony\Component\Form\Extension\Core\Type\FormType"/>
</service>
<service id="sonata.admin.form.extension.choice" class="Sonata\AdminBundle\Form\Extension\ChoiceTypeExtension">
<tag name="form.type_extension" alias="choice" extended-type="Symfony\Component\Form\Extension\Core\Type\ChoiceType"/>
</service>
<!-- Form Filter Type -->
<service id="sonata.admin.form.filter.type.number" class="Sonata\AdminBundle\Form\Type\Filter\NumberType">
<tag name="form.type" alias="sonata_type_filter_number"/>
<argument type="service" id="translator"/>
</service>
<service id="sonata.admin.form.filter.type.choice" class="Sonata\AdminBundle\Form\Type\Filter\ChoiceType">
<tag name="form.type" alias="sonata_type_filter_choice"/>
<argument type="service" id="translator"/>
</service>
<service id="sonata.admin.form.filter.type.default" class="Sonata\AdminBundle\Form\Type\Filter\DefaultType">
<tag name="form.type" alias="sonata_type_filter_default"/>
</service>
<service id="sonata.admin.form.filter.type.date" class="Sonata\AdminBundle\Form\Type\Filter\DateType">
<tag name="form.type" alias="sonata_type_filter_date"/>
<argument type="service" id="translator"/>
</service>
<service id="sonata.admin.form.filter.type.daterange" class="Sonata\AdminBundle\Form\Type\Filter\DateRangeType">
<tag name="form.type" alias="sonata_type_filter_date_range"/>
<argument type="service" id="translator"/>
</service>
<service id="sonata.admin.form.filter.type.datetime" class="Sonata\AdminBundle\Form\Type\Filter\DateTimeType">
<tag name="form.type" alias="sonata_type_filter_datetime"/>
<argument type="service" id="translator"/>
</service>
<service id="sonata.admin.form.filter.type.datetime_range" class="Sonata\AdminBundle\Form\Type\Filter\DateTimeRangeType">
<tag name="form.type" alias="sonata_type_filter_datetime_range"/>
<argument type="service" id="translator"/>
</service>
</services>
</container>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="sonata.admin.menu_builder" class="Sonata\AdminBundle\Menu\MenuBuilder">
<argument type="service" id="sonata.admin.pool"/>
<argument type="service" id="knp_menu.factory"/>
<argument type="service" id="knp_menu.menu_provider"/>
<argument type="service" id="event_dispatcher"/>
</service>
<service id="sonata.admin.sidebar_menu" class="Knp\Menu\MenuItem">
<tag name="knp_menu.menu" alias="sonata_admin_sidebar"/>
</service>
<!--NEXT_MAJOR: Replace empty argument with security.authorization_checker service when bumping requirements to >=Symfony 2.6 -->
<service id="sonata.admin.menu.group_provider" class="Sonata\AdminBundle\Menu\Provider\GroupMenuProvider" public="false">
<argument type="service" id="knp_menu.factory"/>
<argument type="service" id="sonata.admin.pool"/>
<argument/>
<tag name="knp_menu.provider"/>
</service>
<service id="sonata.admin.menu.matcher.voter.admin" class="Sonata\AdminBundle\Menu\Matcher\Voter\AdminVoter">
<tag name="knp_menu.voter" request="true"/>
</service>
<service id="sonata.admin.menu.matcher.voter.children" class="Sonata\AdminBundle\Menu\Matcher\Voter\ChildrenVoter">
<argument type="service" id="knp_menu.matcher"/>
<tag name="knp_menu.voter"/>
</service>
<service id="sonata.admin.menu.matcher.voter.active" class="Sonata\AdminBundle\Menu\Matcher\Voter\ActiveVoter">
<tag name="knp_menu.voter"/>
</service>
</services>
</container>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="sonata.admin.route.path_info" class="Sonata\AdminBundle\Route\PathInfoBuilder">
<argument type="service" id="sonata.admin.audit.manager"/>
</service>
<service id="sonata.admin.route.query_string" class="Sonata\AdminBundle\Route\QueryStringBuilder">
<argument type="service" id="sonata.admin.audit.manager"/>
</service>
<service id="sonata.admin.route.default_generator" class="Sonata\AdminBundle\Route\DefaultRouteGenerator">
<argument type="service" id="router"/>
<argument type="service" id="sonata.admin.route.cache"/>
</service>
<service id="sonata.admin.route.cache" class="Sonata\AdminBundle\Route\RoutesCache">
<argument>%kernel.cache_dir%/sonata/admin</argument>
<argument>%kernel.debug%</argument>
</service>
<service id="sonata.admin.route.cache_warmup" class="Sonata\AdminBundle\Route\RoutesCacheWarmUp">
<argument type="service" id="sonata.admin.route.cache"/>
<argument type="service" id="sonata.admin.pool"/>
<tag name="kernel.cache_warmer"/>
</service>
</services>
</container>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<routes xmlns="http://symfony.com/schema/routing" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
<route id="sonata_admin_redirect" path="/">
<default key="_controller">FrameworkBundle:Redirect:redirect</default>
<default key="route">sonata_admin_dashboard</default>
<default key="permanent">true</default>
</route>
<route id="sonata_admin_dashboard" path="/dashboard">
<default key="_controller">SonataAdminBundle:Core:dashboard</default>
</route>
<route id="sonata_admin_retrieve_form_element" path="/core/get-form-field-element">
<default key="_controller">sonata.admin.controller.admin:retrieveFormFieldElementAction</default>
</route>
<route id="sonata_admin_append_form_element" path="/core/append-form-field-element">
<default key="_controller">sonata.admin.controller.admin:appendFormFieldElementAction</default>
</route>
<route id="sonata_admin_short_object_information" path="/core/get-short-object-description.{_format}">
<default key="_controller">sonata.admin.controller.admin:getShortObjectDescriptionAction</default>
<default key="_format">html</default>
<requirement key="_format">html|json</requirement>
</route>
<route id="sonata_admin_set_object_field_value" path="/core/set-object-field-value">
<default key="_controller">sonata.admin.controller.admin:setObjectFieldValueAction</default>
</route>
<route id="sonata_admin_search" path="/search">
<default key="_controller">SonataAdminBundle:Core:search</default>
</route>
<route id="sonata_admin_retrieve_autocomplete_items" path="/core/get-autocomplete-items">
<default key="_controller">sonata.admin.controller.admin:retrieveAutocompleteItemsAction</default>
</route>
</routes>

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="sonata.admin.security.handler.noop.class">Sonata\AdminBundle\Security\Handler\NoopSecurityHandler</parameter>
<parameter key="sonata.admin.security.handler.role.class">Sonata\AdminBundle\Security\Handler\RoleSecurityHandler</parameter>
<parameter key="sonata.admin.security.handler.acl.class">Sonata\AdminBundle\Security\Handler\AclSecurityHandler</parameter>
<parameter key="sonata.admin.security.mask.builder.class">Sonata\AdminBundle\Security\Acl\Permission\MaskBuilder</parameter>
<parameter key="sonata.admin.manipulator.acl.admin.class">Sonata\AdminBundle\Util\AdminAclManipulator</parameter>
<parameter key="sonata.admin.object.manipulator.acl.admin.class">Sonata\AdminBundle\Util\AdminObjectAclManipulator</parameter>
</parameters>
<services>
<service id="sonata.admin.security.handler.noop" class="%sonata.admin.security.handler.noop.class%" public="false"/>
<service id="sonata.admin.security.handler.role" class="%sonata.admin.security.handler.role.class%" public="false">
<argument/>
<!-- security.authorization_checker or security.context for Symfony <2.6 -->
<argument type="collection">
<argument>ROLE_SUPER_ADMIN</argument>
</argument>
</service>
<service id="sonata.admin.security.handler.acl" class="%sonata.admin.security.handler.acl.class%" public="false">
<argument/>
<!-- security.token_storage or security.context for Symfony <2.6 -->
<argument/>
<!-- security.authorization_checker or security.context for Symfony <2.6 -->
<argument type="service" id="security.acl.provider" on-invalid="null"/>
<argument>%sonata.admin.security.mask.builder.class%</argument>
<argument type="collection">
<argument>ROLE_SUPER_ADMIN</argument>
</argument>
<call method="setAdminPermissions">
<argument>%sonata.admin.configuration.security.admin_permissions%</argument>
</call>
<call method="setObjectPermissions">
<argument>%sonata.admin.configuration.security.object_permissions%</argument>
</call>
</service>
<service id="sonata.admin.manipulator.acl.admin" class="%sonata.admin.manipulator.acl.admin.class%">
<argument>%sonata.admin.security.mask.builder.class%</argument>
</service>
<service id="sonata.admin.object.manipulator.acl.admin" class="%sonata.admin.object.manipulator.acl.admin.class%">
<argument type="service" id="form.factory"/>
<argument>%sonata.admin.security.mask.builder.class%</argument>
</service>
</services>
</container>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
</container>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="sonata.admin.twig.extension.x_editable_type_mapping" type="collection">
<parameter key="choice">select</parameter>
<parameter key="boolean">select</parameter>
<parameter key="text">text</parameter>
<parameter key="textarea">textarea</parameter>
<parameter key="html">textarea</parameter>
<parameter key="email">email</parameter>
<parameter key="string">text</parameter>
<parameter key="smallint">text</parameter>
<parameter key="bigint">text</parameter>
<parameter key="integer">number</parameter>
<parameter key="decimal">number</parameter>
<parameter key="currency">number</parameter>
<parameter key="percent">number</parameter>
<parameter key="url">url</parameter>
<parameter key="date">date</parameter>
</parameter>
</parameters>
<services>
<service id="sonata.admin.twig.extension" class="Sonata\AdminBundle\Twig\Extension\SonataAdminExtension">
<tag name="twig.extension"/>
<argument type="service" id="sonata.admin.pool"/>
<argument type="service" id="logger" on-invalid="ignore"/>
<argument type="service" id="translator"/>
<call method="setXEditableTypeMapping">
<argument>%sonata.admin.twig.extension.x_editable_type_mapping%</argument>
</call>
</service>
</services>
</container>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="sonata.admin.validator.inline" class="Sonata\CoreBundle\Validator\InlineValidator">
<argument type="service" id="service_container"/>
<argument type="service" id="validator.validator_factory"/>
<tag name="validator.constraint_validator" alias="sonata.admin.validator.inline"/>
</service>
</services>
</container>

View File

@@ -0,0 +1,153 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/IoC.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/IoC.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/IoC"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/IoC"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

View File

@@ -0,0 +1,246 @@
# -*- coding: utf-8 -*-
#
# IoC documentation build configuration file, created by
# sphinx-quickstart on Fri Mar 29 01:43:00 2013.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sensio.sphinx.refinclude', 'sensio.sphinx.configurationblock', 'sensio.sphinx.phpcode']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Sonata ~ AdminBundle'
copyright = u'2010-2015, Thomas Rabaix'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
#version = '0.0.1'
# The full version, including alpha/beta/rc tags.
#release = '0.0.1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# This will be used when using the shorthand notation
highlight_language = 'php'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
import sphinx_rtd_theme
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'sphinx_rtd_theme'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'doc'
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
#latex_documents = [
# ('index', 'PythonElement.tex', u'Python Documentation',
# u'Thomas Rabaix', 'manual'),
#]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
#(source start file, name, description, authors, manual section).
#man_pages = [
# ('index', 'ioc', u'IoC Documentation',
# [u'Thomas Rabaix'], 1)
#]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
#texinfo_documents = [
# ('index', 'IoC', u'IoC Documentation',
# u'Thomas Rabaix', 'IoC', 'One line description of project.',
# 'Miscellaneous'),
#]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'

View File

@@ -0,0 +1,27 @@
Bootlint
========
The admin comes with `Bootlint`_ integration
since version 3.0. Bootlint is an HTML linter for Bootstrap projects.
You should use it when you want add some contributions on Sonata UI to check
the eventual Twitter Bootstrap conventions' mistakes.
Enable Bootlint
---------------
To use Bootlint in your admin, you can enable it in ``config.yml``.
.. configuration-block::
.. code-block:: yaml
sonata_admin:
options:
use_bootlint: true # enable Bootlint
Then open your browser debugger to look after some Bootlint warnings on console.
No warning? Congrats! Your page is fully Bootstrap compliant! ;)
.. _`Bootlint`: https://github.com/twbs/bootlint

View File

@@ -0,0 +1,274 @@
Creating a Custom Admin Action
==============================
This is a full working example of creating a custom list action for SonataAdmin.
The example is based on an existing ``CarAdmin`` class in an ``AppBundle``.
It is assumed you already have an admin service up and running.
The recipe
----------
SonataAdmin provides a very straight-forward way of adding your own custom actions.
To do this we need to:
- extend the ``SonataAdmin:CRUD`` Controller and tell our admin class to use it
- create the custom action in our Controller
- create a template to show the action in the list view
- add the route and the new action in the Admin class
Extending the Admin Controller
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
First you need to create your own Controller extending the one from SonataAdmin
.. code-block:: php
<?php
// src/AppBundle/Controller/CRUDController.php
namespace AppBundle\Controller;
use Sonata\AdminBundle\Controller\CRUDController as Controller;
class CRUDController extends Controller
{
// ...
}
Admin classes by default use the ``SonataAdmin:CRUD`` controller, this is the third parameter
of an admin service definition, you need to change it to your own.
Register the Admin as a Service
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Either by using XML:
.. code-block:: xml
<!-- src/AppBundle/Resources/config/admin.xml -->
<service id="app.admin.car" class="AppBundle\Admin\CarAdmin">
<tag name="sonata.admin" manager_type="orm" group="Demo" label="Car" />
<argument />
<argument>AppBundle\Entity\Car</argument>
<argument>AppBundle:CRUD</argument>
</service>
or by adding it to your ``admin.yml``:
.. code-block:: yaml
# src/AppBundle/Resources/config/admin.yml
services:
app.admin.car:
class: AppBundle\Admin\CarAdmin
tags:
- { name: sonata.admin, manager_type: orm, group: Demo, label: Car }
arguments:
- null
- AppBundle\Entity\Car
- AppBundle:CRUD
public: true
For more information about service configuration please refer to Step 3 of :doc:`../reference/getting_started`
Create the custom action in your Controller
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Now it is time to actually create your custom action here, for this example I chose
to implement a ``clone`` action.
.. code-block:: php
<?php
// src/AppBundle/Controller/CRUDController.php
namespace AppBundle\Controller;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Sonata\AdminBundle\Controller\CRUDController as Controller;
use Symfony\Component\HttpFoundation\RedirectResponse;
class CRUDController extends Controller
{
/**
* @param $id
*/
public function cloneAction($id)
{
$object = $this->admin->getSubject();
if (!$object) {
throw new NotFoundHttpException(sprintf('unable to find the object with id: %s', $id));
}
// Be careful, you may need to overload the __clone method of your object
// to set its id to null !
$clonedObject = clone $object;
$clonedObject->setName($object->getName().' (Clone)');
$this->admin->create($clonedObject);
$this->addFlash('sonata_flash_success', 'Cloned successfully');
return new RedirectResponse($this->admin->generateUrl('list'));
// if you have a filtered list and want to keep your filters after the redirect
// return new RedirectResponse($this->admin->generateUrl('list', array('filter' => $this->admin->getFilterParameters())));
}
}
Here we first get the object, see if it exists then clone it and insert the clone
as a new object. Finally we set a flash message indicating success and redirect to the list view.
If you want to add the current filter parameters to the redirect url you can add them to the `generateUrl` method:
.. code-block:: php
return new RedirectResponse($this->admin->generateUrl('list', array('filter' => $this->admin->getFilterParameters())));
Using template in new controller
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you want to render something here you can create new template anywhere, extend sonata layout
and use `sonata_admin_content` block.
.. code-block:: html+jinja
{% extends 'SonataAdminBundle::standard_layout.html.twig' %}
{% block sonata_admin_content %}
Your content here
{% endblock %}
Create a template for the new action
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
You need to tell SonataAdmin how to render your new action. You do that by
creating a ``list__action_clone.html.twig`` in the namespace of your custom
Admin Controller.
.. code-block:: html+jinja
{# src/AppBundle/Resources/views/CRUD/list__action_clone.html.twig #}
<a class="btn btn-sm" href="{{ admin.generateObjectUrl('clone', object) }}">clone</a>
Right now ``clone`` is not a known route, we define it in the next step.
Bringing it all together
^^^^^^^^^^^^^^^^^^^^^^^^
What is left now is actually adding your custom action to the admin class.
You have to add the new route in ``configureRoutes``:
.. code-block:: php
// ...
use Sonata\AdminBundle\Route\RouteCollection;
protected function configureRoutes(RouteCollection $collection)
{
$collection->add('clone', $this->getRouterIdParameter().'/clone');
}
This gives us a route like ``../admin/app/car/1/clone``.
You could also just write ``$collection->add('clone');`` to get a route like ``../admin/app/car/clone?id=1``
Next we have to add the action in ``configureListFields`` specifying the template we created.
.. code-block:: php
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
// other fields...
->add('_action', null, array(
'actions' => array(
// ...
'clone' => array(
'template' => 'AppBundle:CRUD:list__action_clone.html.twig'
)
)
))
;
}
The full ``CarAdmin.php`` example looks like this:
.. code-block:: php
<?php
// src/AppBundle/Admin/CarAdmin.php
namespace AppBundle\Admin;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Route\RouteCollection;
use Sonata\AdminBundle\Show\ShowMapper;
class CarAdmin extends AbstractAdmin
{
protected function configureRoutes(RouteCollection $collection)
{
$collection->add('clone', $this->getRouterIdParameter().'/clone');
}
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
// ...
}
protected function configureFormFields(FormMapper $formMapper)
{
// ...
}
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('name')
->add('engine')
->add('rescueEngine')
->add('createdAt')
->add('_action', null, array(
'actions' => array(
'show' => array(),
'edit' => array(),
'delete' => array(),
'clone' => array(
'template' => 'AppBundle:CRUD:list__action_clone.html.twig'
)
)
));
}
protected function configureShowFields(ShowMapper $showMapper)
{
// ...
}
}
.. note::
If you want to render a custom controller action in a template by using the
render function in twig you need to add ``_sonata_admin`` as an attribute. For
example; ``{{ render(controller('AppBundle:XxxxCRUD:comment', {'_sonata_admin':
'sonata.admin.xxxx' })) }}``. This has to be done because the moment the
rendering should happen the routing, which usually sets the value of this
parameter, is not involved at all, and then you will get an error "There is no
_sonata_admin defined for the controller
AppBundle\Controller\XxxxCRUDController and the current route ' '."

View File

@@ -0,0 +1,108 @@
Customizing a mosaic list
=========================
Since version 3.0, the AdminBundle now include a mosaic list mode in order to have a more visual representation.
.. figure:: ../images/list_mosaic_default.png
:align: center
:alt: Default view
:width: 700px
It is possible to configure the default view by creating a dedicated template.
First, configure the ``outer_list_rows_mosaic`` template key:
.. code-block:: xml
<service id="sonata.media.admin.media" class="%sonata.media.admin.media.class%">
<tag name="sonata.admin" manager_type="orm" group="sonata_media" label_catalogue="%sonata.media.admin.media.translation_domain%" label="media" label_translator_strategy="sonata.admin.label.strategy.underscore" />
<argument />
<argument>%sonata.media.admin.media.entity%</argument>
<argument>%sonata.media.admin.media.controller%</argument>
<call method="setTemplates">
<argument type="collection">
<argument key="outer_list_rows_mosaic">SonataMediaBundle:MediaAdmin:list_outer_rows_mosaic.html.twig</argument>
</argument>
</call>
The ``list_outer_rows_mosaic.html.twig`` is the name of one mosaic's tile. You should also extends the template and overwrite the default blocks availables.
.. code-block:: jinja
{% extends 'SonataAdminBundle:CRUD:list_outer_rows_mosaic.html.twig' %}
{% block sonata_mosaic_background %}{{ meta.image }}{% endblock %}
{% block sonata_mosaic_default_view %}
<span class="label label-primary pull-right">{{ object.providerName|trans({}, 'SonataMediaBundle') }}</span>
{% endblock %}
{% block sonata_mosaic_hover_view %}
<span class="label label-primary pull-right">{{ object.providerName|trans({}, 'SonataMediaBundle') }}</span>
{% if object.width %} {{ object.width }}{% if object.height %}x{{ object.height }}{% endif %}px{% endif %}
{% if object.length > 0 %}
({{ object.length }})
{% endif %}
<br />
{% if object.authorname is not empty %}
{{ object.authorname }}
{% endif %}
{% if object.copyright is not empty and object.authorname is not empty %}
~
{% endif %}
{% if object.copyright is not empty %}
&copy; {{ object.copyright }}
{% endif %}
{% endblock %}
{% block sonata_mosaic_description %}
{% if admin.hasAccess('edit', object) and admin.hasRoute('edit') %}
<a href="{{ admin.generateUrl('edit', {'id' : object|sonata_urlsafeid(admin) }) }}">{{ meta.title|truncate(40) }}</a>
{% elseif admin.hasAccess('show', object) and admin.hasRoute('show') %}
<a href="{{ admin.generateUrl('show', {'id' : object|sonata_urlsafeid(admin) }) }}">{{ meta.title|truncate(40) }}</a>
{% else %}
{{ meta.title|truncate(40) }}
{% endif %}
{% endblock %}
Block types:
- ``sonata_mosaic_background``: this block is the background value defined in the ObjectMetadata object.
- ``sonata_mosaic_default_view``: this block is used when the list is displayed.
- ``sonata_mosaic_hover_view``: this block is used when the mouse is over the tile.
- ``sonata_mosaic_description``: this block will be always on screen and should represent the entity's name.
The ``ObjectMetadata`` object is returned by the related admin class, for instance the MediaBundle defines the method as:
.. code-block:: jinja
<?php
class MediaAdmin extends AbstractAdmin
{
// [...] others methods
public function getObjectMetadata($object)
{
$provider = $this->pool->getProvider($object->getProviderName());
$url = $provider->generatePublicUrl($object, $provider->getFormatName($object, 'admin'));
return new Metadata($object->getName(), $object->getDescription(), $url);
}
}
The final view will look like:
.. figure:: ../images/list_mosaic_custom.png
:align: center
:alt: Customize view
:width: 700px

View File

@@ -0,0 +1,139 @@
Using DataMapper to work with domain entities without per field setters
=======================================================================
This is an example of using DataMapper with entities that avoid setters/getters for each field.
Pre-requisites
--------------
- You already have SonataAdmin and DoctrineORM up and running.
- You already have an Entity class, in this example that class will be called ``Example``.
- You already have an Admin set up, in this example it's called ``ExampleAdmin``.
The recipe
----------
If there is a requirement for the entity to have domain specific methods instead of getters/setters for each
entity field then it won't work with SonataAdmin out of the box. But Symfony Form component provides ``DataMapper``
that can be used to make it work. Symfony itself lacks examples of using ``DataMapper`` but there is an article by
webmozart that covers it - https://webmozart.io/blog/2015/09/09/value-objects-in-symfony-forms/
Example Entity
^^^^^^^^^^^^^^
.. code-block:: php
<?php
// src/AppBundle/Entity/Example.php
namespace AppBundle\Entity;
class Example
{
private $name;
private $description;
public function __construct($name, $description)
{
$this->name = $name;
$this->description = $description;
}
public function update($description)
{
$this->description = $description
}
// rest of the code goes here
}
DataMapper
^^^^^^^^^^
To be able to set entity data without the possibility to use setters a ``DataMapper`` should be created.
.. code-block:: php
<?php
// src/AppBundle/Form/DataMapper/ExampleDataMapper.php
namespace AppBundle\Form\DataMapper;
use Symfony\Component\Form\DataMapperInterface;
use AppBundle\Entity\Example;
class ExampleDataMapper implements DataMapperInterface
{
/**
* @param Example $data
* @param FormInterface[]|\Traversable $forms
*/
public function mapDataToForms($data, $forms)
{
if (null !== $data) {
$forms = iterator_to_array($forms);
$forms['name']->setData($data->getName());
$forms['description']->setData($data->getDescription());
}
}
/**
* @param FormInterface[]|\Traversable $forms
* @param Example $data
*/
public function mapFormsToData($forms, &$data)
{
$forms = iterator_to_array($forms);
if (null === $data->getId()) {
$name = $forms['name']->getData();
$description = $forms['description']->getData();
// New entity is created
$data = new Example(
$name,
$description
);
} else {
$data->update(
$forms['description']->getData()
);
}
}
}
Admin class
^^^^^^^^^^^
Now we need to configure the form to use our ``ExampleDataMapper``.
.. code-block:: php
<?php
// src/AppBundle/Admin/ExampleAdmin.php
namespace AppBundle\Admin;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Form\FormMapper;
use AppBundle\Form\DataMapper\ExampleDataMapper;
class ExampleAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('name', null)
->add('description', null);
;
$builder = $formMapper->getFormBuilder();
$builder->setDataMapper(new ExampleDataMapper());
}
// ...
}

View File

@@ -0,0 +1,45 @@
Deleting a Group of Fields from an Admin
========================================
In some cases, when you extend existing Admins, you might want to delete fields from the admin, or make them not show.
You could delete every field by hand, using the ``FormMapper``s ``remove`` method.
.. code-block:: php
class UserAdmin extends \Sonata\UserBundle\Admin\Model\UserAdmin {
protected function configureFormFields(FormMapper $formMapper)
{
parent::configureFormFields($formMapper);
$formMapper->remove('facebookName');
$formMapper->remove('twitterUid');
$formMapper->remove('twitterName');
$formMapper->remove('gplusUid');
$formMapper->remove('gplusName');
}
}
This works, as long as the extended Admin does not use Groups to organize it's field. In the above example, we try to remove all fields from the User Admin, that comes with the SonataUserBundle.
However, since the fields we deleted, are all part of the 'Social' Group of the form, the fields will be deleted and the empty group will stay.
For this case, the FormMapper comes with a method, which allows you to get rid of a whole form group: ``removeGroup``
.. code-block:: php
class UserAdmin extends \Sonata\UserBundle\Admin\Model\UserAdmin {
protected function configureFormFields(FormMapper $formMapper)
{
parent::configureFormFields($formMapper);
$formMapper->removeGroup('Social', 'User');
}
}
This will remove the whole 'Social' group from the form, which happens to contain all the fields, we deleted manually in the first example. The second argument is the name of the tab, the group belongs to.
This is optional. However, when not provided, it will be assumed that you mean the 'default' tab. If the group is on another tab, it won't be removed, when this is not provided.
There is a third optional argument for the method, which let's you choose, whether or not, tabs are also removed, if you happen to remove all groups of a tab. This behaviour is disabled by default, but
can be enabled, by setting the third argument of ``removeGroup`` to ``true``

View File

@@ -0,0 +1,49 @@
Modifying form fields dynamically depending on edited object
============================================================
It is a quite common situation when you need to modify your form's fields because
of edited object's properties or structure. Let us assume you only want to display
an admin form field for new objects and you do not want it to be shown for those
objects that have already been saved to the database and now are being edited.
This is a way for you to accomplish this.
In your ``Admin`` class' ``configureFormFields`` method you are able to get the
current object by calling ``$this->getSubject()``. The value returned will be your
linked model. And another method ``isCurrentRoute`` for check the current request's route.
Then, you should be able to dynamically add needed fields to the form:
.. code-block:: php
<?php
// src/AppBundle/Admin/PostAdmin
namespace AppBundle\Admin;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Form\FormMapper;
class PostAdmin extends AbstractAdmin
{
// ...
protected function configureFormFields(FormMapper $formMapper)
{
// Description field will always be added to the form:
$formMapper
->add('description', 'textarea')
;
$subject = $this->getSubject();
if ($subject->isNew()) {
// The thumbnail field will only be added when the edited item is created
$formMapper->add('thumbnail', 'file');
}
// Name field will be added only when create an item
if ($this->isCurrentRoute('create')) {
$formMapper->add('name', 'text');
}
}
}

View File

@@ -0,0 +1,335 @@
Uploading and saving documents (including images) using DoctrineORM and SonataAdmin
===================================================================================
This is a full working example of a file upload management method using
SonataAdmin with the DoctrineORM persistence layer.
Pre-requisites
--------------
- you already have SonataAdmin and DoctrineORM up and running
- you already have an Entity class that you wish to be able to connect uploaded
documents to, in this example that class will be called ``Image``.
- you already have an Admin set up, in this example it's called ``ImageAdmin``
- you understand file permissions on your web server and can manage the permissions
needed to allow your web server to upload and update files in the relevant
folder(s)
The recipe
----------
First we will cover the basics of what your Entity needs to contain to enable document
management with Doctrine. There is a good cookbook entry about
`uploading files with Doctrine and Symfony`_ on the Symfony website, so I will show
code examples here without going into the details. It is strongly recommended that
you read that cookbook first.
To get file uploads working with SonataAdmin we need to:
- add a file upload field to our ImageAdmin
- 'touch' the Entity when a new file is uploaded so its lifecycle events are triggered
Basic configuration - the Entity
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Following the guidelines from the Symfony cookbook, we have an Entity definition
that looks something like the YAML below (of course, you can achieve something
similar with XML or Annotation based definitions too). In this example we are using
the ``updated`` field to trigger the lifecycle callbacks by setting it based on the
upload timestamp.
.. configuration-block::
.. code-block:: yaml
# src/AppBundle/Resources/config/Doctrine/Image.orm.yml
AppBundleBundle\Entity\Image:
type: entity
repositoryClass: AppBundle\Entity\Repositories\ImageRepository
table: images
id:
id:
type: integer
generator: { strategy: AUTO }
fields:
filename:
type: string
length: 100
# changed when files are uploaded, to force preUpdate and postUpdate to fire
updated:
type: datetime
nullable: true
# ... other fields ...
lifecycleCallbacks:
prePersist: [ lifecycleFileUpload ]
preUpdate: [ lifecycleFileUpload ]
We then have the following methods in our ``Image`` class to manage file uploads:
.. code-block:: php
<?php
// src/AppBundle/Bundle/Entity/Image.php
const SERVER_PATH_TO_IMAGE_FOLDER = '/server/path/to/images';
/**
* Unmapped property to handle file uploads
*/
private $file;
/**
* Sets file.
*
* @param UploadedFile $file
*/
public function setFile(UploadedFile $file = null)
{
$this->file = $file;
}
/**
* Get file.
*
* @return UploadedFile
*/
public function getFile()
{
return $this->file;
}
/**
* Manages the copying of the file to the relevant place on the server
*/
public function upload()
{
// the file property can be empty if the field is not required
if (null === $this->getFile()) {
return;
}
// we use the original file name here but you should
// sanitize it at least to avoid any security issues
// move takes the target directory and target filename as params
$this->getFile()->move(
self::SERVER_PATH_TO_IMAGE_FOLDER,
$this->getFile()->getClientOriginalName()
);
// set the path property to the filename where you've saved the file
$this->filename = $this->getFile()->getClientOriginalName();
// clean up the file property as you won't need it anymore
$this->setFile(null);
}
/**
* Lifecycle callback to upload the file to the server
*/
public function lifecycleFileUpload()
{
$this->upload();
}
/**
* Updates the hash value to force the preUpdate and postUpdate events to fire
*/
public function refreshUpdated()
{
$this->setUpdated(new \DateTime());
}
// ... the rest of your class lives under here, including the generated fields
// such as filename and updated
When we upload a file to our Image, the file itself is transient and not persisted
to our database (it is not part of our mapping). However, the lifecycle callbacks
trigger a call to ``Image::upload()`` which manages the actual copying of the
uploaded file to the filesystem and updates the ``filename`` property of our Image,
this filename field *is* persisted to the database.
Most of the above is simply from the `uploading files with Doctrine and Symfony`_ cookbook
entry. It is highly recommended reading!
Basic configuration - the Admin class
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
We need to do two things in Sonata to enable file uploads:
1. Add a file upload widget
2. Ensure that the Image class' lifecycle events fire when we upload a file
Both of these are straightforward when you know what to do:
.. code-block:: php
<?php
// src/AppBundle/Admin/ImageAdmin.php
class ImageAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('file', 'file', array(
'required' => false
))
// ...
;
}
public function prePersist($image)
{
$this->manageFileUpload($image);
}
public function preUpdate($image)
{
$this->manageFileUpload($image);
}
private function manageFileUpload($image)
{
if ($image->getFile()) {
$image->refreshUpdated();
}
}
// ...
}
We mark the ``file`` field as not required since we do not need the user to upload a
new image every time the Image is updated. When a file is uploaded (and nothing else
is changed on the form) there is no change to the data which Doctrine needs to persist
so no ``preUpdate`` event would fire. To deal with this we hook into SonataAdmin's
``preUpdate`` event (which triggers every time the edit form is submitted) and use
that to update an Image field which is persisted. This then ensures that Doctrine's
lifecycle events are triggered and our Image manages the file upload as expected.
And that is all there is to it!
However, this method does not work when the ``ImageAdmin`` is embedded in other
Admins using the ``sonata_type_admin`` field type. For that we need something more...
Advanced example - works with embedded Admins
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
When one Admin is embedded in another Admin, the child Admin's ``preUpdate()`` method is
not triggered when the parent is submitted. To deal with this we need to use the parent
Admin's lifecycle events to trigger the file management when needed.
In this example we have a Page class which has three one-to-one Image relationships
defined, linkedImage1 to linkedImage3. The PostAdmin class' form field configuration
looks like this:
.. code-block:: php
<?php
// src/AppBundle/Admin/PostAdmin.php
class PostAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('linkedImage1', 'sonata_type_admin', array(
'delete' => false
))
->add('linkedImage2', 'sonata_type_admin', array(
'delete' => false
))
->add('linkedImage3', 'sonata_type_admin', array(
'delete' => false
))
// ...
;
}
// ...
}
This is easy enough - we have embedded three fields, which will then use our ``ImageAdmin``
class to determine which fields to show.
In our PostAdmin we then have the following code to manage the relationships' lifecycles:
.. code-block:: php
<?php
// src/AppBundle/Admin/PostAdmin.php
class PostAdmin extends AbstractAdmin
{
// ...
public function prePersist($page)
{
$this->manageEmbeddedImageAdmins($page);
}
public function preUpdate($page)
{
$this->manageEmbeddedImageAdmins($page);
}
private function manageEmbeddedImageAdmins($page)
{
// Cycle through each field
foreach ($this->getFormFieldDescriptions() as $fieldName => $fieldDescription) {
// detect embedded Admins that manage Images
if ($fieldDescription->getType() === 'sonata_type_admin' &&
($associationMapping = $fieldDescription->getAssociationMapping()) &&
$associationMapping['targetEntity'] === 'AppBundle\Entity\Image'
) {
$getter = 'get'.$fieldName;
$setter = 'set'.$fieldName;
/** @var Image $image */
$image = $page->$getter();
if ($image) {
if ($image->getFile()) {
// update the Image to trigger file management
$image->refreshUpdated();
} elseif (!$image->getFile() && !$image->getFilename()) {
// prevent Sf/Sonata trying to create and persist an empty Image
$page->$setter(null);
}
}
}
}
}
// ...
}
Here we loop through the fields of our PageAdmin and look for ones which are ``sonata_type_admin``
fields which have embedded an Admin which manages an Image.
Once we have those fields we use the ``$fieldName`` to build strings which refer to our accessor
and mutator methods. For example we might end up with ``getlinkedImage1`` in ``$getter``. Using
this accessor we can get the actual Image object from the Page object under management by the
PageAdmin. Inspecting this object reveals whether it has a pending file upload - if it does we
trigger the same ``refreshUpdated()`` method as before.
The final check is to prevent a glitch where Symfony tries to create blank Images when nothing
has been entered in the form. We detect this case and null the relationship to stop this from
happening.
Notes
-----
If you are looking for richer media management functionality there is a complete SonataMediaBundle
which caters to this need. It is documented online and is created and maintained by the same team
as SonataAdmin.
To learn how to add an image preview to your ImageAdmin take a look at the related cookbook entry.
.. _`uploading files with Doctrine and Symfony`: http://symfony.com/doc/current/cookbook/doctrine/file_uploads.html

View File

@@ -0,0 +1,144 @@
Showing image previews
======================
This is a full working example of one way to add image previews to your create and
edit views in SonataAdmin.
Pre-requisites
--------------
- you have already got the image files on a server somewhere and have a helper
method to retrieve a publicly visible URL for that image, in this example that
method is called ``Image::getWebPath()``
- you have already set up an Admin to edit the object that contains the images,
now you just want to add the previews. In this example that class is called
``ImageAdmin``
.. note::
There is a separate cookbook recipe to demonstrate how to upload images
(and other files) using SonataAdmin.
The recipe
----------
SonataAdmin lets us put raw HTML into the 'help' option for any given form field.
We are going to use this functionality to embed an image tag when an image exists.
To do this we need to:
- get access to the ``Image`` instance from within ``ImageAdmin``
- create an image tag based on the Image's URL
- add a 'help' option to a field on the Image form to display the image tag
For the sake of this example we will use some basic CSS to restrict the size of
the preview image (we are not going to generate and save special thumbnails).
Basic example - for single layer Admins
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If we are working directly with our ``ImageAdmin`` class then getting hold of
the ``Image`` instance is simply a case of calling ``$this->getSubject()``. Since
we are manipulating form fields we do this from within ``ImageAdmin::configureFormFields()``:
.. code-block:: php
class ImageAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
// get the current Image instance
$image = $this->getSubject();
// use $fileFieldOptions so we can add other options to the field
$fileFieldOptions = array('required' => false);
if ($image && ($webPath = $image->getWebPath())) {
// get the container so the full path to the image can be set
$container = $this->getConfigurationPool()->getContainer();
$fullPath = $container->get('request_stack')->getCurrentRequest()->getBasePath().'/'.$webPath;
// add a 'help' option containing the preview's img tag
$fileFieldOptions['help'] = '<img src="'.$fullPath.'" class="admin-preview" />';
}
$formMapper
// ... other fields ...
->add('file', 'file', $fileFieldOptions)
;
}
// ...
}
We then use CSS to restrict the max size of the image:
.. code-block:: css
img.admin-preview {
max-height: 200px;
max-width: 200px;
}
And that is all there is to it!
However, this method does not work when the ``ImageAdmin`` can be embedded in other
Admins using the ``sonata_type_admin`` field type. For that we need...
Advanced example - works with embedded Admins
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
When one Admin is embedded in another Admin, ``$this->getSubject()`` does not return the
instance under management by the embedded Admin. Instead we need to detect that our
Admin class is embedded and use a different method:
.. code-block:: php
class ImageAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
if($this->hasParentFieldDescription()) { // this Admin is embedded
// $getter will be something like 'getlogoImage'
$getter = 'get' . $this->getParentFieldDescription()->getFieldName();
// get hold of the parent object
$parent = $this->getParentFieldDescription()->getAdmin()->getSubject();
if ($parent) {
$image = $parent->$getter();
} else {
$image = null;
}
} else {
$image = $this->getSubject();
}
// use $fileFieldOptions so we can add other options to the field
$fileFieldOptions = array('required' => false);
if ($image && ($webPath = $image->getWebPath())) {
// add a 'help' option containing the preview's img tag
$fileFieldOptions['help'] = '<img src="'.$webPath.'" class="admin-preview" />';
}
$formMapper
// ... other fields ...
->add('file', 'file', $fileFieldOptions)
;
}
// ...
}
As you can see, the only change is how we retrieve set ``$image`` to the relevant Image instance.
When our ImageAdmin is embedded we need to get the parent object first then use a getter to
retrieve the Image. From there on, everything else is the same.
Notes
-----
If you have more than one level of embedding Admins this will (probably) not work. If you know of
a more generic solution, please fork and update this recipe on GitHub. Similarly, if there are any
errors or typos (or a much better way to do this) get involved and share your insights for the
benefit of everyone.

View File

@@ -0,0 +1,48 @@
Improve performance of large datasets
=====================================
If your database table contains thousands of records, the database queries generated
by SonataAdmin may become very slow. Here are tips how to improve the performance of your admin.
Change default Pager to SimplePager
-----------------------------------
Default `Pager` is counting all rows in the table, so user can easily navigate
to any page in the Datagrid. But counting thousands or millions of records
can be slow operation. If you don't need to know the number of all records,
you can use `SimplePager` instead. It doesn't count all rows, but gives user only
information if there is next page or not.
To use `SimplePager` in your admin just define ``pager_type`` inside the service definition:
.. configuration-block::
.. code-block:: xml
<!-- src/AppBundle/Resources/config/admin.xml -->
<service id="app.admin.post" class="AppBundle\Admin\PostAdmin">
<tag name="sonata.admin" manager_type="orm" group="Content" label="Post" pager_type="simple" />
<argument />
<argument>AppBundle\Entity\Post</argument>
<argument />
</service>
.. code-block:: yaml
# src/AppBundle/Resources/config/admin.yml
services:
app.admin.post:
class: AppBundle\Admin\PostAdmin
tags:
- { name: sonata.admin, manager_type: orm, group: "Content", label: "Post", pager_type: "simple" }
arguments:
- ~
- AppBundle\Entity\Post
- ~
public: true
.. note::
The ``pager_results`` template is automatically changed to ``SonataAdminBundle:Pager:simple_pager_results.html.twig`` if it's not already overloaded.

View File

@@ -0,0 +1,293 @@
KnpMenu
=======
The admin comes with `KnpMenu`_ integration.
It integrates a menu with the KnpMenu library. This menu can be a SonataAdmin service, a menu created with a Knp menu provider or a route of a custom controller.
Add a custom controller entry in the menu
-----------------------------------------
To add a custom controller entry in the admin menu:
Create your controller:
.. code-block:: php
class BlogController
{
/**
* @Route("/blog", name="blog_home")
*/
public function blogAction()
{
// ...
}
/**
* @Route("/blog/article/{articleId}", name="blog_article")
*/
public function ArticleAction($articleId)
{
// ...
}
}
Add the controller route as an item of the menu:
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
dashboard:
groups:
news:
label: ~
label_catalogue: ~
items:
- sonata.news.admin.post
- route: blog_home
label: Blog
- route: blog_article
route_params: { articleId: 3 }
label: Article
If you want to show your route to user with different roles, you can configure this for each route. If this is not set,
group roles will be checked.
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
dashboard:
groups:
news:
label: ~
label_catalogue: ~
items:
- sonata.news.admin.post
- route: blog_home
label: Blog
roles: [ ROLE_FOO, ROLE_BAR ]
- route: blog_article
route_params: { articleId: 3 }
label: Article
roles: [ ROLE_ADMIN, ROLE_SONATA_ADMIN]
You can also override the template of knp_menu used by sonata. The default one is `SonataAdminBundle:Menu:sonata_menu.html.twig`:
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
templates:
knp_menu_template: ApplicationAdminBundle:Menu:custom_knp_menu.html.twig
And voilà, now you have a menu group which contains a link to a sonata admin via its id, to your blog and to a specific article.
Using a menu provider
---------------------
As seen above, the main way to declare your menu is by declaring items in your sonata admin config file. In some case you may have to create a more complex menu depending on your business logic. This is possible by using a menu provider to populate a whole menu group. This is done with the ``provider`` config value.
The following configuration uses a menu provider to populate the menu group ``my_group``:
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
dashboard:
groups:
my_group:
provider: 'MyBundle:MyMenuProvider:getMyMenu'
icon: '<i class="fa fa-edit"></i>'
With KnpMenuBundle you can create a custom menu by using a builder class or by declaring it as a service. Please see the `Knp documentation`_ for further information.
In sonata, whatever the implementation you choose, you only have to provide the menu alias to the provider config key:
* If you are using a builder class, your menu alias should be something like ``MyBundle:MyMenuProvider:getMyMenu``.
* If you are using a service, your menu alias is the alias set in the ``knp_menu.menu`` tag. In the following example this is ``my_menu_alias``:
.. configuration-block::
.. code-block:: xml
<service id="my_menu_provider" class="MyBundle/MyDirectory/MyMenuProvider">
<tag name="knp_menu.menu" alias="my_menu_alias" />
</service>
Please note that when using the provider option, you can't set the menu label via the configuration. It is done in your custom menu.
Extending the menu
------------------
You can modify the menu via events easily. You can register as many listeners as you want for the event with name ``sonata.admin.event.configure.menu.sidebar``:
.. code-block:: php
<?php
// src/AppBundle/EventListener/MenuBuilderListener.php
namespace AppBundle\EventListener;
use Sonata\AdminBundle\Event\ConfigureMenuEvent;
class MenuBuilderListener
{
public function addMenuItems(ConfigureMenuEvent $event)
{
$menu = $event->getMenu();
$child = $menu->addChild('reports', [
'label' => 'Daily and monthly reports',
'route' => 'app_reports_index',
])->setExtras([
'icon' => '<i class="fa fa-bar-chart"></i>',
]);
}
}
.. configuration-block::
.. code-block:: yaml
# src/AppBundle/Resources/config/services.yml
services:
app.menu_listener:
class: AppBundle\EventListener\MenuBuilderListener
tags:
- { name: kernel.event_listener, event: sonata.admin.event.configure.menu.sidebar, method: addMenuItems }
Please see the `Using events to allow a menu to be extended`_ for further information.
Hiding menu items
-----------------
You can modify the menu to hide some menu items. You need to add the ``show_in_dashboard`` option in
your admin services or simply remove menu items from the ``sonata_admin`` dashboard group configuration:
.. code-block:: yaml
sonata_admin.admin.post:
class: Sonata\AdminBundle\Admin\PostAdmin
arguments: [~, Sonata\AdminBundle\Entity\Post, SonataAdminBundle:CRUD]
tags:
- {name: sonata.admin, manager_type: orm, group: admin, label: Post, show_in_dashboard: false}
public: true
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
dashboard:
groups:
news:
label: ~
label_catalogue: ~
items:
# just comment or remove the sonata.news.admin.post declaration to hide it from the menu.
# - sonata.news.admin.post
- route: blog_home
label: Blog
- sonata.news.admin.news
Keeping menu group open
-----------------------
You can add the ``keep_open`` option to menu group to keep that group always
open and ignore open/close effects:
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
dashboard:
groups:
sonata.admin.group.content:
keep_open: true
label: sonata_media
label_catalogue: SonataMediaBundle
icon: '<i class="fa fa-image"></i>'
items:
- sonata.media.admin.media
- sonata.media.admin.gallery
.. figure:: ../images/keep_open.png
:align: center
:alt: The navigation side bar with a group which uses "keep_open" option
Show menu item without treeview
-------------------------------
You can modify the menu to show menu item without treeview. You need to add option ``on_top`` in your admin services
or in sonata_admin dashboard group configuration:
.. code-block:: yaml
sonata_admin.admin.post:
class: Sonata\AdminBundle\Admin\PostAdmin
arguments: [~, Sonata\AdminBundle\Entity\Post, SonataAdminBundle:CRUD]
tags:
- {name: sonata.admin, manager_type: orm, group: admin, label: Post, on_top: true}
public: true
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
dashboard:
groups:
news:
on_top: true
label: ~
label_catalogue: ~
items:
- sonata.news.admin.post
.. figure:: ../images/demo_on_top.png
:align: center
:alt: on_top option
:width: 500
In this screenshot, we add ``on_top`` option to ``Tag`` and ``Blog Post`` admin services.
Your can't use this option for two or more items in the same time, for example:
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
dashboard:
groups:
news:
on_top: true
label: ~
label_catalogue: ~
items:
- sonata.news.admin.post
- route: blog_home
label: Blog
In this case you have an exception: "You can't use ``on_top`` option with multiple same name groups".
.. _KnpMenu: https://github.com/KnpLabs/KnpMenu
.. _Knp documentation: http://symfony.com/doc/current/bundles/KnpMenuBundle/index.html#create-your-first-menu
.. _Using events to allow a menu to be extended: http://symfony.com/doc/master/bundles/KnpMenuBundle/events.html

View File

@@ -0,0 +1,81 @@
Lock Protection
===============
Lock protection will prevent data corruption when multiple users edit an object at the same time.
Example
-------
1) Alice starts to edit the object
2) Bob starts to edit the object
3) Alice submits the form
4) Bob submits the form
In this case, a message will tell Bob that someone else has edited the object,
and that he must reload the page and apply the changes again.
Enable Lock Protection
----------------------
By default, lock protection is disabled.
You can enable it in your ``sonata_admin`` configuration :
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
options:
lock_protection: true
You must also configure each entity that you want to support by adding a field called ``$version`` on which the Doctrine ``Version`` feature is activated.
Using Annotations:
.. code-block:: php
<?php
// src/AppBundle/Entity/Car.php
namespace AppBundle\Entity\Car;
use Doctrine\ORM\Mapping as ORM;
class Car
{
// ...
/**
* @ORM\Column(type="integer")
* @ORM\Version
*/
protected $version;
// ...
}
Using XML:
.. code-block:: xml
<?xml version="1.0" encoding="utf-8"?>
<!-- src/AppBundle/Resources/orm/Car.orm.xml -->
<doctrine-mapping>
<entity name="AppBundle\Entity\Car">
<!-- ... -->
<field name="version" type="integer" version="true" />
<!-- ... -->
</entity>
</doctrine-mapping>
For more information about this visit the `Doctrine docs <http://doctrine-orm.readthedocs.org/en/latest/reference/transactions-and-concurrency.html?highlight=optimistic#optimistic-locking>`_
.. note::
If the object model manager does not support object locking,
the lock protection will not be triggered for the object.
Currently, only the ``SonataDoctrineORMAdminBundle`` supports it.

View File

@@ -0,0 +1,38 @@
Overwrite Admin Configuration
=============================
Sometime you might want to overwrite some Admin settings from vendors. This recipe will explain how to achieve this operation. However, keep in mind this operation is quite dangerous and might break code.
From the configuration file, you can add a new section named ``admin_services`` with the following templates:
.. code-block:: yaml
sonata_admin:
admin_services:
id.of.admin.service:
# service configuration
model_manager: sonata.admin.manager.doctrine_orm
form_contractor: sonata.admin.builder.doctrine_orm
show_builder: sonata.admin.builder.doctrine_orm
list_builder: sonata.admin.builder.doctrine_orm
datagrid_builder: sonata.admin.builder.doctrine_orm
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
label_translator_strategy: sonata.admin.label.strategy.native
# templates configuration
templates:
# view templates
view:
user_block: mytemplate.twig.html
# form related theme templates => this feature need to be implemented by the Persistency layer of each Admin Bundle
form: [ 'MyTheme.twig.html', 'MySecondTheme.twig.html']
filter: [ 'MyTheme.twig.html', 'MySecondTheme.twig.html']
With these settings you will be able to change default services and templates used by the `id.of.admin.service`` admin instance.

View File

@@ -0,0 +1,84 @@
Row templates
=============
Since Sonata-2.2 it is possible to define a template per row for the list action.
The default template is a standard table but there are circumstances where this
type of layout might not be suitable. By defining a custom template for the row,
you can tweak the layout into something like this:
.. figure:: ./../images/sonata_inline_row.png
:align: center
:alt: Inline Row from the SonataNewsBundle
:width: 700px
The recipe
----------
Configure your Admin service
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The configuration takes place in the DIC by calling the ``setTemplates`` method.
Two template keys need to be set:
- ``inner_list_row``: The template for the row, which you will customize. Often
you will want this to extend ``SonataAdminBundle:CRUD:base_list_flat_inner_row.html.twig``
- ``base_list_field``: The base template for the cell, the default of
``SonataAdminBundle:CRUD:base_list_flat_field.html.twig`` is suitable for most
cases but it can be customized if required.
.. configuration-block::
.. code-block:: xml
<service id="sonata.admin.comment" class="%sonata.admin.comment.class%">
<tag name="sonata.admin" manager_type="orm" group="sonata_blog" label="comments"
label_catalogue="%sonata.admin.comment.translation_domain%"
label_translator_strategy="sonata.admin.label.strategy.underscore" />
<argument />
<argument>%sonata.admin.comment.entity%</argument>
<argument>%sonata.admin.comment.controller%</argument>
<call method="setTemplates">
<argument type="collection">
<argument key="inner_list_row">
AppBundle:Admin:inner_row_comment.html.twig
</argument>
<argument key="base_list_field">
SonataAdminBundle:CRUD:base_list_flat_field.html.twig
</argument>
</argument>
</call>
</service>
Create your customized template
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Once the templates are defined, create the template to render the row:
.. code-block:: jinja
{# AppBundle:Admin:inner_row_comment.html.twig #}
{# Extend the default template, which provides batch and action cells #}
{# as well as the valid colspan computation #}
{% extends 'SonataAdminBundle:CRUD:base_list_flat_inner_row.html.twig' %}
{% block row %}
{# you can use fields defined in the the Admin class #}
{{ object|render_list_element(admin.list['name']) }} -
{{ object|render_list_element(admin.list['url']) }} -
{{ object|render_list_element(admin.list['email']) }} <br />
<small>
{# or you can use the object variable to render a property #}
{{ object.message }}
</small>
{% endblock %}
While this feature is nice to generate a rich list, it is also very easy to
break the layout and admin features such as batch and object actions. It is
best to familiarise yourself with the default templates and extend them where
possible, only changing what you need to customize.

View File

@@ -0,0 +1,73 @@
Select2
=======
The admin comes with `select2 <http://ivaynberg.github.io/select2/>`_ integration
since version 2.2.6. Select2 is a jQuery based replacement for select boxes.
It supports searching, remote data sets, and infinite scrolling of results.
The select2 is enabled on all ``select`` form elements by default.
Disable select2
---------------
If you don't want to use select2 in your admin, you can disable it in ``config.yml``.
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
options:
use_select2: false # disable select2
.. note::
If you disable select2, autocomplete form types will stop working.
Disable select2 on some form elements
-------------------------------------
To disable select2 on some ``select`` form element, set data attribute ``data-sonata-select2 = "false"`` to this form element.
.. code-block:: php
public function configureFormFields(FormMapper $formMapper)
{
$formMaper
->add('category', 'sonata_type_model', array(
'attr' => array(
'data-sonata-select2' => 'false'
)
))
;
}
.. note::
You have to use false as string! ``"false"``!
AllowClear
----------
Select2 parameter ``allowClear`` is handled automatically by admin. But if you want
to overload the default functionality, you can set data attribute ``data-sonata-select2-allow-clear="true"``
to enable ``allowClear`` or ``data-sonata-select2-allow-clear = "false"`` to disable the ``allowClear`` parameter.
.. code-block:: php
public function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('category', 'sonata_type_model', array(
'attr' => array(
'data-sonata-select2-allow-clear' => 'false'
)
))
;
}
.. note::
You have to use false as string! ``"false"``!

View File

@@ -0,0 +1,172 @@
Sortable behavior in admin listing
==================================
This is a full working example of how to implement a sortable feature in your Sonata admin listing
Background
----------
A sortable behavior is already available for one-to-many relationships (https://sonata-project.org/bundles/doctrine-orm-admin/master/doc/reference/form_field_definition.html#advanced-usage-one-to-many).
However there is no packaged solution to have some up and down arrows to sort
your records such as showed in the following screen
.. figure:: ../images/admin_sortable_listing.png
:align: center
:alt: Sortable listing
:width: 700px
Pre-requisites
--------------
Configuration
^^^^^^^^^^^^^
- you already have SonataAdmin and DoctrineORM up and running
- you already have an Entity class for which you want to implement a sortable feature. For the purpose of the example we are going to call it ``Client``.
- you already have an Admin set up, in this example we will call it ``ClientAdmin``
Bundles
^^^^^^^
- install ``gedmo/doctrine-extensions`` bundle in your project (check ``stof/doctrine-extensions-bundle`` for easier integration in your project) and enable the sortable feature in your config
- install ``pixassociates/sortable-behavior-bundle`` at least version ^1.1 in your project
The recipe
----------
First of all we are going to add a position field in our ``Client`` entity.
.. code-block:: php
/**
* @Gedmo\SortablePosition
* @ORM\Column(name="position", type="integer")
*/
private $position;
Then we need to inject the Sortable listener. If you only have the Gedmo bundle enabled, you only have to add the listener to your config.yml and skip this step.
.. code-block:: yaml
services:
gedmo.listener.sortable:
class: Gedmo\Sortable\SortableListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ "@annotation_reader" ] ]
If you have the ``stof/doctrine-extensions-bundle``, you only need to enable the sortable
feature in your config.yml such as
.. code-block:: yaml
stof_doctrine_extensions:
orm:
default:
sortable: true
In our ``ClientAdmin`` we are going to add a custom action in the ``configureListFields`` method
and use the default twig template provided in the ``pixSortableBehaviorBundle``
.. code-block:: php
$listMapper
->add('_action', null, array(
'actions' => array(
'move' => array(
'template' => 'PixSortableBehaviorBundle:Default:_sort.html.twig'
),
)
)
);
In order to add new routes for these actions we are also adding the following method
.. code-block:: php
<?php
// src/AppBundle/Admin/ClientAdmin.php
namespace AppBundle/Admin;
use Sonata\AdminBundle\Route\RouteCollection;
// ...
protected function configureRoutes(RouteCollection $collection)
{
// ...
$collection->add('move', $this->getRouterIdParameter().'/move/{position}');
}
Now you can update your ``services.yml`` to use the handler provider by the ``pixSortableBehaviorBundle``
.. code-block:: yaml
services:
app.admin.client:
class: AppBundle\Admin\ClientAdmin
tags:
- { name: sonata.admin, manager_type: orm, label: "Clients" }
arguments:
- ~
- AppBundle\Entity\Client
- 'PixSortableBehaviorBundle:SortableAdmin' # define the new controller via the third argument
public: true
Now we need to define the sort by field to be ``$position``:
.. code-block:: php
<?php
// src/AppBundle/Admin/ClientAdmin.php
namespace AppBundle\Admin;
use Sonata\AdminBundle\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Route\RouteCollection;
class ClientAdmin extends AbstractAdmin
{
protected $datagridValues = array(
'_page' => 1,
'_sort_order' => 'ASC',
'_sort_by' => 'position',
);
protected function configureRoutes(RouteCollection $collection)
{
// ...
$collection->add('move', $this->getRouterIdParameter().'/move/{position}');
}
// ...
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('name')
->add('enabled')
->add('_action', null, array(
'actions' => array(
'move' => array(
'template' => 'AppBundle:Admin:_sort.html.twig'
),
),
))
;
}
}
Enjoy ;)
Further work
------------
* handle ajax request
* interface for SonataAdminBundle

View File

@@ -0,0 +1,470 @@
Sortable Sonata Type Model in Admin
===================================
This is a full working example on how to implement a sortable feature in your Sonata admin form between two entities.
Background
----------
The sortable function is already available inside Sonata for the ``ChoiceType``. But the ``ModelType`` (or sonata_type_model) extends from choice, so this function is already available in our form type.
We just need some configuration to make it work.
The goal here is to fully configure a working example to handle the following need :
User got some expectations, but some are more relevant than the others.
Pre-requisites
--------------
Configuration
^^^^^^^^^^^^^
- you already have SonataAdmin and DoctrineORM up and running.
- you already have a ``UserBundle``.
- you already have ``User`` and ``Expectation`` Entities classes.
- you already have an ``UserAdmin`` and ``ExpectationAdmin`` set up.
The recipe
----------
Part 1 : Update the data model configuration
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The first thing to do is to update the Doctrine ORM configuration and create the join entity between ``User`` and ``Expectation``.
We are going to call this join entity ``UserHasExpectations``.
.. note::
We can't use a ``Many-To-Many`` relation here because the joined entity will required an extra field to handle ordering.
So we start by updating ``UserBundle/Resources/config/doctrine/User.orm.xml`` and adding a ``One-To-Many`` relation.
.. code-block:: xml
<one-to-many field="userHasExpectations" target-entity="UserBundle\Entity\UserHasExpectations" mapped-by="user" orphan-removal="true">
<cascade>
<cascade-persist />
</cascade>
<order-by>
<order-by-field name="position" direction="ASC" />
</order-by>
</one-to-many>
Then update ``UserBundle/Resources/config/doctrine/Expectation.orm.xml`` and also adding a ``One-To-Many`` relation.
.. code-block:: xml
<one-to-many field="userHasExpectations" target-entity="UserBundle\Entity\UserHasExpectations" mapped-by="expectation" orphan-removal="false">
<cascade>
<cascade-persist />
</cascade>
</one-to-many>
We now need to create the join entity configuration, create the following file in ``UserBundle/Resources/config/doctrine/UserHasExpectations.orm.xml``.
.. code-block:: xml
<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="UserBundle\Entity\UserHasExpectations" table="user__expectations">
<id name="id" type="integer">
<generator strategy="AUTO" />
</id>
<field name="position" column="position" type="integer">
<options>
<option name="default">0</option>
</options>
</field>
<many-to-one field="user" target-entity="UserBundle\Entity\User" inversed-by="userHasExpectations" orphan-removal="false">
<join-column name="user_id" referenced-column-name="id" on-delete="CASCADE"/>
</many-to-one>
<many-to-one field="expectation" target-entity="UserBundle\Entity\Expectation" inversed-by="userHasExpectations" orphan-removal="false">
<join-column name="expectation_id" referenced-column-name="id" on-delete="CASCADE"/>
</many-to-one>
</entity>
</doctrine-mapping>
Part 2 : Update the data model entities
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Update the ``UserBundle\Entity\User.php`` entity with the following :
.. code-block:: php
// ...
/**
* @var UserHasExpectations[]
*/
protected $userHasExpectations;
/**
* {@inheritdoc}
*/
public function __construct()
{
$this->userHasExpectations = new ArrayCollection;
}
/**
* @param ArrayCollection $userHasExpectations
*/
public function setUserHasExpectations(ArrayCollection $userHasExpectations)
{
$this->userHasExpectations = new ArrayCollection;
foreach ($userHasExpectations as $one) {
$this->addUserHasExpectations($one);
}
}
/**
* @return ArrayCollection
*/
public function getUserHasExpectations()
{
return $this->userHasExpectations;
}
/**
* @param UserHasExpectations $userHasExpectations
*/
public function addUserHasExpectations(UserHasExpectations $userHasExpectations)
{
$userHasExpectations->setUser($this);
$this->userHasExpectations[] = $userHasExpectations;
}
/**
* @param UserHasExpectations $userHasExpectations
*
* @return $this
*/
public function removeUserHasExpectations(UserHasExpectations $userHasExpectations)
{
$this->userHasExpectations->removeElement($userHasExpectations);
return $this;
}
// ...
Update the ``UserBundle\Entity\Expectation.php`` entity with the following :
.. code-block:: php
// ...
/**
* @var UserHasExpectations[]
*/
protected $userHasExpectations;
/**
* @param UserHasExpectations[] $userHasExpectations
*/
public function setUserHasExpectations($userHasExpectations)
{
$this->userHasExpectations = $userHasExpectations;
}
/**
* @return UserHasExpectations[]
*/
public function getUserHasExpectations()
{
return $this->userHasExpectations;
}
/**
* @return string
*/
public function __toString()
{
return $this->getLabel();
}
// ...
Create the ``UserBundle\Entity\UserHasExpectations.php`` entity with the following :
.. code-block:: php
<?php
namespace UserBundle\Entity;
class UserHasExpectations
{
/**
* @var integer $id
*/
protected $id;
/**
* @var User $user
*/
protected $user;
/**
* @var Expectation $expectation
*/
protected $expectation;
/**
* @var integer $position
*/
protected $position;
/**
* Get id
*
* @return integer $id
*/
public function getId()
{
return $this->id;
}
/**
* @return User
*/
public function getUser()
{
return $this->user;
}
/**
* @param User $user
*
* @return $this
*/
public function setUser(User $user)
{
$this->user = $user;
return $this;
}
/**
* @return Expectation
*/
public function getExpectation()
{
return $this->expectation;
}
/**
* @param Expectation $expectation
*
* @return $this
*/
public function setExpectation(Expectation $expectation)
{
$this->expectation = $expectation;
return $this;
}
/**
* @return int
*/
public function getPosition()
{
return $this->position;
}
/**
* @param int $position
*
* @return $this
*/
public function setPosition($position)
{
$this->position = $position;
return $this;
}
/**
* @return string
*/
public function __toString()
{
return (string) $this->getExpectation();
}
}
Part 3 : Update admin classes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This is a very important part, the admin class **should** be created for the join entity. If you don't do that, the field will never display properly.
So we are going to start by creating this ``UserBundle\Admin\UserHasExpectationsAdmin.php`` ...
.. code-block:: php
<?php
namespace UserBundle\Admin;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;
class UserHasExpectationsAdmin extends AbstractAdmin
{
/**
* @param \Sonata\AdminBundle\Form\FormMapper $formMapper
*/
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('expectation', 'sonata_type_model', array('required' => false))
->add('position', 'hidden')
;
}
/**
* @param \Sonata\AdminBundle\Datagrid\ListMapper $listMapper
*/
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->add('expectation')
->add('user')
->add('position')
;
}
}
... and define the service in ``UserBundle\Resources\config\admin.xml``.
.. code-block:: xml
<service id="user.admin.user_has_expectations" class="UserBundle\Admin\UserHasExpectationsAdmin">
<tag name="sonata.admin" manager_type="orm" group="UserHasExpectations" label="UserHasExpectations" />
<argument />
<argument>UserBundle\Entity\UserHasExpectations</argument>
<argument />
</service>
Now update the ``UserBundle\Admin\UserAdmin.php`` by adding the ``sonata_type_model`` field.
.. code-block:: php
/**
* {@inheritdoc}
*/
protected function configureFormFields(FormMapper $formMapper)
{
// ...
$formMapper
->add('userHasExpectations', 'sonata_type_model', array(
'label' => 'User\'s expectations',
'query' => $this->modelManager->createQuery('UserBundle\Entity\Expectation'),
'required' => false,
'multiple' => true,
'by_reference' => false,
'sortable' => true,
))
;
$formMapper->get('userHasExpectations')->addModelTransformer(new ExpectationDataTransformer($this->getSubject(), $this->modelManager));
}
There is two important things that we need to show here :
* We use the field ``userHasExpectations`` of the user, but we need a list of ``Expectation`` to be displayed, that's explain the use of ``query``.
* We want to persist ``UserHasExpectations``Entities, but we manage ``Expectation``, so we need to use a custom `ModelTransformer <http://symfony.com/doc/current/cookbook/form/data_transformers.html>`_ to deal with it.
Part 4 : Data Transformer
^^^^^^^^^^^^^^^^^^^^^^^^^
The last (but not least) step is create the ``UserBundle\Form\DataTransformer\ExpectationDataTransformer.php`` to handle the conversion of ``Expectation`` to ``UserHasExpectations``.
.. code-block:: php
<?php
namespace UserBundle\Form\DataTransformer;
class ExpectationDataTransformer implements Symfony\Component\Form\DataTransformerInterface
{
/**
* @var User $user
*/
private $user;
/**
* @var ModelManager $modelManager
*/
private $modelManager;
/**
* @param User $user
* @param ModelManager $modelManager
*/
public function __construct(User $user, ModelManager $modelManager)
{
$this->user = $user;
$this->modelManager = $modelManager;
}
/**
* {@inheritdoc}
*/
public function transform($data)
{
if (!is_null($data)) {
$results = [];
/** @var UserHasExpectations $userHasExpectations */
foreach ($data as $userHasExpectations) {
$results[] = $userHasExpectations->getExpectation();
}
return $results;
}
return $data;
}
/**
* {@inheritdoc}
*/
public function reverseTransform($expectations)
{
$results = new ArrayCollection;
$position = 0;
/** @var Expectation $expectation */
foreach ($expectations as $expectation) {
$userHasExpectations = $this->create();
$userHasExpectations->setExpectation($expectation);
$userHasExpectations->setPosition($position++);
$results->add($userHasExpectations);
}
// Remove Old values
$qb = $this->modelManager->getEntityManager()->createQueryBuilder();
$expr = $this->modelManager->getEntityManager()->getExpressionBuilder();
$userHasExpectationsToRemove = $qb->select('entity')
->from($this->getClass(), 'entity')
->where($expr->eq('entity.user', $this->user->getId()))
->getQuery()
->getResult();
foreach ($userHasExpectationsToRemove as $userHasExpectations) {
$this->modelManager->delete($userHasExpectations, false);
}
$this->modelManager->getEntityManager()->flush();
return $results;
}
}
Hope this will work for you :)

View File

@@ -0,0 +1,12 @@
Virtual Field Descriptions
==========================
Some fields including in the various Mappers don't rely on any actual field in
the Model (e.g., ``_action`` or ``batch``).
In order to prevent any side-effects when trying to retrieve the value of this
field (which doesn't exist), the option ``virtual_field`` is specified for these
fields.
When the template is instantiated, or whenever the value of the field is
required, ``null`` will simply be returned without prying on the Model itself.

View File

@@ -0,0 +1,229 @@
Creating an Admin
=================
You've been able to get the admin interface working in :doc:`the previous
chapter <installation>`. In this tutorial, you'll learn how to tell SonataAdmin
how an admin can manage your models.
Step 0: Create a Model
----------------------
For the rest of the tutorial, you'll need some sort of model. In this tutorial,
two very simple ``Post`` and ``Tag`` entities will be used. Generate them by
using these commands:
.. code-block:: bash
$ php bin/console doctrine:generate:entity --entity="AppBundle:Category" --fields="name:string(255)" --no-interaction
$ php bin/console doctrine:generate:entity --entity="AppBundle:BlogPost" --fields="title:string(255) body:text draft:boolean" --no-interaction
After this, you'll need to tweak the entities a bit:
.. code-block:: php
// src/AppBundle/Entity/BlogPost.php
// ...
class BlogPost
{
// ...
/**
* @ORM\ManyToOne(targetEntity="Category", inversedBy="blogPosts")
*/
private $category;
public function setCategory(Category $category)
{
$this->category = $category;
}
public function getCategory()
{
return $this->category;
}
// ...
}
Set the default value to ``false``.
.. code-block:: php
// src/AppBundle/Entity/BlogPost.php
// ...
class BlogPost
{
// ...
/**
* @var bool
*
* @ORM\Column(name="draft", type="boolean")
*/
private $draft = false;
// ...
}
.. code-block:: php
// src/AppBundle/Entity/Category.php
// ...
use Doctrine\Common\Collections\ArrayCollection;
// ...
class Category
{
// ...
/**
* @ORM\OneToMany(targetEntity="BlogPost", mappedBy="category")
*/
private $blogPosts;
public function __construct()
{
$this->blogPosts = new ArrayCollection();
}
public function getBlogPosts()
{
return $this->blogPosts;
}
// ...
}
After this, create the schema for these entities:
.. code-block:: bash
$ php bin/console doctrine:schema:create
.. note::
This article assumes you have basic knowledge of the Doctrine2 ORM and
you've set up a database correctly.
Step 1: Create an Admin Class
-----------------------------
SonataAdminBundle helps you manage your data using a graphical interface that
will let you create, update or search your model instances. The bundle relies
on Admin classes to know which models will be managed and how these actions
will look like.
An Admin class decides which fields to show on a listing, which fields are used
to find entries and how the create form will look like. Each model will have
its own Admin class.
Knowing this, let's create an Admin class for the ``Category`` entity. The
easiest way to do this is by extending ``Sonata\AdminBundle\Admin\AbstractAdmin``.
.. code-block:: php
// src/AppBundle/Admin/CategoryAdmin.php
namespace AppBundle\Admin;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Form\FormMapper;
class CategoryAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper->add('name', 'text');
}
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper->add('name');
}
protected function configureListFields(ListMapper $listMapper)
{
$listMapper->addIdentifier('name');
}
}
So, what does this code do?
* **Line 11-14**: These lines configure which fields are displayed on the edit
and create actions. The ``FormMapper`` behaves similar to the ``FormBuilder``
of the Symfony Form component;
* **Line 16-19**: This method configures the filters, used to filter and sort
the list of models;
* **Line 21-24**: Here you specify which fields are shown when all models are
listed (the ``addIdentifier()`` method means that this field will link to the
show/edit page of this particular model).
This is the most basic example of the Admin class. You can configure a lot more
with the Admin class. This will be covered by other, more advanced, articles.
Step 3: Register the Admin class
--------------------------------
You've now created an Admin class, but there is currently no way for the
SonataAdminBundle to know that this Admin class exists. To tell the
SonataAdminBundle of the existence of this Admin class, you have to create a
service and tag it with the ``sonata.admin`` tag:
.. code-block:: yaml
# app/config/services.yml
services:
# ...
admin.category:
class: AppBundle\Admin\CategoryAdmin
arguments: [~, AppBundle\Entity\Category, ~]
tags:
- { name: sonata.admin, manager_type: orm, label: Category }
public: true
The constructor of the base Admin class has many arguments. SonataAdminBundle
provides a compiler pass which takes care of configuring it correctly for you.
You can often tweak things using tag attributes. The code shown here is the
shortest code needed to get it working.
Step 4: Register SonataAdmin custom Routes
------------------------------------------
SonataAdminBundle generates routes for the Admin classes on the fly. To load these
routes, you have to make sure the routing loader of the SonataAdminBundle is executed:
.. code-block:: yaml
# app/config/routing.yml
# ...
_sonata_admin:
resource: .
type: sonata_admin
prefix: /admin
View the Category Admin Interface
---------------------------------
Now you've created the admin class for your category, you probably want to know
how this looks like in the admin interface. Well, let's find out by going to
http://localhost:8000/admin
.. image:: ../images/getting_started_category_dashboard.png
Feel free to play around and add some categories, like "Symfony" and "Sonata
Project". In the next chapters, you'll create an admin for the ``BlogPost``
entity and learn more about this class.
.. tip::
If you're not seeing the nice labels, but instead something like
"link_add", you should make sure that you've `enabled the translator`_.
.. _`enabled the translator`: http://symfony.com/doc/current/book/translation.html#configuration

View File

@@ -0,0 +1,231 @@
Installation
============
SonataAdminBundle is just a bundle and as such, you can install it at any
moment during a project's lifecycle.
1. Download the Bundle
----------------------
Open a command console, enter your project directory and execute the
following command to download the latest stable version of this bundle:
.. code-block:: bash
$ composer require sonata-project/admin-bundle
This command requires you to have Composer installed globally, as explained in
the `installation chapter`_ of the Composer documentation.
1.1. Download a Storage Bundle
------------------------------
You've now downloaded the SonataAdminBundle. While this bundle contains all
functionality, it needs storage bundles to be able to communicate with a
database. Before using the SonataAdminBundle, you have to download one of these
storage bundles. The official storage bundles are:
* `SonataDoctrineORMAdminBundle`_ (integrates the Doctrine ORM);
* `SonataDoctrineMongoDBAdminBundle`_ (integrates the Doctrine MongoDB ODM);
* `SonataPropelAdminBundle`_ (integrates Propel);
* `SonataDoctrinePhpcrAdminBundle`_ (integrates the Doctrine PHPCR ODM).
You can download them in the same way as the SonataAdminBundle. For instance,
to download the SonataDoctrineORMAdminBundle, execute the following command:
.. code-block:: bash
$ composer require sonata-project/doctrine-orm-admin-bundle
.. tip::
Don't know which to choose? Most new users prefer SonataDoctrineORMAdmin,
to interact with traditional relational databases (MySQL, PostgreSQL, etc).
Step 2: Enable the Bundle
-------------------------
Then, enable the bundle and the bundles it relies on by adding the following
line in the `app/AppKernel.php` file of your project:
.. code-block:: php
// app/AppKernel.php
// ...
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = array(
// ...
// The admin requires some twig functions defined in the security
// bundle, like is_granted. Register this bundle if it wasn't the case
// already.
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
// These are the other bundles the SonataAdminBundle relies on
new Sonata\CoreBundle\SonataCoreBundle(),
new Sonata\BlockBundle\SonataBlockBundle(),
new Knp\Bundle\MenuBundle\KnpMenuBundle(),
// And finally, the storage and SonataAdminBundle
new Sonata\AdminBundle\SonataAdminBundle(),
);
// ...
}
// ...
}
.. note::
If a bundle is already registered somewhere in your ``AppKernel.php``, you
should not register it again.
.. note::
Since version 2.3, the bundle comes with jQuery and other front-end
libraries. To update the versions (which isn't required), you can use
`Bower`_. To make sure you get the dependencies that match the version of
SonataAdminBundle you are using, you can make bower use the local bower
dependency file, like this:
.. code-block:: bash
$ bower install ./vendor/sonata-project/admin-bundle/bower.json
.. note::
You must enable translator service in `config.yml`.
.. code-block:: yaml
framework:
translator: { fallbacks: ["%locale%"] }
For more information: http://symfony.com/doc/current/translation.html#configuration
Step 3: Configure the Installed Bundles
---------------------------------------
Now all needed bundles are downloaded and registered, you have to add some
configuration. The admin interface is using SonataBlockBundle to put everything
in blocks. You just have to tell the block bundle about the existence of the
admin block:
.. code-block:: yaml
# app/config/config.yml
sonata_block:
default_contexts: [cms]
blocks:
# enable the SonataAdminBundle block
sonata.admin.block.admin_list:
contexts: [admin]
# ...
.. note::
Don't worry too much if, at this point, you don't yet understand fully
what a block is. The SonataBlockBundle is a useful tool, but it's not vital
that you understand it in order to use the admin bundle.
Step 4: Import Routing Configuration
------------------------------------
The bundles are now registered and configured correctly. Before you can use it,
the Symfony router needs to know the routes provided by the SonataAdminBundle.
You can do this by importing them in the routing configuration:
.. code-block:: yaml
# app/config/routing.yml
admin_area:
resource: "@SonataAdminBundle/Resources/config/routing/sonata_admin.xml"
prefix: /admin
Step 5: Enable the "translator" service
---------------------------------------
The translator service is required by SonataAdmin to display all labels properly.
.. code-block:: yaml
# app/config/config.yml
framework:
translator: { fallbacks: [en] }
Step 6: Define routes
---------------------
To be able to access SonataAdminBundle's pages, you need to add its routes
to your application's routing file:
.. configuration-block::
.. code-block:: yaml
# app/config/routing.yml
admin:
resource: '@SonataAdminBundle/Resources/config/routing/sonata_admin.xml'
prefix: /admin
_sonata_admin:
resource: .
type: sonata_admin
prefix: /admin
.. note::
If you're using XML or PHP to specify your application's configuration,
the above routing configuration must be placed in routing.xml or
routing.php according to your format (i.e. XML or PHP).
.. note::
For those curious about the ``resource: .`` setting: it is unusual syntax but used
because Symfony requires a resource to be defined (which points to a real file).
Once this validation passes Sonata's ``AdminPoolLoader`` is in charge of processing
this route and it simply ignores the resource setting.
At this point you can already access the (empty) admin dashboard by visiting the URL:
``http://yoursite.local/admin/dashboard``.
Step 7: Preparing your Environment
----------------------------------
As with all bundles you install, it's a good practice to clear the cache and
install the assets:
.. code-block:: bash
$ php bin/console cache:clear
$ php bin/console assets:install
The Admin Interface
-------------------
You've finished the installation process, congratulations. If you fire up the
server, you can now visit the admin page on http://localhost:8000/admin
.. note::
This tutorial assumes you are using the build-in server using the
``php bin/console server:start`` (or ``server:run``) command.
.. image:: ../images/getting_started_empty_dashboard.png
As you can see, the admin panel is very empty. This is because no bundle has
provided admin functionality for the admin bundle yet. Fortunately, you'll
learn how to do this in the :doc:`next chapter <creating_an_admin>`.
.. _`installation chapter`: https://getcomposer.org/doc/00-intro.md
.. _SonataDoctrineORMAdminBundle: http://sonata-project.org/bundles/doctrine-orm-admin/master/doc/index.html
.. _SonataDoctrineMongoDBAdminBundle: http://sonata-project.org/bundles/mongo-admin/master/doc/index.html
.. _SonataPropelAdminBundle: http://sonata-project.org/bundles/propel-admin/master/doc/index.html
.. _SonataDoctrinePhpcrAdminBundle: http://sonata-project.org/bundles/doctrine-phpcr-admin/master/doc/index.html
.. _Bower: http://bower.io/

View File

@@ -0,0 +1,279 @@
The Form View
=============
You've seen the absolute top of the iceberg in
:doc:`the previous chapter <creating_an_admin>`. But there is a lot more to
discover! In the coming chapters, you'll create an Admin class for the more
complex ``BlogPost`` model. Meanwhile, you'll learn how to make things a bit
more pretty.
Bootstrapping the Admin Class
-----------------------------
The basic class definition will look the same as the ``CategoryAdmin``:
.. code-block:: php
// src/AppBundle/Admin/BlogPostAdmin.php
namespace AppBundle\Admin;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;
class BlogPostAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
// ... configure $formMapper
}
protected function configureListFields(ListMapper $listMapper)
{
// ... configure $listMapper
}
}
The same applies to the service definition:
.. code-block:: yaml
# app/config/services.yml
services:
# ...
admin.blog_post:
class: AppBundle\Admin\BlogPostAdmin
arguments: [~, AppBundle\Entity\BlogPost, ~]
tags:
- { name: sonata.admin, manager_type: orm, label: Blog post }
public: true
Configuring the Form Mapper
---------------------------
If you already know the `Symfony Form component`_, the ``FormMapper`` will look
very similar.
You use the ``add()`` method to add fields to the form. The first argument is
the name of the property the field value maps to, the second argument is the
type of the field (see the `field type reference`_) and the third argument are
additional options to customize the form type. Only the first argument is
required as the Form component has type guessers to guess the type.
The ``BlogPost`` model has 4 properties: ``id``, ``title``, ``body``,
``category``. The ``id`` property's value is generated automatically by the
database. This means the form view just needs 3 fields: title, body and
category.
The title and body fields are simple "text" and "textarea" fields, you can add
them straight away:
.. code-block:: php
// src/AppBundle/Admin/BlogPostAdmin.php
// ...
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('title', 'text')
->add('body', 'textarea')
;
}
However, the category field will reference another model. How can you solve that?
Adding Fields that Reference Other Models
-----------------------------------------
You have a couple different choices on how to add fields that reference other
models. The most basic choice is to use the `entity field type`_ provided by
the DoctrineBundle. This will render a choice field with the available entities
as choice.
.. code-block:: php
// src/AppBundle/Admin/BlogPostAdmin.php
// ...
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
// ...
->add('category', 'entity', array(
'class' => 'AppBundle\Entity\Category',
'property' => 'name',
))
;
}
.. note::
The `property`_ option is not supported by Symfony >= 2.7. You should use `choice_label`_ instead.
As each blog post will only have one category, it renders as a select list:
.. image:: ../images/getting_started_entity_type.png
When an admin would like to create a new category, they need to go to the
category admin page and create a new category.
Using the Sonata Model Type
~~~~~~~~~~~~~~~~~~~~~~~~~~~
To make life easier for admins, you can use the
:ref:`sonata_type_model field type <field-types-model>`. This field type will
also render as a choice field, but it includes a create button to open a
dialog with the admin of the referenced model in it:
.. code-block:: php
// src/AppBundle/Admin/BlogPostAdmin.php
// ...
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
// ...
->add('category', 'sonata_type_model', array(
'class' => 'AppBundle\Entity\Category',
'property' => 'name',
))
;
}
.. image:: ../images/getting_started_sonata_model_type.png
Using Groups
------------
Currently, everything is put into one block. Since the form only has three
fields, it is still usable, but it can become quite a mess pretty quick. To
solve this, the form mapper also supports grouping fields together.
For instance, the title and body fields can belong to the Content group and the
category field to a Meta data group. To do this, use the ``with()`` method:
.. code-block:: php
// src/AppBundle/Admin/BlogPostAdmin.php
// ...
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->with('Content')
->add('title', 'text')
->add('body', 'textarea')
->end()
->with('Meta data')
->add('category', 'sonata_type_model', array(
'class' => 'AppBundle\Entity\Category',
'property' => 'name',
))
->end()
;
}
The first argument is the name/label of the group and the second argument is an
array of options. For instance, you can pass HTML classes to the group in
order to tweak the styling:
.. code-block:: php
// src/AppBundle/Admin/BlogPostAdmin.php
// ...
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->with('Content', array('class' => 'col-md-9'))
// ...
->end()
->with('Meta data', array('class' => 'col-md-3')
// ...
->end()
;
}
This will now result in a much nicer edit page:
.. image:: ../images/getting_started_post_edit_grid.png
Using Tabs
~~~~~~~~~~
If you get even more options, you can also use multiple tabs by using the
``tab()`` shortcut method:
.. code-block:: php
$formMapper
->tab('Post')
->with('Content', ...)
// ...
->end()
// ...
->end()
->tab('Publish Options')
// ...
->end()
;
Creating a Blog Post
--------------------
You've now finished your nice form view for the ``BlogPost`` model. Now it's
time to test it out by creating a post.
After pressing the "Create" button, you probably see a green message like:
*Item "AppBundle\Entity\BlogPost:00000000192ba93c000000001b786396" has been
successfully created.*
While it's very friendly of the SonataAdminBundle to notify the admin of a
successful creation, the classname and some sort of hash aren't really nice to
read. This is the default string representation of an object in the
SonataAdminBundle. You can change it by defining a ``toString()`` method in the
Admin class. This receives the object to transform to a string as the first parameter:
.. note::
No underscore prefix! ``toString()`` is correct!
.. code-block:: php
// src/AppBundle/Admin/BlogPostAdmin.php
// ...
use AppBundle\Entity\BlogPost;
class BlogPostAdmin extends AbstractAdmin
{
// ...
public function toString($object)
{
return $object instanceof BlogPost
? $object->getTitle()
: 'Blog Post'; // shown in the breadcrumb on the create view
}
}
Round Up
--------
In this tutorial, you've made your first contact with the greatest feature of
the SonataAdminBundle: Being able to customize literally everything. You've
started by creating a simple form and ended up with a nice edit page for your
admin.
In the :doc:`next chapter <the_list_view>`, you're going to look at the list
and datagrid actions.
.. _`Symfony Form component`: http://symfony.com/doc/current/book/forms.html
.. _`field type reference`: http://symfony.com/doc/current/reference/forms/types.html
.. _`entity field type`: http://symfony.com/doc/current/reference/forms/types/entity.html
.. _`choice_label`: http://symfony.com/doc/current/reference/forms/types/entity.html#choice-label
.. _`property`: http://symfony.com/doc/2.6/reference/forms/types/entity.html#property

View File

@@ -0,0 +1,215 @@
The List View
=============
You've given the admin a nice interface to create and edit blog posts and
categories. But there is not much to change if the admin doesn't have a list of
all available blog posts. Because of this, this chapter will teach you more
about the list view.
If you have created some posts and go to
http://localhost:8000/admin/app/blogpost/list, you'll see an empty list page.
That's not because there is no content, but because you didn't configure your
Admin's list view. As Sonata doesn't know which fields to show, it just shows
empty rows.
Configuring the List Mapper
---------------------------
Fixing the above problem is easy: Just add the fields you want to show on the
list page to the list view:
.. code-block:: php
// src/AppBundle/Admin/BlogPostAdmin.php
namespace AppBundle\Admin;
// ...
class BlogPostAdmin extends AbstractAdmin
{
// ...
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->add('title')
->add('draft')
;
}
}
Going to the list view of the blog post admin again, you'll see the available
blog posts:
.. image:: ../images/getting_started_basic_list_view.png
You can see that Sonata already guesses a correct field type and makes sure it
shows it in a friendly way. The boolean draft field for instance is show as a
red "no" block, indicating ``false``.
Cool! But... how can an admin go from this page to the edit page for a blog post?
There seem to be nothing that looks like a link. That's correct, you need to
tell Sonata which field(s) you want to use as a link.
Defining the Identifier Field(s)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The fields which contain a link to the edit pages are called identifier fields.
It makes sense to make the title field link to the edit page, so you can add it
as an identifier field. This is done by using ``ListMapper#addIdentifier()``
instead of ``ListMapper#add()``:
.. code-block:: php
// src/AppBundle/Admin/BlogPostAdmin.php
namespace AppBundle\Admin;
// ...
class BlogPostAdmin extends AbstractAdmin
{
// ...
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('title')
->add('draft')
;
}
}
When saving this, you can now see that the title field has the link you were
looking for.
Displaying Other Models
~~~~~~~~~~~~~~~~~~~~~~~
Now you probably also want the Category to be included in the list. To do that,
you need to reference it. You can't add the ``category`` field to the list
mapper, as it will then try to show the entity as a string. As you've learned
in the previous chapter, adding ``__toString`` to the entity is not recommended
as well.
Fortunately, there is an easy way to reference other models by using the dot
notation. Using this notation, you can specify which fields you want to show.
For instance, ``category.name`` will show the ``name`` property of the
category.
.. code-block:: php
// src/AppBundle/Admin/BlogPostAdmin.php
namespace AppBundle\Admin;
// ...
class BlogPostAdmin extends AbstractAdmin
{
// ...
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('title')
->add('category.name')
->add('draft')
;
}
}
Adding Filter/Search Options
----------------------------
Assume you had a very successful blog site containing many blog posts. After a
while, finding the blog post you wanted to edit would be like finding a needle
in a haystack. As with all user experience problems, Sonata provides a solution
for it!
It does this by allowing you to configure datagrid filters in the
``Admin#configureDatagridFilters()`` method. For instance, to allow the admin
to search blog posts by title (and also order them by alphabet in the list), you
would do something like:
.. code-block:: php
// src/AppBundle/Admin/BlogPostAdmin.php
namespace AppBundle\Admin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
// ...
class BlogPostAdmin extends AbstractAdmin
{
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper->add('title');
}
}
This will add a little block to the left of the block showing a search input
for the title field.
Filtering by Category
~~~~~~~~~~~~~~~~~~~~~
Filtering by another model's properties is a little bit more difficult. The add
field has 5 arguments:
.. code-block:: php
public function add(
$name,
// filter
$type = null,
array $filterOptions = array(),
// field
$fieldType = null,
$fieldOptions = null
)
As you can see, you can both customize the type used to filter and the type
used to display the search field. You can rely on the type guessing mechanism
of Sonata to pick the correct field types. However, you still need to configure
the search field to use the ``name`` property of the Category:
.. code-block:: php
// src/AppBundle/Admin/BlogPostAdmin.php
namespace AppBundle\Admin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
// ...
class BlogPostAdmin extends AbstractAdmin
{
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('title')
->add('category', null, array(), 'entity', array(
'class' => 'AppBundle\Entity\Category',
'choice_label' => 'name', // In Symfony2: 'property' => 'name'
))
;
}
}
With this code, a dropdown will be shown including all available categories.
This will make it easy to filter by category.
.. image:: ../images/getting_started_filter_category.png
Round Up
--------
This time, you've learned how to make it easy to find posts to edit. You've
learned how to create a nice list view and how to add options to search, order
and filter this list.
There might have been some very difficult things, but imagine the difficulty
writing everything yourself! As you're now already quite good with the basics,
you can start reading other articles in the documentation, like:
* :doc:`Customizing the Dashboard <../reference/dashboard>`
* :doc:`Configuring the Security system <../reference/security>`
* :doc:`Adding export functionality <../reference/action_export>`
* :doc:`Adding a preview page <../reference/preview_mode>`

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

View File

@@ -0,0 +1,98 @@
Admin Bundle
============
**SonataAdminBundle is split into 5 bundles:**
* SonataAdminBundle: contains core libraries and services
* `SonataDoctrineORMAdminBundle <https://github.com/sonata-project/SonataDoctrineORMAdminBundle>`_: integrates Doctrine ORM project with the core admin bundle
* `SonataDoctrineMongoDBAdminBundle <https://github.com/sonata-project/SonataDoctrineMongoDBAdminBundle>`_: integrates MongoDB with the core admin bundle (early stage)
* `SonataDoctrinePhpcrAdminBundle <https://github.com/sonata-project/SonataDoctrinePhpcrAdminBundle>`_: integrates PHPCR with the core admin bundle (early stage)
* `SonataPropelAdminBundle <https://github.com/sonata-project/SonataPropelAdminBundle>`_: integrates Propel with the core admin bundle (early stage)
The demo website can be found at http://demo.sonata-project.org.
**Usage examples:**
* `SonataMediaBundle <https://github.com/sonata-project/SonataMediaBundle>`_: a media manager bundle
* `SonataNewsBundle <https://github.com/sonata-project/SonataNewsBundle>`_: a news/blog bundle
* `SonataPageBundle <https://github.com/sonata-project/SonataPageBundle>`_: a page (CMS like) bundle
* `SonataUserBundle <https://github.com/sonata-project/SonataUserBundle>`_: integration of FOSUserBundle and SonataAdminBundle
.. toctree::
:caption: Getting Started
:name: getting-started
:maxdepth: 1
:numbered:
getting_started/installation
getting_started/creating_an_admin
getting_started/the_form_view
getting_started/the_list_view
.. toctree::
:caption: Reference Guide
:name: reference-guide
:maxdepth: 1
:numbered:
reference/installation
reference/getting_started
reference/configuration
reference/architecture
reference/child_admin
reference/dashboard
reference/search
reference/action_list
reference/action_create_edit
reference/action_show
reference/action_delete
reference/action_export
reference/saving_hooks
reference/form_types
reference/form_help_message
reference/field_types
reference/batch_actions
reference/console
reference/troubleshooting
reference/breadcrumbs
.. toctree::
:caption: Advanced Options
:name: advanced-options
:maxdepth: 1
:numbered:
reference/routing
reference/translation
reference/conditional_validation
reference/templates
reference/security
reference/extensions
reference/events
reference/advanced_configuration
reference/annotations
reference/preview_mode
.. toctree::
:caption: Cookbook
:name: cookbook
:maxdepth: 1
:numbered:
cookbook/recipe_select2
cookbook/recipe_knp_menu
cookbook/recipe_file_uploads
cookbook/recipe_image_previews
cookbook/recipe_row_templates
cookbook/recipe_sortable_listing
cookbook/recipe_dynamic_form_modification
cookbook/recipe_custom_action
cookbook/recipe_customizing_a_mosaic_list
cookbook/recipe_overwrite_admin_configuration
cookbook/recipe_improve_performance_large_datasets
cookbook/recipe_virtual_field
cookbook/recipe_bootlint
cookbook/recipe_lock_protection
cookbook/recipe_sortable_sonata_type_model
cookbook/recipe_delete_field_group
cookbook/recipe_data_mapper

View File

@@ -0,0 +1,209 @@
Creating and Editing objects
============================
.. note::
This document is a stub representing a new work in progress. If you're reading
this you can help contribute, **no matter what your experience level with Sonata
is**. Check out the `issues on Github`_ for more information about how to get involved.
This document will cover the Create and Edit actions. It will cover configuration
of the fields and forms available in these views and any other relevant settings.
Basic configuration
-------------------
SonataAdmin Options that may affect the create or edit view:
.. code-block:: yaml
sonata_admin:
options:
html5_validate: true # enable or disable html5 form validation
confirm_exit: true # enable or disable a confirmation before navigating away
use_select2: true # enable or disable usage of the Select2 jQuery library
use_icheck: true # enable or disable usage of the iCheck library
use_bootlint: false # enable or disable usage of Bootlint
use_stickyforms: true # enable or disable the floating buttons
form_type: standard # can also be 'horizontal'
templates:
edit: SonataAdminBundle:CRUD:edit.html.twig
tab_menu_template: SonataAdminBundle:Core:tab_menu_template.html.twig
For more information about optional libraries:
- Select2: https://github.com/select2/select2
- iCheck: http://icheck.fronteed.com/
- Bootlint: https://github.com/twbs/bootlint#in-the-browser
.. note::
**TODO**:
* options available when adding fields, inc custom templates
Routes
~~~~~~
You can disable creating or editing entities by removing the corresponding routes in your Admin.
For more detailed information about routes, see :doc:`routing`.
.. code-block:: php
<?php
// src/AppBundle/Admin/PersonAdmin.php
class PersonAdmin extends AbstractAdmin
{
// ...
protected function configureRoutes(RouteCollection $collection): void
{
/* Removing the edit route will disable editing entities. It will also
* use the 'show' view as default link on the identifier columns in the list view.
*/
$collection->remove('edit');
/* Removing the create route will disable creating new entities. It will also
* remove the 'Add new' button in the list view.
*/
$collection->remove('create');
}
// ...
}
Adding form fields
------------------
Within the configureFormFields method you can define which fields should
be shown when editing or creating entities.
Each field has to be added to a specific form group. And form groups can
optionally be added to a tab. See `FormGroup options`_ for additional
information about configuring form groups.
Using the FormMapper add method, you can add form fields. The add method
has 4 parameters:
- ``name``: The name of your entity.
- ``type``: The type of field to show; by defaults this is ``null`` to let
Sonata decide which type to use. See :doc:`Field Types <field_types>`
for more information on available types.
- ``options``: The form options to be used for the field. These may differ
per type. See :doc:`Field Types <field_types>` for more information on
available options.
- ``fieldDescriptionOptions``: The field description options. Options here
are passed through to the field template. See :ref:`Form Types, FieldDescription
options <form_types_fielddescription_options>` for more information.
.. note::
The property entered in ``name`` should be available in your Entity
through getters/setters or public access.
.. code-block:: php
<?php
// src/AppBundle/Admin/PersonAdmin.php
class PersonAdmin extends AbstractAdmin
{
// ...
protected function configureFormFields(FormMapper $formMapper): void
{
$formMapper
->tab('General') // The tab call is optional
->with('Addresses')
->add('title') // Add a field and let Sonata decide which type to use
->add('streetname', TextType::class) // Add a textfield
->add('housenumber', NumberType::class) // Add a number field
->add('housenumberAddition', TextType::class, ['required' => false]) // Add a non-required text field
->end() // End form group
->end() // End tab
;
}
// ...
}
FormGroup options
~~~~~~~~~~~~~~~~~
When adding a form group to your edit/create form, you may specify some
options for the group itself.
- ``collapsed``: unused at the moment
- ``class``: The class for your form group in the admin; by default, the
value is set to ``col-md-12``.
- ``fields``: The fields in your form group (you should NOT override this
unless you know what you're doing).
- ``box_class``: The class for your form group box in the admin; by default,
the value is set to ``box box-primary``.
- ``description``: A text shown at the top of the form group.
- ``translation_domain``: The translation domain for the form group title
(the Admin translation domain is used by default).
To specify options, do as follows:
.. code-block:: php
<?php
// src/AppBundle/Admin/PersonAdmin.php
class PersonAdmin extends AbstractAdmin
{
// ...
public function configureFormFields(FormMapper $formMapper): void
{
$formMapper
->tab('General') // the tab call is optional
->with('Addresses', [
'class' => 'col-md-8',
'box_class' => 'box box-solid box-danger',
'description' => 'Lorem ipsum',
// ...
])
->add('title')
// ...
->end()
->end()
;
}
// ...
}
Here is an example of what you can do with customizing the box_class on
a group:
.. figure:: ../images/box_class.png
:align: center
:alt: Box Class
:width: 500
Embedding other Admins
----------------------
.. note::
**TODO**:
* how to embed one Admin in another (1:1, 1:M, M:M)
* how to access the right object(s) from the embedded Admin's code
Customizing just one of the actions
-----------------------------------
.. note::
**TODO**:
* how to create settings/fields that appear on just one of the create/edit views
* and any controller changes needed to manage them
.. _`issues on GitHub`: https://github.com/sonata-project/SonataAdminBundle/issues/1519

View File

@@ -0,0 +1,23 @@
Deleting objects
================
.. note::
This document is a stub representing a new work in progress. If you're reading
this you can help contribute, **no matter what your experience level with Sonata
is**. Check out the `issues on GitHub`_ for more information about how to get involved.
This document will cover the Delete action and any related configuration options.
Basic configuration
-------------------
.. note::
**TODO**:
* global (yml) options that affect the delete action
* a note about Routes and how disabling them disables the related action
* any Admin configuration options that affect delete
* a note about lifecycle events triggered by delete?
.. _`issues on Github`: https://github.com/sonata-project/SonataAdminBundle/issues/1519

View File

@@ -0,0 +1,100 @@
The Export action
=================
This document will cover the Export action and related configuration options.
Basic configuration
-------------------
If you have registered the ``SonataExporterBundle`` bundle in the kernel of your application,
you can benefit from a lot of flexibility:
* You can configure default exporters globally.
* You can add custom exporters, also globally.
* You can configure every default writer.
See `the exporter bundle documentation`_ for more information.
Translation
~~~~~~~~~~~
All field names are translated by default.
An internal mechanism checks if a field matching the translator strategy label exists in the current translation file
and will use the field name as a fallback.
Picking which fields to export
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
By default, all fields are exported. More accurately, it depends on the
persistence backend you are using, but for instance, the doctrine ORM backend
exports all fields (associations are not exported). If you want to change this
behavior for a specific admin, you can override the ``getExportFields()`` method:
.. code-block:: php
<?php
public function getExportFields()
{
return array('givenName', 'familyName', 'contact.phone');
}
.. note::
Note that you can use `contact.phone` to access the `phone` property of `Contact` entity
You can also tweak the list by creating an admin extension that implements the
``configureExportFields()`` method.
.. code-block:: php
public function configureExportFields(AdminInterface $admin, array $fields)
{
unset($fields['updatedAt']);
return $fields;
}
Overriding the export formats for a specific admin
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Changing the export formats can be done by defining a ``getExportFormats()``
method in your admin class.
.. code-block:: php
<?php
public function getExportFormats()
{
return array('pdf', 'html');
}
Customizing the query used to fetch the results
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you want to customize the query used to fetch the results for a specific admin,
you can override the ``getDataSourceIterator()`` method:
.. code-block:: php
<?php
// src/AppBundle/Admin/PersonAdmin.php
class PersonAdmin extends AbstractAdmin
{
public function getDataSourceIterator()
{
$iterator = parent::getDataSourceIterator();
$iterator->setDateTimeFormat('d/m/Y'); //change this to suit your needs
return $iterator;
}
}
.. note::
**TODO**:
* customising the templates used to render the output
* publish the exporter documentation on the project's website and update the link
.. _`the exporter bundle documentation`: https://github.com/sonata-project/exporter/blob/1.x/docs/reference/symfony.rst

View File

@@ -0,0 +1,645 @@
The List View
=============
.. note::
This document is a stub representing a new work in progress. If you're reading
this you can help contribute, **no matter what your experience level with Sonata
is**. Check out the `issues on GitHub`_ for more information about how to get involved.
This document will cover the List view which you use to browse the objects in your
system. It will cover configuration of the list itself and the filters you can use
to control what's visible.
Basic configuration
-------------------
SonataAdmin Options that may affect the list view:
.. code-block:: yaml
sonata_admin:
templates:
list: SonataAdminBundle:CRUD:list.html.twig
action: SonataAdminBundle:CRUD:action.html.twig
select: SonataAdminBundle:CRUD:list__select.html.twig
list_block: SonataAdminBundle:Block:block_admin_list.html.twig
short_object_description: SonataAdminBundle:Helper:short-object-description.html.twig
batch: SonataAdminBundle:CRUD:list__batch.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
.. note::
**TODO**:
* a note about Routes and how disabling them disables the related action
* adding custom columns
Customizing the fields displayed on the list page
-------------------------------------------------
You can customize the columns displayed on the list through the ``configureListFields`` method.
Here is an example:
.. code-block:: php
<?php
// ...
public function configureListFields(ListMapper $listMapper)
{
$listMapper
// addIdentifier allows to specify that this column
// will provide a link to the entity
// (edit or show route, depends on your access rights)
->addIdentifier('name')
// you may specify the field type directly as the
// second argument instead of in the options
->add('isVariation', 'boolean')
// if null, the type will be guessed
->add('enabled', null, array(
'editable' => true
))
// editable association field
->add('status', 'choice', array(
'editable' => true,
'class' => 'Vendor\ExampleBundle\Entity\ExampleStatus',
'choices' => array(
1 => 'Active',
2 => 'Inactive',
3 => 'Draft',
),
))
// we can add options to the field depending on the type
->add('price', 'currency', array(
'currency' => $this->currencyDetector->getCurrency()->getLabel()
))
// Here we specify which property is used to render the label of each entity in the list
->add('productCategories', null, array(
'associated_property' => 'name')
)
// you may also use dotted-notation to access
// specific properties of a relation to the entity
->add('image.name')
// You may also specify the actions you want to be displayed in the list
->add('_action', null, array(
'actions' => array(
'show' => array(),
'edit' => array(),
'delete' => array(),
)
))
;
}
Options
^^^^^^^
.. note::
* ``(m)`` stands for mandatory
* ``(o)`` stands for optional
- ``type`` (m): defines the field type - mandatory for the field description itself but will try to detect the type automatically if not specified
- ``template`` (o): the template used to render the field
- ``label`` (o): the name used for the column's title
- ``link_parameters`` (o): add link parameter to the related Admin class when the ``Admin::generateUrl`` is called
- ``code`` (o): the method name to retrieve the related value (for example,
if you have an `array` type field, you would like to show info prettier
than `[0] => 'Value'`; useful when simple getter is not enough).
Notice: works with string-like types (string, text, html)
- ``associated_property`` (o): property path to retrieve the "string" representation of the collection element, or a closure with the element as argument and return a string.
- ``identifier`` (o): if set to true a link appears on the value to edit the element
Available types and associated options
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. note::
``(m)`` means that option is mandatory
+-----------+----------------+-----------------------------------------------------------------------+
| Type | Options | Description |
+===========+================+=======================================================================+
| actions | actions | List of available actions |
+-----------+----------------+-----------------------------------------------------------------------+
| batch | | Renders a checkbox |
+-----------+----------------+-----------------------------------------------------------------------+
| select | | Renders a select box |
+-----------+----------------+-----------------------------------------------------------------------+
| array | | Displays an array |
+-----------+----------------+-----------------------------------------------------------------------+
| boolean | ajax_hidden | Yes/No; ajax_hidden allows to hide list field during an AJAX context. |
+ +----------------+-----------------------------------------------------------------------+
| | editable | Yes/No; editable allows to edit directly from the list if authorized. |
+ +----------------+-----------------------------------------------------------------------+
| | inverse | Yes/No; reverses the background color (green for false, red for true) |
+-----------+----------------+-----------------------------------------------------------------------+
| choice | choices | Possible choices |
+ +----------------+-----------------------------------------------------------------------+
| | multiple | Is it a multiple choice option? Defaults to false. |
+ +----------------+-----------------------------------------------------------------------+
| | delimiter | Separator of values if multiple. |
+ +----------------+-----------------------------------------------------------------------+
| | catalogue | Translation catalogue. |
+ +----------------+-----------------------------------------------------------------------+
| | class | Class path for editable association field. |
+-----------+----------------+-----------------------------------------------------------------------+
| currency | currency (m) | A currency string (EUR or USD for instance). |
+-----------+----------------+-----------------------------------------------------------------------+
| date | format | A format understandable by Twig's ``date`` function. |
+-----------+----------------+-----------------------------------------------------------------------+
| datetime | format | A format understandable by Twig's ``date`` function. |
+-----------+----------------+-----------------------------------------------------------------------+
| email | as_string | Renders the email as string, without any link. |
+ +----------------+-----------------------------------------------------------------------+
| | subject | Add subject parameter to email link. |
+ +----------------+-----------------------------------------------------------------------+
| | body | Add body parameter to email link. |
+-----------+----------------+-----------------------------------------------------------------------+
| percent | | Renders value as a percentage. |
+-----------+----------------+-----------------------------------------------------------------------+
| string | | Renders a simple string. |
+-----------+----------------+-----------------------------------------------------------------------+
| text | | See 'string' |
+-----------+----------------+-----------------------------------------------------------------------+
| html | | Renders string as html |
+-----------+----------------+-----------------------------------------------------------------------+
| time | | Renders a datetime's time with format ``H:i:s``. |
+-----------+----------------+-----------------------------------------------------------------------+
| trans | catalogue | Translates the value with catalogue ``catalogue`` if defined. |
+-----------+----------------+-----------------------------------------------------------------------+
| url | url | Adds a link with url ``url`` to the displayed value |
+ +----------------+-----------------------------------------------------------------------+
| | route | Give a route to generate the url |
+ + + +
| | name | Route name |
+ + + +
| | parameters | Route parameters |
+ +----------------+-----------------------------------------------------------------------+
| | hide_protocol | Hide http:// or https:// (default: false) |
+-----------+----------------+-----------------------------------------------------------------------+
If you have the SonataDoctrineORMAdminBundle installed, you have access to more field types, see `SonataDoctrineORMAdminBundle Documentation <https://sonata-project.org/bundles/doctrine-orm-admin/master/doc/reference/list_field_definition.html>`_.
.. note::
It is better to prefer non negative notions when possible for boolean
values so use the ``inverse`` option if you really cannot find a good enough
antonym for the name you have.
Customizing the query used to generate the list
-----------------------------------------------
You can customize the list query thanks to the ``createQuery`` method.
.. code-block:: php
<?php
public function createQuery($context = 'list')
{
$query = parent::createQuery($context);
$query->andWhere(
$query->expr()->eq($query->getRootAliases()[0] . '.my_field', ':my_param')
);
$query->setParameter('my_param', 'my_value');
return $query;
}
Customizing the sort order
--------------------------
Configure the default ordering in the list view
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Configuring the default ordering column can simply be achieved by overriding
the ``datagridValues`` array property. All three keys ``_page``, ``_sort_order`` and
``_sort_by`` can be omitted.
.. code-block:: php
<?php
// src/AppBundle/Admin/PostAdmin.php
use Sonata\AdminBundle\Admin\AbstractAdmin;
class PostAdmin extends AbstractAdmin
{
// ...
protected $datagridValues = array(
// display the first page (default = 1)
'_page' => 1,
// reverse order (default = 'ASC')
'_sort_order' => 'DESC',
// name of the ordered field (default = the model's id field, if any)
'_sort_by' => 'updatedAt',
);
// ...
}
.. note::
The ``_sort_by`` key can be of the form ``mySubModel.mySubSubModel.myField``.
.. note::
**TODO**: how to sort by multiple fields (this might be a separate recipe?)
Filters
-------
You can add filters to let user control which data will be displayed.
.. code-block:: php
<?php
// src/AppBundle/Admin/PostAdmin.php
use Sonata\AdminBundle\Datagrid\DatagridMapper;
class ClientAdmin extends AbstractAdmin
{
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('phone')
->add('email')
;
}
// ...
}
All filters are hidden by default for space-saving. User has to check which filter he wants to use.
To make the filter always visible (even when it is inactive), set the parameter
``show_filter`` to ``true``.
.. code-block:: php
<?php
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('phone')
->add('email', null, array(
'show_filter' => true
))
// ...
;
}
By default the template generates an ``operator`` for a filter which defaults to ``sonata_type_equal``.
Though this ``operator_type`` is automatically detected it can be changed or even be hidden:
.. code-block:: php
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('foo', null, array(
'operator_type' => 'sonata_type_boolean'
))
->add('bar', null, array(
'operator_type' => 'hidden'
))
// ...
;
}
If you don't need the advanced filters, or all your ``operator_type`` are hidden, you can disable them by setting
``advanced_filter`` to ``false``. You need to disable all advanced filters to make the button disappear.
.. code-block:: php
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('bar', null, array(
'operator_type' => 'hidden',
'advanced_filter' => false
))
// ...
;
}
Default filters
^^^^^^^^^^^^^^^
Default filters can be added to the datagrid values by using the ``configureDefaultFilterValues`` method.
A filter has a ``value`` and an optional ``type``. If no ``type`` is given the default type ``is equal`` is used.
.. code-block:: php
public function configureDefaultFilterValues(array &$filterValues)
{
$filterValues['foo'] = array(
'type' => ChoiceFilter::TYPE_CONTAINS,
'value' => 'bar',
);
}
Available types are represented through classes which can be found here:
https://github.com/sonata-project/SonataCoreBundle/tree/master/Form/Type
Types like ``equal`` and ``boolean`` use constants to assign a choice of ``type`` to an ``integer`` for its ``value``:
.. code-block:: php
<?php
// SonataCoreBundle/Form/Type/EqualType.php
namespace Sonata\CoreBundle\Form\Type;
class EqualType extends AbstractType
{
const TYPE_IS_EQUAL = 1;
const TYPE_IS_NOT_EQUAL = 2;
}
The integers are then passed in the URL of the list action e.g.:
**/admin/user/user/list?filter[enabled][type]=1&filter[enabled][value]=1**
This is an example using these constants for an ``boolean`` type:
.. code-block:: php
use Sonata\UserBundle\Admin\Model\UserAdmin as SonataUserAdmin;
use Sonata\CoreBundle\Form\Type\EqualType;
use Sonata\CoreBundle\Form\Type\BooleanType;
class UserAdmin extends SonataUserAdmin
{
protected $datagridValues = array(
'enabled' => array(
'type' => EqualType::TYPE_IS_EQUAL, // => 1
'value' => BooleanType::TYPE_YES // => 1
)
);
}
Please note that setting a ``false`` value on a the ``boolean`` type will not work since the type expects an integer of ``2`` as ``value`` as defined in the class constants:
.. code-block:: php
<?php
// SonataCoreBundle/Form/Type/BooleanType.php
namespace Sonata\CoreBundle\Form\Type;
class BooleanType extends AbstractType
{
const TYPE_YES = 1;
const TYPE_NO = 2;
}
Default filters can also be added to the datagrid values by overriding the ``getFilterParameters`` method.
.. code-block:: php
use Sonata\CoreBundle\Form\Type\EqualType;
use Sonata\CoreBundle\Form\Type\BooleanType;
class UserAdmin extends SonataUserAdmin
{
public function getFilterParameters()
{
$this->datagridValues = array_merge(array(
'enabled' => array (
'type' => EqualType::TYPE_IS_EQUAL,
'value' => BooleanType::TYPE_YES
)
), $this->datagridValues);
return parent::getFilterParameters();
}
}
This approach is useful when you need to create dynamic filters.
.. code-block:: php
class PostAdmin extends SonataUserAdmin
{
public function getFilterParameters()
{
// Assuming security context injected
if (!$this->securityContext->isGranted('ROLE_ADMIN')) {
$user = $this->securityContext->getToken()->getUser();
$this->datagridValues = array_merge(array(
'author' => array (
'type' => EqualType::TYPE_IS_EQUAL,
'value' => $user->getId()
)
), $this->datagridValues);
}
return parent::getFilterParameters();
}
}
Please note that this is not a secure approach to hide posts from others. It's just an example for setting filters on demand.
Callback filter
^^^^^^^^^^^^^^^
If you have the **SonataDoctrineORMAdminBundle** installed you can use the ``doctrine_orm_callback`` filter type e.g. for creating a full text filter:
.. code-block:: php
use Sonata\UserBundle\Admin\Model\UserAdmin as SonataUserAdmin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
class UserAdmin extends SonataUserAdmin
{
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('full_text', CallbackFilter::class, array(
'callback' => array($this, 'getFullTextFilter'),
'field_type' => 'text'
))
// ...
;
}
public function getFullTextFilter($queryBuilder, $alias, $field, $value)
{
if (!$value['value']) {
return;
}
// Use `andWhere` instead of `where` to prevent overriding existing `where` conditions
$queryBuilder->andWhere($queryBuilder->expr()->orX(
$queryBuilder->expr()->like($alias.'.username', $queryBuilder->expr()->literal('%' . $value['value'] . '%')),
$queryBuilder->expr()->like($alias.'.firstName', $queryBuilder->expr()->literal('%' . $value['value'] . '%')),
$queryBuilder->expr()->like($alias.'.lastName', $queryBuilder->expr()->literal('%' . $value['value'] . '%'))
));
return true;
}
}
You can also get the filter type which can be helpful to change the operator type of your condition(s):
.. code-block:: php
use Sonata\CoreBundle\Form\Type\EqualType;
class UserAdmin extends SonataUserAdmin
{
public function getFullTextFilter($queryBuilder, $alias, $field, $value)
{
if (!$value['value']) {
return;
}
$operator = $value['type'] == EqualType::TYPE_IS_EQUAL ? '=' : '!=';
$queryBuilder
->andWhere($alias.'.username '.$operator.' :username')
->setParameter('username', $value['value'])
;
return true;
}
}
.. note::
**TODO**:
* basic filter configuration and options
* targeting submodel fields using dot-separated notation
* advanced filter options (global_search)
Visual configuration
--------------------
You have the possibility to configure your List View to customize the render without overriding to whole template.
You can :
- `header_style`: Customize the style of header (width, color, background, align...)
- `header_class`: Customize the class of the header
- `collapse`: Allow to collapse long text fields with a "read more" link
- `row_align`: Customize the alignment of the rendered inner cells
- `label_icon`: Add an icon before label
.. code-block:: php
<?php
public function configureListFields(ListMapper $list)
{
$list
->add('id', null, array(
'header_style' => 'width: 5%; text-align: center',
'row_align' => 'center'
))
->add('name', 'text', array(
'header_style' => 'width: 35%'
)
->add('description', 'text', array(
'header_style' => 'width: 35%',
'collapse' => true
)
->add('upvotes', null, array(
'label_icon' => 'fa fa-thumbs-o-up'
)
->add('actions', null, array(
'header_class' => 'customActions',
'row_align' => 'right'
)
// ...
;
}
If you want to customise the `collapse` option, you can also give an array to override the default parameters.
.. code-block:: php
// ...
->add('description', 'text', array(
'header_style' => 'width: 35%',
'collapse' => array(
'height' => 40, // height in px
'read_more' => 'I want to see the full description', // content of the "read more" link
'read_less' => 'This text is too long, reduce the size' // content of the "read less" link
)
)
// ...
If you want to show only the `label_icon`:
.. code-block:: php
// ...
->add('upvotes', null, array(
'label' => false,
'label_icon' => 'fa fa-thumbs-o-up'
)
// ...
.. _`issues on GitHub`: https://github.com/sonata-project/SonataAdminBundle/issues/1519
Mosaic view button
------------------
You have the possibility to show/hide mosaic view button.
.. code-block:: yaml
sonata_admin:
# for hide mosaic view button on all screen using `false`
show_mosaic_button: true
You can show/hide mosaic view button using admin service configuration. You need to add option ``show_mosaic_button``
in your admin services:
.. code-block:: yaml
sonata_admin.admin.post:
class: Sonata\AdminBundle\Admin\PostAdmin
arguments: [~, Sonata\AdminBundle\Entity\Post, ~]
tags:
- { name: sonata.admin, manager_type: orm, group: admin, label: Post, show_mosaic_button: true }
sonata_admin.admin.news:
class: Sonata\AdminBundle\Admin\NewsAdmin
arguments: [~, Sonata\AdminBundle\Entity\News, ~]
tags:
- { name: sonata.admin, manager_type: orm, group: admin, label: News, show_mosaic_button: false }
Checkbox range selection
------------------------
.. tip::
You can check / uncheck a range of checkboxes by clicking a first one,
then a second one with shift + click.

View File

@@ -0,0 +1,180 @@
The Show action
===============
.. note::
This document is a stub representing a new work in progress. If you're reading
this you can help contribute, **no matter what your experience level with Sonata
is**. Check out the `issues on GitHub`_ for more information about how to get involved.
This document will cover the Show action and related configuration options.
Basic configuration
-------------------
.. note::
**TODO**:
* a note about Routes and how disabling them disables the related action
* a note about lifecycle events triggered by delete?
* options available when adding general fields, inc custom templates
* targeting submodel fields using dot-separated notation
* (Note, if this is very similar to the form documentation it can be combined)
Group options
~~~~~~~~~~~~~
When adding a group to your show page, you may specify some options for the group itself.
- ``collapsed``: unused at the moment
- ``class``: the class for your group in the admin; by default, the value is set to ``col-md-12``.
- ``fields``: the fields in your group (you should NOT override this unless you know what you're doing).
- ``box_class``: the class for your group box in the admin; by default, the value is set to ``box box-primary``.
- ``description``: to complete
- ``translation_domain``: to complete
To specify options, do as follow:
.. code-block:: php
<?php
// src/AppBundle/Admin/PersonAdmin.php
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Show\ShowMapper;
class PersonAdmin extends AbstractAdmin
{
public function configureShowFields(ShowMapper $showMapper)
{
$showMapper
->tab('General') // the tab call is optional
->with('Addresses', array(
'class' => 'col-md-8',
'box_class' => 'box box-solid box-danger',
'description' => 'Lorem ipsum',
))
->add('title')
// ...
->end()
->end()
;
}
}
When extending an existing Admin, you may want to remove some fields, groups or tabs.
Here is an example of how to achieve this :
.. code-block:: php
<?php
// src/AppBundle/Admin/PersonAdmin.php
use Sonata\AdminBundle\Show\ShowMapper;
class PersonAdmin extends ParentAdmin
{
public function configureShowFields(ShowMapper $showMapper)
{
parent::configureShowFields($showMapper);
// remove just one field
$showMapper->remove('field_to_remove');
// remove a group from the "default" tab
$showMapper->removeGroup('GroupToRemove1');
// remove a group from a specific tab
$showMapper->removeGroup('GroupToRemove2', 'Tab2');
// remove a group from a specific tab and also remove the tab if it ends up being empty
$showMapper->removeGroup('GroupToRemove3', 'Tab3', true);
}
}
Customising the query used to show the object from within your Admin class
--------------------------------------------------------------------------
Setting up a showAction is pretty much the same as a form, which we did in the initial setup.
It is actually a bit easier, because we are only concerned with displaying information.
Smile, the hard part is already done.
The following is a working example of a ShowAction
.. code-block:: php
<?php
// src/AppBundle/Admin/PostAdmin.php
use Sonata\AdminBundle\Show\ShowMapper;
class ClientAdmin extends AbstractAdmin
{
protected function configureShowFields(ShowMapper $showMapper)
{
// here we set the fields of the ShowMapper variable,
// $showMapper (but this can be called anything)
$showMapper
// The default option is to just display the
// value as text (for boolean this will be 1 or 0)
->add('name')
->add('phone')
->add('email')
// The boolean option is actually very cool
// true shows a check mark and the 'yes' label
// false shows a check mark and the 'no' label
->add('dateCafe', 'boolean')
->add('datePub', 'boolean')
->add('dateClub', 'boolean')
;
}
}
.. tip::
To customize the displayed label of a show field you can use the ``label`` option:
.. code-block:: php
$showMapper->add('name', null, array('label' => 'UserName'));
Setting this option to ``false`` will make the label empty.
Setting up a custom show template (very useful)
===============================================
The first thing you need to do is define it in app/config/config/yml:
.. configuration-block::
.. code-block:: yaml
sonata_admin:
title: Acme
title_logo: img/logo_small.png
templates:
show: AppBundle:Admin:Display_Client.html.twig
Once you have defined this, Sonata Admin looks for it in the following location:
``src/AppBundle/Resources/views/Admin/Display_Client.html.twig``
Now that you have told Sonata Admin where to find the template, it is time to put one in there.
The recommended way to start is to copy the default template, and paste it into its new home.
This ensures that you can update Sonata Admin and keep all of your hard work.
The original template can be found in the following location:
``vendor/sonata-project/admin-bundle/Resources/views/CRUD/base_show.html.twig``
Now that you have a copy of the default template, check to make sure it works.
That's it, now go code.
.. _`issues on GitHub`: https://github.com/sonata-project/SonataAdminBundle/issues/1519

View File

@@ -0,0 +1,455 @@
Advanced configuration
======================
Service Configuration
---------------------
When you create a new Admin service you can configure its dependencies, the services which are injected by default are:
========================= =============================================
Dependencies Service Id
========================= =============================================
model_manager sonata.admin.manager.%manager-type%
form_contractor sonata.admin.builder.%manager-type%_form
show_builder sonata.admin.builder.%manager-type%_show
list_builder sonata.admin.builder.%manager-type%_list
datagrid_builder sonata.admin.builder.%manager-type%_datagrid
translator translator
configuration_pool sonata.admin.pool
router router
validator validator
security_handler sonata.admin.security.handler
menu_factory knp_menu.factory
route_builder sonata.admin.route.path_info | sonata.admin.route.path_info_slashes
label_translator_strategy sonata.admin.label.strategy.form_component
========================= =============================================
.. note::
%manager-type% is to be replaced by the manager type (orm, doctrine_mongodb...),
and the default route_builder depends on it.
You have 2 ways of defining the dependencies inside ``services.xml``:
* With a tag attribute, less verbose:
.. configuration-block::
.. code-block:: xml
<service id="app.admin.project" class="AppBundle\Admin\ProjectAdmin">
<tag
name="sonata.admin"
manager_type="orm"
group="Project"
label="Project"
label_translator_strategy="sonata.admin.label.strategy.native"
route_builder="sonata.admin.route.path_info"
/>
<argument />
<argument>AppBundle\Entity\Project</argument>
<argument />
</service>
.. configuration-block::
.. code-block:: yaml
app.admin.project:
class: AppBundle\Admin\ProjectAdmin
tags:
- { name: sonata.admin, manager_type: orm, group: "Project", label: "Project", label_translator_strategy: "sonata.admin.label.strategy.native", route_builder: "sonata.admin.route.path_info" }
arguments:
- ~
- AppBundle\Entity\Project
- ~
public: true
* With a method call, more verbose
.. configuration-block::
.. code-block:: xml
<service id="app.admin.project" class="AppBundle\Admin\ProjectAdmin">
<tag
name="sonata.admin"
manager_type="orm"
group="Project"
label="Project"
/>
<argument />
<argument>AppBundle\Entity\Project</argument>
<argument />
<call method="setLabelTranslatorStrategy">
<argument type="service" id="sonata.admin.label.strategy.native" />
</call>
<call method="setRouteBuilder">
<argument type="service" id="sonata.admin.route.path_info" />
</call>
</service>
.. configuration-block::
.. code-block:: yaml
app.admin.project:
class: AppBundle\Admin\ProjectAdmin
tags:
- { name: sonata.admin, manager_type: orm, group: "Project", label: "Project" }
arguments:
- ~
- AppBundle\Entity\Project
- ~
calls:
- [ setLabelTranslatorStrategy, [ "@sonata.admin.label.strategy.native" ]]
- [ setRouteBuilder, [ "@sonata.admin.route.path_info" ]]
public: true
If you want to modify the service that is going to be injected, add the following code to your
application's config file:
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
admins:
sonata_admin:
sonata.order.admin.order: # id of the admin service this setting is for
model_manager: # dependency name, from the table above
sonata.order.admin.order.manager # customised service id
Creating a custom RouteBuilder
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To create your own RouteBuilder create the PHP class and register it as a service:
* php Route Generator
.. code-block:: php
<?php
namespace AppBundle\Route;
use Sonata\AdminBundle\Builder\RouteBuilderInterface;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Route\PathInfoBuilder;
use Sonata\AdminBundle\Route\RouteCollection;
class EntityRouterBuilder extends PathInfoBuilder implements RouteBuilderInterface
{
/**
* @param AdminInterface $admin
* @param RouteCollection $collection
*/
public function build(AdminInterface $admin, RouteCollection $collection)
{
parent::build($admin, $collection);
$collection->add('yourSubAction');
// The create button will disappear, delete functionality will be disabled as well
// No more changes needed!
$collection->remove('create');
$collection->remove('delete');
}
}
* xml service registration
.. configuration-block::
.. code-block:: xml
<service id="app.admin.entity_route_builder" class="AppBundle\Route\EntityRouterBuilder">
<argument type="service" id="sonata.admin.audit.manager" />
</service>
* YAML service registration
.. configuration-block::
.. code-block:: yaml
services:
app.admin.entity_route_builder:
class: AppBundle\Route\EntityRouterBuilder
arguments:
- "@sonata.admin.audit.manager"
Inherited classes
-----------------
You can manage inherited classes by injecting subclasses using the service configuration.
Lets consider a base class named `Person` and its subclasses `Student` and `Teacher`:
.. configuration-block::
.. code-block:: xml
<service id="app.admin.person" class="AppBundle\Admin\PersonAdmin">
<tag name="sonata.admin" manager_type="orm" group="admin" label="Person" />
<argument/>
<argument>AppBundle\Entity\Person</argument>
<argument></argument>
<call method="setSubClasses">
<argument type="collection">
<argument key="student">AppBundle\Entity\Student</argument>
<argument key="teacher">AppBundle\Entity\Teacher</argument>
</argument>
</call>
</service>
You will just need to change the way forms are configured in order to take into account these new subclasses:
.. code-block:: php
<?php
// src/AppBundle/Admin/PersonAdmin.php
protected function configureFormFields(FormMapper $formMapper)
{
$subject = $this->getSubject();
$formMapper
->add('name')
;
if ($subject instanceof Teacher) {
$formMapper->add('course', 'text');
}
elseif ($subject instanceof Student) {
$formMapper->add('year', 'integer');
}
}
Tab Menu
--------
ACL
^^^
Though the route linked by a menu may be protected the Tab Menu will not automatically check the ACl for you.
The link will still appear unless you manually check it using the `hasAccess` method:
.. code-block:: php
<?php
protected function configureTabMenu(MenuItemInterface $menu, $action, AdminInterface $childAdmin = null)
{
// Link will always appear even if it is protected by ACL
$menu->addChild($this->trans('Show'), array('uri' => $admin->generateUrl('show', array('id' => $id))));
// Link will only appear if access to ACL protected URL is granted
if ($this->hasAccess('edit')) {
$menu->addChild($this->trans('Edit'), array('uri' => $admin->generateUrl('edit', array('id' => $id))));
}
}
Dropdowns
^^^^^^^^^
You can use dropdowns inside the Tab Menu by default. This can be achieved by using
the `'dropdown' => true` attribute:
.. code-block:: php
<?php
// src/AppBundle/Admin/PersonAdmin.php
protected function configureTabMenu(MenuItemInterface $menu, $action, AdminInterface $childAdmin = null)
{
// other tab menu stuff ...
$menu->addChild('comments', array('attributes' => array('dropdown' => true)));
$menu['comments']->addChild('list', array('uri' => $admin->generateUrl('listComment', array('id' => $id))));
$menu['comments']->addChild('create', array('uri' => $admin->generateUrl('addComment', array('id' => $id))));
}
If you want to use the Tab Menu in a different way, you can replace the Menu Template:
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
templates:
tab_menu_template: AppBundle:Admin:own_tab_menu_template.html.twig
Translations
^^^^^^^^^^^^
The translation parameters and domain can be customised by using the
``translation_domain`` and ``translation_parameters`` keys of the extra array
of data associated with the item, respectively.
.. code-block:: php
<?php
$menuItem->setExtras(array(
'translation_parameters' => array('myparam' => 'myvalue'),
'translation_domain' => 'My domain',
));
You can also set the translation domain on the menu root, and children will
inherit it :
.. code-block:: php
<?php
$menu->setExtra('translation_domain', 'My domain');
Filter parameters
^^^^^^^^^^^^^^^^^
You can add or override filter parameters to the Tab Menu:
.. code-block:: php
<?php
use Knp\Menu\ItemInterface as MenuItemInterface;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\CoreBundle\Form\Type\EqualType;
class DeliveryAdmin extends AbstractAdmin
{
protected function configureTabMenu(MenuItemInterface $menu, $action, AdminInterface $childAdmin = null)
{
if (!$childAdmin && !in_array($action, array('edit', 'show', 'list'))) {
return;
}
if ($action == 'list') {
// Get current filter parameters
$filterParameters = $this->getFilterParameters();
// Add or override filter parameters
$filterParameters['status'] = array(
'type' => EqualType::TYPE_IS_EQUAL, // => 1
'value' => Delivery::STATUS_OPEN,
);
// Add filters to uri of tab
$menu->addChild('List open deliveries', array('uri' => $this->generateUrl('list', array(
'filter' => $filterParameters,
))));
return;
}
}
}
The `Delivery` class is based on the `sonata_type_translatable_choice` example inside the Core's documentation:
http://sonata-project.org/bundles/core/master/doc/reference/form_types.html#sonata-type-translatable-choice
Actions Menu
------------
You can add custom items to the actions menu for a specific action by overriding the following method:
.. code-block:: php
public function configureActionButtons(AdminInterface $admin, $list, $action, $object)
{
if (in_array($action, array('show', 'edit', 'acl')) && $object) {
$list['custom'] = array(
'template' => 'AppBundle:Button:custom_button.html.twig',
);
}
// Remove history action
unset($list['history']);
return $list;
}
.. figure:: ../images/custom_action_buttons.png
:align: center
:alt: Custom action buttons
Disable content stretching
--------------------------
You can disable ``html``, ``body`` and ``sidebar`` elements stretching. These containers are forced
to be full height by default. If you use custom layout or just don't need such behavior,
add ``no-stretch`` class to the ``<html>`` tag.
For example:
.. code-block:: html+jinja
{# src/AppBundle/Resources/views/standard_layout.html.twig #}
{% block html_attributes %}class="no-js no-stretch"{% endblock %}
Custom Action Access Management
-------------------------------
You can customize the access system inside the CRUDController by adding some entries inside the `$accessMapping` array in the linked Admin.
.. code-block:: php
<?php
// src/AppBundle/Admin/PostAdmin.php
class CustomAdmin extends AbstractAdmin
{
protected $accessMapping = array(
'myCustomFoo' => 'EDIT',
'myCustomBar' => array('EDIT', 'LIST'),
);
}
<?php
// src/AppBundle/Controller/CustomCRUDController.php
class CustomCRUDController extends CRUDController
{
public function myCustomFooAction()
{
$this->admin->checkAccess('myCustomFoo');
// If you can't access to EDIT role for the linked admin, an AccessDeniedException will be thrown
// ...
}
public function myCustomBarAction($object)
{
$this->admin->checkAccess('myCustomBar', $object);
// If you can't access to EDIT AND LIST roles for the linked admin, an AccessDeniedException will be thrown
// ...
}
// ...
}
You can also fully customize how you want to handle your access management by simply overriding ``checkAccess`` function
.. code-block:: php
<?php
// src/AppBundle/Admin/CustomAdmin.php
class CustomAdmin extends AbstractAdmin
{
public function checkAccess($action, $object = null)
{
$this->customAccessLogic();
}
// ...
}

View File

@@ -0,0 +1,77 @@
Annotations
===========
All annotations require jms/di-extra-bundle, it can easily be installed by composer:
.. code-block:: bash
composer require jms/di-extra-bundle
if you want to know more: http://jmsyst.com/bundles/JMSDiExtraBundle
The annotations get registered with JMSDiExtraBundle automatically if it is installed.
If you need to disable this for some reason, you can do this via the configuration:
.. configuration-block::
.. code-block:: yaml
sonata_admin:
options:
enable_jms_di_extra_autoregistration: false
.. note::
Starting with version 4.0, SonataAdminBundle will no longer register
annotations with JMSDiExtraBundle automatically. Please add the following to
your config.yml to register the annotations yourself:
.. code-block:: yaml
jms_di_extra:
annotation_patterns:
- JMS\DiExtraBundle\Annotation
- Sonata\AdminBundle\Annotation
Define Admins
^^^^^^^^^^^^^
All you have to do is include ``Sonata\AdminBundle\Annotation`` and define the values you need.
.. code-block:: php
<?php
namespace AcmeBundle\Admin;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Annotation as Sonata;
/**
* @Sonata\Admin(
* class="AcmeBundle\Entity\MyEntity",
* id="service id (generated per default)",
* managerType="doctrine_mongodb (orm per default)",
* baseControllerName="SonataAdminBundle:CRUD",
* group="myGroup",
* label="myLabel",
* showInDashboard=true,
* translationDomain="OMG",
* pagerType="",
* persistFilters="",
* icon="<i class='fa fa-folder'></i>",
* keepOpen=false,
* onTop=false
* )
*/
class MyAdmin extends AbstractAdmin
{
}
.. note::
If you need to define custom controllers you can also use jms/di-extra-bundle by using
the DI\Service annotation.

View File

@@ -0,0 +1,317 @@
Architecture
============
The architecture of the ``SonataAdminBundle`` is primarily inspired by the Django Admin
Project, which is truly a great project. More information can be found at the
`Django Project Website`_.
If you followed the instructions on the :doc:`getting_started` page, you should by
now have an ``Admin`` class and an ``Admin`` service. In this chapter, we'll discuss more in
depth how it works.
The Admin Class
---------------
The ``Admin`` class maps a specific model to the rich CRUD interface provided by
``SonataAdminBundle``. In other words, using your ``Admin`` classes, you can configure
what is shown by ``SonataAdminBundle`` in each CRUD action for the associated model.
By now you've seen 3 of those actions in the ``getting started`` page: list,
filter and form (for creation/editing). However, a fully configured ``Admin`` class
can define more actions:
============= =========================================================================
Actions Description
============= =========================================================================
list The fields displayed in the list table
filter The fields available for filtering the list
form The fields used to create/edit the entity
show The fields used to show the entity
batch actions Actions that can be performed on a group of entities (e.g. bulk delete)
============= =========================================================================
The ``Sonata\AdminBundle\Admin\AbstractAdmin`` class is provided as an easy way to
map your models, by extending it. However, any implementation of the
``Sonata\AdminBundle\Admin\AdminInterface`` can be used to define an ``Admin``
service. For each ``Admin`` service, the following required dependencies are
automatically injected by the bundle:
========================= =========================================================================
Class Description
========================= =========================================================================
ConfigurationPool configuration pool where all Admin class instances are stored
ModelManager service which handles specific code relating to your persistence layer (e.g. Doctrine ORM)
FormContractor builds the forms for the edit/create views using the Symfony ``FormBuilder``
ShowBuilder builds the show fields
ListBuilder builds the list fields
DatagridBuilder builds the filter fields
Request the received http request
RouteBuilder allows you to add routes for new actions and remove routes for default actions
RouterGenerator generates the different URLs
SecurityHandler handles permissions for model instances and actions
Validator handles model validation
Translator generates translations
LabelTranslatorStrategy a strategy to use when generating labels
MenuFactory generates the side menu, depending on the current action
========================= =========================================================================
.. note::
Each of these dependencies is used for a specific task, briefly described above.
If you wish to learn more about how they are used, check the respective documentation
chapter. In most cases, you won't need to worry about their underlying implementation.
All of these dependencies have default values that you can override when declaring any of
your ``Admin`` services. This is done using a ``call`` to the matching ``setter`` :
.. configuration-block::
.. code-block:: xml
<service id="app.admin.post" class="AppBundle\Admin\PostAdmin">
<tag name="sonata.admin" manager_type="orm" group="Content" label="Post" />
<argument />
<argument>AppBundle\Entity\Post</argument>
<argument />
<call method="setLabelTranslatorStrategy">
<argument type="service" id="sonata.admin.label.strategy.underscore" />
</call>
</service>
.. code-block:: yaml
services:
app.admin.post:
class: AppBundle\Admin\PostAdmin
tags:
- { name: sonata.admin, manager_type: orm, group: "Content", label: "Post" }
arguments:
- ~
- AppBundle\Entity\Post
- ~
calls:
- [ setLabelTranslatorStrategy, ["@sonata.admin.label.strategy.underscore"]]
public: true
Here, we declare the same ``Admin`` service as in the :doc:`getting_started` chapter, but using a
different label translator strategy, replacing the default one. Notice that
``sonata.admin.label.strategy.underscore`` is a service provided by ``SonataAdminBundle``,
but you could just as easily use a service of your own.
CRUDController
--------------
The ``CRUDController`` contains the actions you have available to manipulate
your model instances, like create, list, edit or delete. It uses the ``Admin``
class to determine its behavior, like which fields to display in the edit form,
or how to build the list view. Inside the ``CRUDController``, you can access the
``Admin`` class instance via the ``$admin`` variable.
.. note::
`CRUD`_ is an acronym for "Create, Read, Update and Delete"
The ``CRUDController`` is no different from any other Symfony controller, meaning
that you have all the usual options available to you, like getting services from
the Dependency Injection Container (DIC).
This is particularly useful if you decide to extend the ``CRUDController`` to
add new actions or change the behavior of existing ones. You can specify which controller
to use when declaring the ``Admin`` service by passing it as the 3rd argument. For example
to set the controller to ``AppBundle:PostAdmin``:
.. configuration-block::
.. code-block:: xml
<service id="app.admin.post" class="AppBundle\Admin\PostAdmin">
<tag name="sonata.admin" manager_type="orm" group="Content" label="Post" />
<argument />
<argument>AppBundle\Entity\Post</argument>
<argument>AppBundle:PostAdmin</argument>
<call method="setTranslationDomain">
<argument>AppBundle</argument>
</call>
</service>
.. code-block:: yaml
services:
app.admin.post:
class: AppBundle\Admin\PostAdmin
tags:
- { name: sonata.admin, manager_type: orm, group: "Content", label: "Post" }
arguments:
- ~
- AppBundle\Entity\Post
- AppBundle:PostAdmin
calls:
- [ setTranslationDomain, [AppBundle]]
public: true
When extending ``CRUDController``, remember that the ``Admin`` class already has
a set of automatically injected dependencies that are useful when implementing several
scenarios. Refer to the existing ``CRUDController`` actions for examples of how to get
the best out of them.
In your overloaded CRUDController you can overload also these methods to limit
the number of duplicated code from SonataAdmin:
* ``preCreate``: called from ``createAction``
* ``preEdit``: called from ``editAction``
* ``preDelete``: called from ``deleteAction``
* ``preShow``: called from ``showAction``
* ``preList``: called from ``listAction``
These methods are called after checking the access rights and after retrieving the object
from database. You can use them if you need to redirect user to some other page under certain conditions.
Fields Definition
-----------------
Your ``Admin`` class defines which of your model's fields will be available in each
action defined in your ``CRUDController``. So, for each action, a list of field mappings
is generated. These lists are implemented using the ``FieldDescriptionCollection`` class
which stores instances of ``FieldDescriptionInterface``. Picking up on our previous
``PostAdmin`` class example:
.. code-block:: php
<?php
// src/AppBundle/Admin/PostAdmin.php
namespace AppBundle\Admin;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;
class PostAdmin extends AbstractAdmin
{
// Fields to be shown on create/edit forms
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('title', 'text', array(
'label' => 'Post Title'
))
->add('author', 'entity', array(
'class' => 'AppBundle\Entity\User'
))
// if no type is specified, SonataAdminBundle tries to guess it
->add('body')
// ...
;
}
// Fields to be shown on filter forms
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('title')
->add('author')
;
}
// Fields to be shown on lists
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('title')
->add('slug')
->add('author')
;
}
// Fields to be shown on show action
protected function configureShowFields(ShowMapper $showMapper)
{
$showMapper
->add('id')
->add('title')
->add('slug')
->add('author')
;
}
}
Internally, the provided ``Admin`` class will use these three functions to create three
``FieldDescriptionCollection`` instances:
* ``$formFieldDescriptions``, containing three ``FieldDescriptionInterface`` instances
for title, author and body
* ``$filterFieldDescriptions``, containing two ``FieldDescriptionInterface`` instances
for title and author
* ``$listFieldDescriptions``, containing three ``FieldDescriptionInterface`` instances
for title, slug and author
* ``$showFieldDescriptions``, containing four ``FieldDescriptionInterface`` instances
for id, title, slug and author
The actual ``FieldDescription`` implementation is provided by the storage abstraction
bundle that you choose during the installation process, based on the
``BaseFieldDescription`` abstract class provided by ``SonataAdminBundle``.
Each ``FieldDescription`` contains various details about a field mapping. Some of
them are independent of the action in which they are used, like ``name`` or ``type``,
while others are used only in specific actions. More information can be found in the
``BaseFieldDescription`` class file.
In most scenarios, you will not actually need to handle the ``FieldDescription`` yourself.
However, it is important that you know it exists and how it is used, as it sits at the
core of ``SonataAdminBundle``.
Templates
---------
Like most actions, ``CRUDController`` actions use view files to render their output.
``SonataAdminBundle`` provides ready to use views as well as ways to easily customize them.
The current implementation uses ``Twig`` as the template engine. All templates
are located in the ``Resources/views`` directory of the bundle.
There are two base templates, one of these is ultimately used in every action:
* ``SonataAdminBundle::standard_layout.html.twig``
* ``SonataAdminBundle::ajax_layout.html.twig``
Like the names say, one if for standard calls, the other one for AJAX.
The subfolders include Twig files for specific sections of ``SonataAdminBundle``:
Block:
``SonataBlockBundle`` block views. By default there is only one, which
displays all the mapped classes on the dashboard
Button:
Buttons such as ``Add new`` or ``Delete`` that you can see across several
CRUD actions
CRUD:
Base views for every CRUD action, plus several field views for each field type
Core:
Dashboard view, together with deprecated and stub twig files.
Form:
Views related to form rendering
Helper:
A view providing a short object description, as part of a specific form field
type provided by ``SonataAdminBundle``
Pager:
Pagination related view files
These will be discussed in greater detail in the specific :doc:`templates` section, where
you will also find instructions on how to configure ``SonataAdminBundle`` to use your templates
instead of the default ones.
Managing ``Admin`` Service
--------------------------
Your ``Admin`` service definitions are parsed when Symfony is loaded, and handled by
the ``Pool`` class. This class, available as the ``sonata.admin.pool`` service from the
DIC, handles the ``Admin`` classes, lazy-loading them on demand (to reduce overhead)
and matching each of them to a group. It is also responsible for handling the top level
template files, administration panel title and logo.
.. _`Django Project Website`: http://www.djangoproject.com/
.. _`CRUD`: http://en.wikipedia.org/wiki/CRUD

View File

@@ -0,0 +1,250 @@
Batch actions
=============
Batch actions are actions triggered on a set of selected objects. By default,
Admins have a ``delete`` action which allows you to remove several entries at once.
Defining new actions
--------------------
To create a new custom batch action which appears in the list view follow these steps:
Override ``configureBatchActions()`` in your ``Admin`` class to define the new batch actions
by adding them to the ``$actions`` array. Each key represent a batch action and could contain these settings:
- **label**: The name to use when offering this option to users, should be passed through the translator
(default: the label is generated via the labelTranslatorStrategy)
- **translation_domain**: The domain which will be used to translate the key.
(default: the translation domain of the admin)
- **ask_confirmation**: defaults to true and means that the user will be asked
for confirmation before the batch action is processed
For example, lets define a new ``merge`` action which takes a number of source items and
merges them onto a single target item. It should only be available when two conditions are met:
- the EDIT and DELETE routes exist for this Admin (have not been disabled)
- the logged in administrator has EDIT and DELETE permissions
.. code-block:: php
<?php
// in your Admin class
public function configureBatchActions($actions)
{
if (
$this->hasRoute('edit') && $this->hasAccess('edit') &&
$this->hasRoute('delete') && $this->hasAccess('delete')
) {
$actions['merge'] = array(
'ask_confirmation' => true
);
}
return $actions;
}
Define the core action logic
----------------------------
The method ``batchAction<MyAction>`` will be executed to process your batch in your ``CRUDController`` class. The selected
objects are passed to this method through a query argument which can be used to retrieve them.
If for some reason it makes sense to perform your batch action without the default selection
method (for example you defined another way, at template level, to select model at a lower
granularity), the passed query is ``null``.
.. note::
You can check how to declare your own ``CRUDController`` class in the Architecture section.
.. code-block:: php
<?php
// src/AppBundle/Controller/CRUDController.php
namespace AppBundle\Controller;
use Sonata\AdminBundle\Controller\CRUDController as BaseController;
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class CRUDController extends BaseController
{
/**
* @param ProxyQueryInterface $selectedModelQuery
* @param Request $request
*
* @return RedirectResponse
*/
public function batchActionMerge(ProxyQueryInterface $selectedModelQuery, Request $request = null)
{
$this->admin->checkAccess('edit');
$this->admin->checkAccess('delete');
$modelManager = $this->admin->getModelManager();
$target = $modelManager->find($this->admin->getClass(), $request->get('targetId'));
if ($target === null){
$this->addFlash('sonata_flash_info', 'flash_batch_merge_no_target');
return new RedirectResponse(
$this->admin->generateUrl('list', array('filter' => $this->admin->getFilterParameters()))
);
}
$selectedModels = $selectedModelQuery->execute();
// do the merge work here
try {
foreach ($selectedModels as $selectedModel) {
$modelManager->delete($selectedModel);
}
$modelManager->update($selectedModel);
} catch (\Exception $e) {
$this->addFlash('sonata_flash_error', 'flash_batch_merge_error');
return new RedirectResponse(
$this->admin->generateUrl('list', array('filter' => $this->admin->getFilterParameters()))
);
}
$this->addFlash('sonata_flash_success', 'flash_batch_merge_success');
return new RedirectResponse(
$this->admin->generateUrl('list', array('filter' => $this->admin->getFilterParameters()))
);
}
// ...
}
(Optional) Overriding the batch selection template
--------------------------------------------------
A merge action requires two kinds of selection: a set of source objects to merge from
and a target object to merge into. By default, batch_actions only let you select one set
of objects to manipulate. We can override this behavior by changing our list template
(``list__batch.html.twig``) and adding a radio button to choose the target object.
.. code-block:: html+jinja
{# src/AppBundle/Resources/views/CRUD/list__batch.html.twig #}
{# see SonataAdminBundle:CRUD:list__batch.html.twig for the current default template #}
{% extends admin.getTemplate('base_list_field') %}
{% block field %}
<input type="checkbox" name="idx[]" value="{{ admin.id(object) }}" />
{# the new radio button #}
<input type="radio" name="targetId" value="{{ admin.id(object) }}" />
{% endblock %}
And add this:
.. code-block:: php
<?php
// src/AppBundle/AppBundle.php
public function getParent()
{
return 'SonataAdminBundle';
}
See the `Symfony bundle overriding mechanism`_
for further explanation of overriding bundle templates.
(Optional) Overriding the default relevancy check function
----------------------------------------------------------
By default, batch actions are not executed if no object was selected, and the user is notified of
this lack of selection. If your custom batch action needs more complex logic to determine if
an action can be performed or not, just define a ``batchAction<MyAction>IsRelevant`` method
(e.g. ``batchActionMergeIsRelevant``) in your ``CRUDController`` class. This check is performed
before the user is asked for confirmation, to make sure there is actually something to confirm.
This method may return three different values:
- ``true``: The batch action is relevant and can be applied.
- ``false``: Same as above, with the default "action aborted, no model selected" notification message.
- ``string``: The batch action is not relevant given the current request parameters
(for example the ``target`` is missing for a ``merge`` action).
The returned string is a message displayed to the user.
.. code-block:: php
<?php
// src/AppBundle/Controller/CRUDController.php
namespace AppBundle\Controller;
use Sonata\AdminBundle\Controller\CRUDController as BaseController;
use Symfony\Component\HttpFoundation\Request;
class CRUDController extends BaseController
{
public function batchActionMergeIsRelevant(array $selectedIds, $allEntitiesSelected, Request $request = null)
{
// here you have access to all POST parameters, if you use some custom ones
// POST parameters are kept even after the confirmation page.
$parameterBag = $request->request;
// check that a target has been chosen
if (!$parameterBag->has('targetId')) {
return 'flash_batch_merge_no_target';
}
$targetId = $parameterBag->get('targetId');
// if all entities are selected, a merge can be done
if ($allEntitiesSelected) {
return true;
}
// filter out the target from the selected models
$selectedIds = array_filter($selectedIds,
function($selectedId) use($targetId){
return $selectedId !== $targetId;
}
);
// if at least one but not the target model is selected, a merge can be done.
return count($selectedIds) > 0;
}
// ...
}
(Optional) Executing a pre batch hook
-------------------------------------
In your admin class you can create a ``preBatchAction`` method to execute something before doing the batch action.
The main purpose of this method is to alter the query or the list of selected ids.
.. code-block:: php
<?php
// in your Admin class
public function preBatchAction($actionName, ProxyQueryInterface $query, array & $idx, $allElements)
{
// altering the query or the idx array
$foo = $query->getParameter('foo')->getValue();
// Doing something with the foo object
// ...
$query->setParameter('foo', $bar);
}
.. _Symfony bundle overriding mechanism: http://symfony.com/doc/current/cookbook/bundles/inheritance.html

View File

@@ -0,0 +1,28 @@
The breadcrumbs builder
=======================
The ``sonata.admin.breadcrumbs_builder`` service is used in the layout of every
page to compute the underlying data for two breadcrumbs:
* one as text, appearing in the ``title`` tag of the document's ``head`` tag;
* the other as html, visible as an horizontal bar at the top of the page.
Getting the breadcrumbs for a given action of a given admin is done like this:
.. code-block:: php
<?php
$this->get('sonata.admin.breadcrumbs_builder')->getBreadcrumbs($admin, $action);
Configuration
-------------
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
breadcrumbs:
# use this to change the default route used to generate the link to the parent object inside a breadcrumb, when in a child admin
child_admin_route: edit

View File

@@ -0,0 +1,129 @@
Create child admins
-------------------
Let us say you have a ``PlaylistAdmin`` and a ``VideoAdmin``. You can optionally declare the ``VideoAdmin``
to be a child of the ``PlaylistAdmin``. This will create new routes like, for example, ``/playlist/{id}/video/list``,
where the videos will automatically be filtered by post.
To do this, you first need to call the ``addChild`` method in your ``PlaylistAdmin`` service configuration:
.. configuration-block::
.. code-block:: xml
<!-- app/config/config.xml -->
<service id="sonata.admin.playlist" class="AppBundle\Admin\PlaylistAdmin">
<!-- ... -->
<call method="addChild">
<argument type="service" id="sonata.admin.video" />
</call>
</service>
Then, you have to set the VideoAdmin ``parentAssociationMapping`` attribute to ``playlist`` :
.. code-block:: php
<?php
namespace AppBundle\Admin;
// ...
class VideoAdmin extends AbstractAdmin
{
protected $parentAssociationMapping = 'playlist';
// OR
public function getParentAssociationMapping()
{
return 'playlist';
}
}
To display the ``VideoAdmin`` extend the menu in your ``PlaylistAdmin`` class:
.. code-block:: php
<?php
namespace AppBundle\Admin;
use Knp\Menu\ItemInterface as MenuItemInterface;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Admin\AdminInterface;
class PlaylistAdmin extends AbstractAdmin
{
// ...
protected function configureSideMenu(MenuItemInterface $menu, $action, AdminInterface $childAdmin = null)
{
if (!$childAdmin && !in_array($action, array('edit', 'show'))) {
return;
}
$admin = $this->isChild() ? $this->getParent() : $this;
$id = $admin->getRequest()->get('id');
$menu->addChild('View Playlist', array('uri' => $admin->generateUrl('show', array('id' => $id))));
if ($this->isGranted('EDIT')) {
$menu->addChild('Edit Playlist', array('uri' => $admin->generateUrl('edit', array('id' => $id))));
}
if ($this->isGranted('LIST')) {
$menu->addChild('Manage Videos', array(
'uri' => $admin->generateUrl('sonata.admin.video.list', array('id' => $id))
));
}
}
}
It also possible to set a dot-separated value, like ``post.author``, if your parent and child admins are not directly related.
Be wary that being a child admin is optional, which means that regular routes
will be created regardless of whether you actually need them or not. To get rid
of them, you may override the ``configureRoutes`` method::
<?php
namespace AppBundle\Admin;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Route\RouteCollection;
class VideoAdmin extends AbstractAdmin
{
protected $parentAssociationMapping = 'playlist';
protected function configureRoutes(RouteCollection $collection)
{
if ($this->isChild()) {
// This is the route configuration as a child
$collection->clearExcept(['show', 'edit']);
return;
}
// This is the route configuration as a parent
$collection->clear();
}
}
You can nest admins as deep as you wish.
Let's say you want to add comments to videos.
You can then add your ``CommentAdmin`` admin service as a child of
the ``VideoAdmin`` admin service.
Finally, the admin interface will look like this:
.. figure:: ../images/child_admin.png
:align: center
:alt: Child admin interface
:width: 700px

View File

@@ -0,0 +1,46 @@
Inline Validation
=================
The inline validation code is now part of the SonataCoreBundle. You can refer to the related documentation for more information.
The above examples show how the integration has been done with the SonataAdminBundle. For completeness, it's worth remembering that
the ``Admin`` class itself contains an empty ``validate`` method. This is automatically called, so you can override it in your own admin class:
.. code-block:: php
// add this to your existing use statements
use Sonata\CoreBundle\Validator\ErrorElement;
class MyAdmin extends AbstractAdmin
{
// add this method
public function validate(ErrorElement $errorElement, $object)
{
$errorElement
->with('name')
->assertLength(array('max' => 32))
->end()
;
}
Troubleshooting
---------------
Make sure your validator method is being called. If in doubt, try throwing an exception:
.. code-block:: php
public function validate(ErrorElement $errorElement, $object)
{
throw new \Exception(__METHOD__);
}
There should not be any validation_groups defined for the form. If you have code like the example below in
your ``Admin`` class, remove the 'validation_groups' entry, the whole $formOptions property or set validation_groups
to an empty array:
.. code-block:: php
protected $formOptions = array(
'validation_groups' => array()
);

View File

@@ -0,0 +1,222 @@
Configuration
=============
.. note::
This page will be removed soon, as it's content is being improved and moved to
other pages of the documentation. Please refer to each section's documentation for up-to-date
information on SonataAdminBundle configuration options.
Configuration
-------------
Configuration options
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
security:
# the default value
handler: sonata.admin.security.handler.role
# use this service if you want ACL
handler: sonata.admin.security.handler.acl
Full Configuration Options
--------------------------
.. configuration-block::
.. code-block:: yaml
# Default configuration for extension with alias: "sonata_admin"
sonata_admin:
security:
handler: sonata.admin.security.handler.noop
information:
# Prototype
id: []
admin_permissions:
# Defaults:
- CREATE
- LIST
- DELETE
- UNDELETE
- EXPORT
- OPERATOR
- MASTER
object_permissions:
# Defaults:
- VIEW
- EDIT
- DELETE
- UNDELETE
- OPERATOR
- MASTER
- OWNER
acl_user_manager: null
title: 'Sonata Admin'
title_logo: bundles/sonataadmin/logo_title.png
options:
html5_validate: true
# Auto order groups and admins by label or id
sort_admins: false
confirm_exit: true
use_select2: true
use_icheck: true
use_bootlint: false
use_stickyforms: true
pager_links: null
form_type: standard
dropdown_number_groups_per_colums: 2
title_mode: ~ # One of "single_text"; "single_image"; "both"
# Enable locking when editing an object, if the corresponding object manager supports it.
lock_protection: false
# Enable automatic registration of annotations with JMSDiExtraBundle
enable_jms_di_extra_autoregistration: true
dashboard:
groups:
# Prototype
id:
label: ~
label_catalogue: ~
icon: '<i class="fa fa-folder"></i>'
provider: ~
items:
admin: ~
label: ~
route: ~
route_params: []
item_adds: []
roles: []
blocks:
type: ~
roles: []
settings:
# Prototype
id: ~
position: right
class: col-md-4
admin_services:
model_manager: null
form_contractor: null
show_builder: null
list_builder: null
datagrid_builder: null
translator: null
configuration_pool: null
route_generator: null
validator: null
security_handler: null
label: null
menu_factory: null
route_builder: null
label_translator_strategy: null
pager_type: null
templates:
form: []
filter: []
view:
# Prototype
id: ~
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'
search: 'SonataAdminBundle:Core:search.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'
preview: 'SonataAdminBundle:CRUD:preview.html.twig'
history: 'SonataAdminBundle:CRUD:history.html.twig'
acl: 'SonataAdminBundle:CRUD:acl.html.twig'
history_revision_timestamp: 'SonataAdminBundle:CRUD:history_revision_timestamp.html.twig'
action: 'SonataAdminBundle:CRUD:action.html.twig'
select: 'SonataAdminBundle:CRUD:list__select.html.twig'
list_block: 'SonataAdminBundle:Block:block_admin_list.html.twig'
search_result_block: 'SonataAdminBundle:Block:block_search_result.html.twig'
short_object_description: 'SonataAdminBundle:Helper:short-object-description.html.twig'
delete: 'SonataAdminBundle:CRUD:delete.html.twig'
batch: 'SonataAdminBundle:CRUD:list__batch.html.twig'
batch_confirmation: 'SonataAdminBundle:CRUD:batch_confirmation.html.twig'
inner_list_row: 'SonataAdminBundle:CRUD:list_inner_row.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'
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'
assets:
stylesheets:
# Defaults:
- 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
- bundles/sonataadmin/css/colors.css
javascripts:
# Defaults:
- 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/Admin.js
- bundles/sonataadmin/treeview.js
extensions:
# Prototype
id:
admins: []
excludes: []
implements: []
extends: []
instanceof: []
uses: []
persist_filters: false
show_mosaic_button: true
global_search:
show_empty_boxes: show

View File

@@ -0,0 +1,120 @@
Console/Command-Line Commands
=============================
SonataAdminBundle provides the following console commands:
* ``cache:create-cache-class``
* ``sonata:admin:generate``
* ``sonata:admin:list``
* ``sonata:admin:explain``
* ``sonata:admin:setup-acl``
* ``sonata:admin:generate-object-acl``
cache:create-cache-class
------------------------
The ``cache:create-cache-class`` command generates the cache class
(``app/cache/...env.../classes.php``) from the classes.map file.
Usage example:
.. code-block:: bash
$ php app/console cache:create-cache-class
sonata:admin:generate
---------------------
The ``sonata:admin:generate`` command generates a new Admin class based on the given model
class, registers it as a service and potentially creates a new controller.
As an argument you need to specify the fully qualified model class.
All passed arguments and options are used as default values in interactive mode.
You can disable the interactive mode with ``--no-interaction`` option.
The command require the SensioGeneratorBundle_ to work. If you don't already have it, you can install it with :
.. code-block:: bash
$ composer require --dev sensio/generator-bundle
=============== ===============================================================================================================================
Options Description
=============== ===============================================================================================================================
**bundle** the bundle name (the default value is determined by the given model class, e.g. "AppBundle" or "YourNSFooBundle")
**admin** the admin class basename (by default this adds "Admin" to the model class name, e.g. "BarAdmin")
**controller** the controller class basename (by default this adds "AdminController" to the model class name, e.g. "BarAdminController")
**manager** the model manager type (by default this is the first registered model manager type, e.g. "orm")
**services** the services YAML file (the default value is "services.yml" or "admin.yml" if it already exist)
**id** the admin service ID (the default value is combination of the bundle name and admin class basename like "your_ns_foo.admin.bar")
=============== ===============================================================================================================================
Usage example:
.. code-block:: bash
$ php app/console sonata:admin:generate AppBundle/Entity/Foo
sonata:admin:list
-----------------
To see which admin services are available use the ``sonata:admin:list`` command.
It prints all the admin service ids available in your application. This command
gets the ids from the ``sonata.admin.pool`` service where all the available admin
services are registered.
Usage example:
.. code-block:: bash
$ php app/console sonata:admin:list
.. figure:: ../images/console_admin_list.png
:align: center
:alt: List command
:width: 700px
List command
sonata:admin:explain
--------------------
The ``sonata:admin:explain`` command prints details about the admin of a model.
As an argument you need to specify the admin service id of the Admin to explain.
Usage example:
.. code-block:: bash
$ php app/console sonata:admin:explain sonata.news.admin.post
.. figure:: ../images/console_admin_explain.png
:align: center
:alt: Explain command
:width: 700px
Explain command
sonata:admin:setup-acl
----------------------
The ``sonata:admin:setup-acl`` command updates ACL definitions for all Admin
classes available in ``sonata.admin.pool``. For instance, every time you create a
new ``Admin`` class, you can create its ACL by using the ``sonata:admin:setup-acl``
command. The ACL database will be automatically updated with the latest masks
and roles.
Usage example:
.. code-block:: bash
$ php app/console sonata:admin:setup-acl
sonata:admin:generate-object-acl
--------------------------------
The ``sonata:admin:generate-object-acl`` is an interactive command which helps
you to generate ACL entities for the objects handled by your Admins. See the help
of the command for more information.
.. _SensioGeneratorBundle: http://symfony.com/doc/current/bundles/SensioGeneratorBundle/index.html

View File

@@ -0,0 +1,419 @@
Dashboard
=========
The Dashboard is the main landing page. By default it lists your mapped models,
as defined by your ``Admin`` services. This is useful to help you start using
``SonataAdminBundle`` right away, but there is much more that you can do to take
advantage of the Dashboard.
The Dashboard is, by default, available at ``/admin/dashboard``, which is handled by
the ``SonataAdminBundle:Core:dashboard`` controller action. The default view file for
this action is ``SonataAdminBundle:Core:dashboard.html.twig``, but you can change
this in your ``config.yml``:
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
templates:
dashboard: SonataAdminBundle:Core:dashboard.html.twig
.. note::
This view, like most of the ``SonataAdminBundle`` views, extends a global
template file, which also contains significant parts to the page. More information
about this is available in the :doc:`templates` chapter.
Blocks
------
The Dashboard is actually built using ``Blocks`` from ``SonataBlockBundle``. You
can learn more about this bundle and how to build your own Blocks on the
`SonataBlock documentation page`_.
The ``Admin`` list block
------------------------
The ``Admin`` list is a ``Block`` that fetches information from the ``Admin`` service's
``Pool`` and prints it in the nicely formatted list you have on your default Dashboard.
The ``Admin`` list is defined by the ``sonata.admin.block.admin_list`` service, which is
implemented by the ``Block\AdminListBlockService`` class. It is then rendered using the
``SonataAdminBundle:Block:block_admin_list.html.twig`` template file.
Feel free to take a look at these files. You'll find the code rather short and easy to
understand, and it will be a great help when implementing your own blocks.
Configuring the ``Admin`` list
------------------------------
As you probably noticed by now, the ``Admin`` list groups ``Admin`` mappings together.
There are several ways in which you can configure these groups.
By default the admins are ordered the way you defined them. With the setting ``sort_admins``
groups and admins will be ordered by their respective label with a fallback to the admin id.
Using the ``Admin`` service declaration
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The first, and most commonly used, method is to set a group when defining your ``Admin``
services:
.. configuration-block::
.. code-block:: xml
<service id="app.admin.post" class="AppBundle\Admin\PostAdmin">
<tag name="sonata.admin" manager_type="orm"
group="Content"
label="Post" />
<argument />
<argument>AppBundle\Entity\Post</argument>
<argument />
</service>
.. code-block:: yaml
services:
app.admin.post:
class: AppBundle\Admin\PostAdmin
tags:
- name: sonata.admin
manager_type: orm
group: "Content"
label: "Post"
arguments:
- ~
- AppBundle\Entity\Post
- ~
public: true
In these examples, notice the ``group`` tag, stating that this particular ``Admin``
service belongs to the ``Content`` group.
.. configuration-block::
.. code-block:: xml
<service id="app.admin.post" class="AppBundle\Admin\PostAdmin">
<tag name="sonata.admin" manager_type="orm"
group="app.admin.group.content"
label="app.admin.model.post" label_catalogue="AppBundle" />
<argument />
<argument>AppBundle\Entity\Post</argument>
<argument />
</service>
.. code-block:: yaml
services:
app.admin.post:
class: AppBundle\Admin\PostAdmin
tags:
- name: sonata.admin
manager_type: orm
group: "app.admin.group.content"
label: "app.admin.model.post"
label_catalogue: "AppBundle"
arguments:
- ~
- AppBundle\Entity\Post
- ~
In this example, the labels are translated by ``AppBundle``, using the given
``label_catalogue``. So, you can use the above examples to support multiple languages
in your project.
.. note::
You can use parameters (e.g. ``%app_admin.group_post%``) for the group names
in either scenario.
Using the ``config.yml``
^^^^^^^^^^^^^^^^^^^^^^^^
You can also configure the ``Admin`` list in your ``config.yml`` file. This
configuration method overrides any settings defined in the Admin service
declarations.
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
dashboard:
groups:
app.admin.group.content:
label: app.admin.group.content
label_catalogue: AppBundle
items:
- app.admin.post
app.admin.group.blog:
items: ~
item_adds:
- sonata.admin.page
roles: [ ROLE_ONE, ROLE_TWO ]
app.admin.group.misc: ~
.. note::
This is an academic, full configuration, example. In real cases, you will usually
not need to use all the displayed options. To use a default value for any setting
either leave out that key or use the ``~`` value for that option.
This configuration specifies that the ``app.admin.group.content`` group uses the
``app.admin.group.content`` label, which is translated using the ``AppBundle``
translation catalogue (the same label and translation configuration that we declared
previously, in the service definition example).
It also states that the ``app.admin.group.content`` group contains just the
``app.admin.post`` ``Admin`` mapping, meaning that any other ``Admin`` services
declared as belonging to this group will not be displayed here.
Secondly, we declare a ``app.admin.group.blog`` group as having all its default items
(i.e. the ones specified in the ``Admin`` service declarations), plus an *additional*
``sonata.admin.page`` mapping, that was not initially part of this group.
We also use the ``roles`` option here, which means that only users with the ``ROLE_ONE``
or ``ROLE_TWO`` privileges will be able to see this group, as opposed to the default setting
which allows everyone to see a given group. Users with ``ROLE_SUPER_ADMIN`` are always
able to see groups that would otherwise be hidden by this configuration option.
The third group, ``app.admin.group.misc``, is set up as a group which uses all its
default values, as declared in the service declarations.
Adding more Blocks
------------------
Like we said before, the Dashboard comes with a default ``Admin`` list block, but
you can create and add more blocks to it.
.. figure:: ../images/dashboard.png
:align: center
:alt: Dashboard
:width: 500
In this screenshot, in addition to the default ``Admin`` list block on the left, we added
a text block and RSS feed block on the right. The configuration for this scenario would be:
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
dashboard:
blocks:
-
position: left
type: sonata.admin.block.admin_list
-
position: right
type: sonata.block.service.text
settings:
content: >
<h2>Welcome to the Sonata Admin</h2>
<p>This is a <code>sonata.block.service.text</code> from the Block
Bundle, you can create and add new block in these area by configuring
the <code>sonata_admin</code> section.</p> <br /> For instance, here
a RSS feed parser (<code>sonata.block.service.rss</code>):
-
position: right
type: sonata.block.service.rss
roles: [POST_READER]
settings:
title: Sonata Project's Feeds
url: https://sonata-project.org/blog/archive.rss
.. note::
Blocks may accept/require additional settings to be passed in order to
work properly. Refer to the associated documentation/implementation to
get more information on each block's options and requirements.
You can also configure the ``roles`` section to configure users that can
view the block.
Display two ``Admin`` list blocks with different dashboard groups
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The same block can have multiple instances, and be displayed multiple times
across the Dashboard using different configuration settings for each instance.
A particular example is the ``Admin`` list block, which can be configured to
suit this scenario.
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
dashboard:
blocks:
# display two dashboard blocks
-
position: left
type: sonata.admin.block.admin_list
settings:
groups: [sonata_page1, sonata_page2]
-
position: right
type: sonata.admin.block.admin_list
settings:
groups: [sonata_page3]
groups:
sonata_page1:
items:
- sonata.page.admin.myitem1
sonata_page2:
items:
- sonata.page.admin.myitem2
- sonata.page.admin.myitem3
sonata_page3:
items:
- sonata.page.admin.myitem4
In this example, you would have two ``admin_list`` blocks on your dashboard, each
of them containing just the respectively configured groups.
.. _`SonataBlock documentation page`: https://sonata-project.org/bundles/block/master/doc/index.html
Statistic Block
~~~~~~~~~~~~~~~
A statistic block can be used to display a simple counter with a color, an font awesome icon and a text. A
counter is related to the filters from one admin
.. configuration-block::
.. code-block:: yaml
sonata_admin:
dashboard:
blocks:
-
class: col-lg-3 col-xs-6 # twitter bootstrap responsive code
position: top # zone in the dashboard
type: sonata.admin.block.stats # block id
settings:
code: sonata.page.admin.page # admin code - service id
icon: fa-magic # font awesome icon
text: Edited Pages
color: bg-yellow # colors: bg-green, bg-red and bg-aqua
filters: # filter values
edited: { value: 1 }
Dashboard Layout
~~~~~~~~~~~~~~~~
Supported positions right now are the following:
* top
* left
* center
* right
* bottom
The layout is as follows:
.. code-block:: bash
TOP TOP TOP
LEFT CENTER RIGHT
LEFT CENTER RIGHT
LEFT CENTER RIGHT
BOTTOM BOTTOM BOTTOM
On ``top`` and ``bottom`` positions, you can also specify an optional ``class`` option to set the width of the block.
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
dashboard:
blocks:
# display dashboard block in the top zone with a col-md-6 css class
-
position: top
class: col-md-6
type: sonata.admin.block.admin_list
Configuring what actions are available for each item on the dashboard
---------------------------------------------------------------------
By default. A "list" and a "create" option are available for each item on the
dashboard. If you created a custom action and want to display it along the
other two on the dashboard, you can do so by overriding the
``getDashboardActions()`` method of your admin class:
.. code-block:: php
<?php
// src/AppBundle/Admin/PostAdmin.php
class PostAdmin extends AbstractAdmin
{
// ...
public function getDashboardActions()
{
$actions = parent::getDashboardActions();
$actions['import'] = array(
'label' => 'Import',
'url' => $this->generateUrl('import'),
'icon' => 'import',
'translation_domain' => 'SonataAdminBundle', // optional
'template' => 'SonataAdminBundle:CRUD:dashboard__action.html.twig', // optional
);
return $actions;
}
}
You can also hide an action from the dashboard by unsetting it:
.. code-block:: php
<?php
// src/AppBundle/Admin/PostAdmin.php
class PostAdmin extends AbstractAdmin
{
// ...
public function getDashboardActions()
{
$actions = parent::getDashboardActions();
unset($actions['list']);
return $actions;
}
}
If you do this, you need to be aware that the action is only hidden. it will
still be available by directly calling its URL, unless you prevent that using
proper security measures (e.g. ACL or role based).

View File

@@ -0,0 +1,49 @@
Events
======
An event mechanism is available to add an extra entry point to extend an Admin instance.
ConfigureEvent
~~~~~~~~~~~~~~
This event is generated when a form, list, show, datagrid is configured. The event names are:
- ``sonata.admin.event.configure.form``
- ``sonata.admin.event.configure.list``
- ``sonata.admin.event.configure.datagrid``
- ``sonata.admin.event.configure.show``
PersistenceEvent
~~~~~~~~~~~~~~~~
This event is generated when a persistency layer update, save or delete an object. The event names are:
- ``sonata.admin.event.persistence.pre_update``
- ``sonata.admin.event.persistence.post_update``
- ``sonata.admin.event.persistence.pre_persist``
- ``sonata.admin.event.persistence.post_persist``
- ``sonata.admin.event.persistence.pre_remove``
- ``sonata.admin.event.persistence.post_remove``
ConfigureQueryEvent
~~~~~~~~~~~~~~~~~~~
This event is generated when a list query is defined. The event name is: ``sonata.admin.event.configure.query``
BlockEvent
~~~~~~~~~~
Block events help you customize your templates. Available events are :
- ``sonata.admin.dashboard.top``
- ``sonata.admin.dashboard.bottom``
- ``sonata.admin.list.table.top``
- ``sonata.admin.list.table.bottom``
- ``sonata.admin.edit.form.top``
- ``sonata.admin.edit.form.bottom``
- ``sonata.admin.show.top``
- ``sonata.admin.show.bottom``
If you want more information about block events, you should check the
`"Event" section of block bundle documentation <https://sonata-project.org/bundles/block/master/doc/reference/events.html>`_.

View File

@@ -0,0 +1,128 @@
Extensions
==========
Admin extensions allow you to add or change features of one or more Admin instances. To create an extension your class
must implement the interface ``Sonata\AdminBundle\Admin\AdminExtensionInterface`` and be registered as a service. The
interface defines a number of functions which you can use to customize the edit form, list view, form validation,
alter newly created objects and other admin features.
.. code-block:: php
use Sonata\AdminBundle\Admin\AbstractAdminExtension;
use Sonata\AdminBundle\Form\FormMapper;
class PublishStatusAdminExtension extends AbstractAdminExtension
{
public function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('status', 'choice', array(
'choices' => array(
'draft' => 'Draft',
'published' => 'Published',
),
))
;
}
}
Configuration
~~~~~~~~~~~~~
There are two ways to configure your extensions and connect them to an admin.
You can include this information in the service definition of your extension.
Add the tag *sonata.admin.extension* and use the *target* attribute to point to
the admin you want to modify. Please note you can specify as many tags you want.
Set the *global* attribute to *true* and the extension will be added to all admins.
The *priority* attribute is *0* by default and can be a positive or negative integer.
The higher the priority, the earlier it's executed.
.. configuration-block::
.. code-block:: yaml
services:
app.publish.extension:
class: AppBundle\Admin\Extension\PublishStatusAdminExtension
tags:
- { name: sonata.admin.extension, target: app.admin.article }
- { name: sonata.admin.extension, target: app.admin.blog }
app.order.extension:
class: AppBundle\Admin\Extension\OrderAdminExtension
tags:
- { name: sonata.admin.extension, global: true }
app.important.extension:
class: AppBundle\Admin\Extension\ImportantAdminExtension
tags:
- { name: sonata.admin.extension, priority: 5 }
The second option is to add it to your config.yml file.
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
extensions:
app.publish.extension:
admins:
- app.admin.article
Using the ``config.yml`` file has some advantages, it allows you to keep your configuration centralized and it provides some
extra options you can use to wire your extensions in a more dynamic way. This means you can change the behaviour of all
admins that manage a class of a specific type.
admins:
specify one or more admin service ids to which the Extension should be added
excludes:
specify one or more admin service ids to which the Extension should not be added (this will prevent it matching
any of the other settings)
extends:
specify one or more classes. If the managed class of an admin extends one of the specified classes the extension
will be added to that admin.
implements:
specify one or more interfaces. If the managed class of an admin implements one of the specified interfaces the
extension will be added to that admin.
instanceof:
specify one or more classes. If the managed class of an admin extends one of the specified classes or is an instance
of that class the extension will be added to that admin.
uses:
Requires PHP >= 5.4.0. Specify one or more traits. If the managed class of an admin uses one of the specified traits the extension will be
added to that admin.
priority:
Can be a positive or negative integer. The higher the priority, the earlier its executed.
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
extensions:
app.publish.extension:
admins:
- app.admin.article
implements:
- AppBundle\Publish\PublishStatusInterface
excludes:
- app.admin.blog
- app.admin.news
extends:
- AppBundle\Document\Blog
instanceof:
- AppBundle\Document\Page
uses:
- AppBundle\Trait\Timestampable

View File

@@ -0,0 +1,254 @@
Field Types
===========
List and Show Actions
---------------------
There are many field types that can be used in the list and show action :
============ =============================================
Fieldtype Description
============ =============================================
array display value from an array
boolean display a green or red picture dependant on the boolean value
date display a formatted date. Accepts an optional ``format`` parameter
datetime display a formatted date and time. Accepts an optional ``format`` and ``timezone`` parameter
text display a text
textarea display a textarea
trans translate the value with a provided ``catalogue`` option
string display a text
number display a number
currency display a number with a provided ``currency`` option
percent display a percentage
choice uses the given value as index for the ``choices`` array and displays (and optionally translates) the matching value
url display a link
html display (and optionally truncate or strip tags from) raw html
============ =============================================
Theses types accept an ``editable`` parameter to edit the value from within the list action.
This is currently limited to scalar types (text, integer, url...) and choice types with association field.
.. note::
If the ``SonataIntlBundle`` is installed in the project some template types
will be changed to use localized information.
Option for currency type must be an official ISO code, example : EUR for "euros".
List of ISO codes : `http://en.wikipedia.org/wiki/List_of_circulating_currencies <http://en.wikipedia.org/wiki/List_of_circulating_currencies>`_
In ``date`` and ``datetime`` field types, ``format`` pattern must match twig's
``date`` filter specification, available at: `http://twig.sensiolabs.org/doc/filters/date.html <http://twig.sensiolabs.org/doc/filters/date.html>`_
In ``datetime`` field types, ``timezone`` syntax must match twig's
``date`` filter specification, available at: `http://twig.sensiolabs.org/doc/filters/date.html <http://twig.sensiolabs.org/doc/filters/date.html>`_
and php timezone list: `https://php.net/manual/en/timezones.php <https://php.net/manual/en/timezones.php>`_
You can use in lists what `view-timezone <http://symfony.com/doc/current/reference/forms/types/datetime.html#view-timezone>`_ allows on forms,
a way to render the date in the user timezone.
.. code-block:: php
public function configureListFields(ListMapper $listMapper)
{
$listMapper
// store date in UTC but display is in the user timezone
->add('date', null, array(
'format' => 'Y-m-d H:i',
'timezone' => 'America/New_York'
))
;
}
More types might be provided based on the persistency layer defined. Please refer to their
related documentations.
Choice
^^^^^^
.. code-block:: php
public function configureListFields(ListMapper $listMapper)
{
// For the value `prog`, the displayed text is `In progress`. The `AppBundle` catalogue will be used to translate `In progress` message.
$listMapper
->add('status', 'choice', array(
'choices' => array(
'prep' => 'Prepared',
'prog' => 'In progress',
'done' => 'Done'
),
'catalogue' => 'AppBundle'
))
;
}
The ``choice`` field type also supports multiple values that can be separated by a ``delimiter``.
.. code-block:: php
public function configureListFields(ListMapper $listMapper)
{
// For the value `array('r', 'b')`, the displayed text ist `red | blue`.
$listMapper
->add('colors', 'choice', array(
'multiple' => true,
'delimiter' => ' | ',
'choices' => array(
'r' => 'red',
'g' => 'green',
'b' => 'blue'
)
))
;
}
.. note::
The default delimiter is a comma ``,``.
URL
^^^
Display URL link to external website or controller action.
You can use the following parameters:
====================================== ==================================================================
Parameter Description
====================================== ==================================================================
**hide_protocol** remove protocol part from the link text
**url** URL address (e.g. ``http://example.com``)
**attributes** array of html tag attributes (e.g. ``array('target' => '_blank')``)
**route.name** route name (e.g. ``acme_blog_homepage``)
**route.parameters** array of route parameters (e.g. ``array('type' => 'example', 'display' => 'full')``)
**route.absolute** boolean value, create absolute or relative url address based on ``route.name`` and ``route.parameters`` (default ``false``)
**route.identifier_parameter_name** parameter added to ``route.parameters``, its value is an object identifier (e.g. 'id') to create dynamic links based on rendered objects.
====================================== ==================================================================
.. code-block:: php
public function configureListFields(ListMapper $listMapper)
{
$listMapper
// Output for value `http://example.com`:
// `<a href="http://example.com">http://example.com</a>`
->add('targetUrl', 'url')
// Output for value `http://example.com`:
// `<a href="http://example.com" target="_blank">example.com</a>`
->add('targetUrl', 'url', array(
'attributes' => array('target' => '_blank')
))
// Output for value `http://example.com`:
// `<a href="http://example.com">example.com</a>`
->add('targetUrl', 'url', array(
'hide_protocol' => true
))
// Output for value `Homepage of example.com` :
// `<a href="http://example.com">Homepage of example.com</a>`
->add('title', 'url', array(
'url' => 'http://example.com'
))
// Output for value `Acme Blog Homepage`:
// `<a href="http://blog.example.com">Acme Blog Homepage</a>`
->add('title', 'url', array(
'route' => array(
'name' => 'acme_blog_homepage',
'absolute' => true
)
))
// Output for value `Sonata is great!` (related object has identifier `123`):
// `<a href="http://blog.example.com/xml/123">Sonata is great!</a>`
->add('title', 'url', array(
'route' => array(
'name' => 'acme_blog_article',
'absolute' => true,
'parameters' => array('format' => 'xml'),
'identifier_parameter_name' => 'id'
)
))
;
}
.. note::
Do not use ``url`` type with ``addIdentifier()`` method, because it will create invalid nested URLs.
HTML
^^^^
Display (and optionally truncate or strip tags from) raw html.
You can use the following parameters:
======================== ==================================================================
Parameter Description
======================== ==================================================================
**strip** Strip HTML and PHP tags from a string
**truncate** Truncate a string to ``length`` characters beginning from start. Implies strip. Beware of HTML entities. Make sure to configure your HTML editor to disable entities if you want to use truncate. For instance, use `config.entities <http://docs.ckeditor.com/#!/api/CKEDITOR.config-cfg-entities>`_ for ckeditor
**truncate.length** The length to truncate the string to (default ``30``)
**truncate.preserve** Preserve whole words (default ``false``)
**truncate.separator** Separator to be appended to the trimmed string (default ``...``)
======================== ==================================================================
.. code-block:: php
public function configureListFields(ListMapper $listMapper)
{
$listMapper
// Output for value `<p><strong>Creating a Template for the Field</strong> and form</p>`:
// `<p><strong>Creating a Template for the Field</strong> and form</p>` (no escaping is done)
->add('content', 'html')
// Output for value `<p><strong>Creating a Template for the Field</strong> and form</p>`:
// `Creating a Template for the Fi...`
->add('content', 'html', array(
'strip' => true
))
// Output for value `<p><strong>Creating a Template for the Field</strong> and form</p>`:
// `Creating a Template for...`
->add('content', 'html', array(
'truncate' => true
))
// Output for value `<p><strong>Creating a Template for the Field</strong> and form</p>`:
// `Creating a...`
->add('content', 'html', array(
'truncate' => array(
'length' => 10
)
))
// Output for value `<p><strong>Creating a Template for the Field</strong> and form</p>`:
// `Creating a Template for the Field...`
->add('content', 'html', array(
'truncate' => array(
'preserve' => true
)
))
// Output for value `<p><strong>Creating a Template for the Field</strong> and form</p>`:
// `Creating a Template for the Fi, etc.`
->add('content', 'html', array(
'truncate' => array(
'separator' => ', etc.'
)
))
// Output for value `<p><strong>Creating a Template for the Field</strong> and form</p>`:
// `Creating a Template for***`
->add('content', 'html', array(
'truncate' => array(
'length' => 20,
'preserve' => true,
'separator' => '***'
)
))
;
}

View File

@@ -0,0 +1,201 @@
Form Help Messages and Descriptions
===================================
Help Messages
-------------
Help messages are short notes that are rendered together with form fields. They are generally used to show additional information so the user can complete the form element faster and more accurately. The text is not escaped, so HTML can be used.
Example
^^^^^^^
.. code-block:: php
<?php
// src/AppBundle/Admin/PostAdmin.php
class PostAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->with('General')
->add('title', null, array(
'help' => 'Set the title of a web page'
))
->add('keywords', null, array(
'help' => 'Set the keywords of a web page'
))
->end()
;
}
}
Alternative Ways To Define Help Messages
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
All at once
.. code-block:: php
<?php
// src/AppBundle/Admin/PostAdmin.php
class PostAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->with('General')
->add('title')
->add('keywords')
->setHelps(array(
'title' => 'Set the title of a web page',
'keywords' => 'Set the keywords of a web page',
))
->end()
;
}
}
or step by step.
.. code-block:: php
<?php
// src/AppBundle/Admin/PostAdmin.php
class PostAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->with('General')
->add('title')
->add('keywords')
->setHelp('title', 'Set the title of a web page')
->setHelp('keywords', 'Set the keywords of a web page')
->end()
;
}
}
This can be very useful if you want to apply general help messages via an ``AdminExtension``.
This Extension for example adds a note field to some entities which use a custom trait.
.. code-block:: php
<?php
namespace AppBundle\Admin\Extension;
use Sonata\AdminBundle\Admin\AbstractAdminExtension;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;
class NoteAdminExtension extends AbstractAdminExtension
{
// add this field to the datagrid every time its available
/**
* @param DatagridMapper $datagridMapper
*/
public function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('note')
;
}
// here we don't add the field, because we would like to define
// the place manually in the admin. But if the filed is available,
// we want to add the following help message to the field.
/**
* @param FormMapper $formMapper
*/
public function configureFormFields(FormMapper $formMapper)
{
$formMapper
->addHelp('note', 'Use this field for an internal note.')
;
}
// if the field exists, add it in a special tab on the show view.
/**
* @param ShowMapper $showMapper
*/
public function configureShowFields(ShowMapper $showMapper)
{
$showMapper
->with('Internal')
->add('note')
->end()
;
}
}
Help messages in a sub-field
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: php
<?php
// src/AppBundle/Admin/PostAdmin.php
class PostAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('enabled')
->add('settings', 'sonata_type_immutable_array', array(
'keys' => array(
array('content', 'textarea', array(
'sonata_help' => 'Set the content'
)),
array('public', 'checkbox', array()),
))
;
}
}
Advanced usage
^^^^^^^^^^^^^^
Since help messages can contain HTML they can be used for more advanced solutions.
See the cookbook entry :doc:`Showing image previews <../cookbook/recipe_image_previews>` for a detailed example of how to
use help messages to display an image tag.
Form Group Descriptions
-----------------------
A form group description is a block of text rendered below the group title. These can be used to describe a section of a form. The text is not escaped, so HTML can be used.
Example
^^^^^^^
.. code-block:: php
<?php
// src/AppBundle/Admin/PostAdmin.php
class PostAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->with('General', array(
'description' => 'This section contains general settings for the web page'
))
->add('title', null, array(
'help' => 'Set the title of a web page'
))
->add('keywords', null, array(
'help' => 'Set the keywords of a web page'
))
->end()
;
}
}

View File

@@ -0,0 +1,751 @@
Form Types
==========
Admin related form types
------------------------
When defining fields in your Admin classes you can use any of the standard
`Symfony field types`_ and configure them as you would normally. In addition
there are some special Sonata field types which allow you to work with
relationships between one entity class and another.
.. _field-types-model:
sonata_type_model
^^^^^^^^^^^^^^^^^
Setting a field type of ``sonata_type_model`` will use an instance of
``ModelType`` to render that field. This Type allows you to choose an existing
entity from the linked model class. In effect it shows a list of options from
which you can choose a value (or values).
For example, we have an entity class called ``Page`` which has a field called
``image1`` which maps a relationship to another entity class called ``Image``.
All we need to do now is add a reference for this field in our ``PageAdmin`` class:
.. code-block:: php
<?php
// src/AppBundle/Admin/PageAdmin.php
class PageAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$imageFieldOptions = array(); // see available options below
$formMapper
->add('image1', 'sonata_type_model', $imageFieldOptions)
;
}
}
Note that the third parameter to ``FormMapper::add()`` is optional so
there is no need to pass in an empty array, it is shown here just to demonstrate
where the options go when you want to use them.
Since the ``image1`` field refers to a related entity we do not need to specify
any options. Sonata will calculate that the linked admin class is of type ``Image`` and,
by default, use the ``ImageAdmin`` class to retrieve a list of all existing Images
to display as choices in the selector.
.. tip::
You need to create ``ImageAdmin`` class in this case to use ``sonata_type_model`` type.
:ref:`You can also use <form_types_fielddescription_options>` use the ``admin_code`` parameter.
The available options are:
property
defaults to null. You can set this to a `Symfony PropertyPath`_ compatible
string to designate which field to use for the choice values.
query
defaults to null. You can set this to a QueryBuilder instance in order to
define a custom query for retrieving the available options.
template
defaults to 'choice' (not currently used?)
multiple
defaults to false - see the `Symfony choice Field Type docs`_ for more info
expanded
defaults to false - see the `Symfony choice Field Type docs`_ for more info
choices
defaults to null - see the `Symfony choice Field Type docs`_ for more info
preferred_choices
defaults to array() - see the `Symfony choice Field Type docs`_ for more info
choice_list
**(deprecated in favor of choice_loader since Symfony 2.7)**
defaults to a ``ModelChoiceList`` built from the other options
choice_loader
defaults to a ``ModelChoiceLoader`` built from the other options
model_manager
defaults to null, but is actually calculated from the linked Admin class.
You usually should not need to set this manually.
class
The entity class managed by this field. Defaults to null, but is actually
calculated from the linked Admin class. You usually should not need to set
this manually.
btn_add, btn_list, btn_delete and btn_catalogue:
The labels on the ``add``, ``list`` and ``delete`` buttons can be customized
with these parameters. Setting any of them to ``false`` will hide the
corresponding button. You can also specify a custom translation catalogue
for these labels, which defaults to ``SonataAdminBundle``.
.. note::
An admin class for the linked model class needs to be defined to render this form type.
.. note::
If you need to use a sortable ``sonata_type_model`` check the :doc:`../cookbook/recipe_sortable_sonata_type_model` page.
.. note::
When using ``sonata_type_model`` with ``btn_add``, a jQuery event will be
triggered when a child form is added to the DOM
(``sonata-admin-setup-list-modal`` by default and
``sonata-admin-append-form-element`` when using ``edit:inline``).
sonata_type_model_hidden
^^^^^^^^^^^^^^^^^^^^^^^^
Setting a field type of ``sonata_type_model_hidden`` will use an instance of
``ModelHiddenType`` to render hidden field. The value of hidden field is
identifier of related entity.
.. code-block:: php
<?php
// src/AppBundle/Admin/PageAdmin.php
class PageAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
// generates hidden form field with id of related Category entity
$formMapper
->add('categoryId', 'sonata_type_model_hidden')
;
}
}
The available options are:
model_manager
defaults to null, but is actually calculated from the linked Admin class.
You usually should not need to set this manually.
class
The entity class managed by this field. Defaults to null, but is actually
calculated from the linked Admin class. You usually should not need to set
this manually.
sonata_type_model_autocomplete
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Setting a field type of ``sonata_type_model_autocomplete`` will use an instance of
``ModelAutocompleteType`` to render that field. This Type allows you to choose an existing
entity from the linked model class. In effect it shows a list of options from
which you can choose a value. The list of options is loaded dynamically
with ajax after typing 3 chars (autocomplete). It is best for entities with many
items.
This field type works by default if the related entity has an admin instance and
in the related entity datagrid is a string filter on the ``property`` field.
For example, we have an entity class called ``Article`` (in the ``ArticleAdmin``)
which has a field called ``category`` which maps a relationship to another entity
class called ``Category``. All we need to do now is add a reference for this field
in our ``ArticleAdmin`` class and make sure, that in the CategoryAdmin exists
datagrid filter for the property ``title``.
.. code-block:: php
<?php
// src/AppBundle/Admin/ArticleAdmin.php
class ArticleAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
// the dropdown autocomplete list will show only Category
// entities that contain specified text in "title" attribute
$formMapper
->add('category', 'sonata_type_model_autocomplete', array(
'property' => 'title'
))
;
}
}
.. code-block:: php
<?php
// src/AppBundle/Admin/CategoryAdmin.php
class CategoryAdmin extends AbstractAdmin
{
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
// this text filter will be used to retrieve autocomplete fields
$datagridMapper
->add('title')
;
}
}
The available options are:
property
defaults to null. You have to set this to designate which field (or a list of fields) to use for the choice values.
This value can be string or array of strings.
class
The entity class managed by this field. Defaults to null, but is actually
calculated from the linked Admin class. You usually should not need to set
this manually.
model_manager
defaults to null, but is actually calculated from the linked Admin class.
You usually should not need to set this manually.
callback
defaults to null. Callable function that can be used to modify the query which is used to retrieve autocomplete items.
The callback should receive three parameters - the Admin instance, the property (or properties) defined as searchable and the
search value entered by the user.
From the ``$admin`` parameter it is possible to get the ``Datagrid`` and the ``Request``:
.. code-block:: php
$formMapper
->add('category', 'sonata_type_model_autocomplete', array(
'property' => 'title',
'callback' => function ($admin, $property, $value) {
$datagrid = $admin->getDatagrid();
$queryBuilder = $datagrid->getQuery();
$queryBuilder
->andWhere($queryBuilder->getRootAlias() . '.foo=:barValue')
->setParameter('barValue', $admin->getRequest()->get('bar'))
;
$datagrid->setValue($property, null, $value);
},
))
;
to_string_callback
defaults to null. Callable function that can be used to change the default toString behaviour of entity.
.. code-block:: php
$formMapper
->add('category', 'sonata_type_model_autocomplete', array(
'property' => 'title',
'to_string_callback' => function($entity, $property) {
return $entity->getTitle();
},
))
;
multiple
defaults to false. Set to true, if your field is in a many-to-many relation.
placeholder
defaults to "". Placeholder is shown when no item is selected.
minimum_input_length
defaults to 3. Minimum number of chars that should be typed to load ajax data.
items_per_page
defaults to 10. Number of items per one ajax request.
quiet_millis
defaults to 100. Number of milliseconds to wait for the user to stop typing before issuing the ajax request.
cache
defaults to false. Set to true, if the requested pages should be cached by the browser.
url
defaults to "". Target external remote URL for ajax requests.
You usually should not need to set this manually.
route
The route ``name`` with ``parameters`` that is used as target URL for ajax
requests.
width
defaults to "". Controls the width style attribute of the Select2 container div.
dropdown_auto_width
defaults to false. Set to true to enable the `dropdownAutoWidth` Select2 option,
which allows the drop downs to be wider than the parent input, sized according to their content.
container_css_class
defaults to "". Css class that will be added to select2's container tag.
dropdown_css_class
defaults to "". CSS class of dropdown list.
dropdown_item_css_class
defaults to "". CSS class of dropdown item.
req_param_name_search
defaults to "q". Ajax request parameter name which contains the searched text.
req_param_name_page_number
defaults to "_page". Ajax request parameter name which contains the page number.
req_param_name_items_per_page
defaults to "_per_page". Ajax request parameter name which contains the limit of
items per page.
template
defaults to ``SonataAdminBundle:Form/Type:sonata_type_model_autocomplete.html.twig``.
Use this option if you want to override the default template of this form type.
.. code-block:: php
<?php
// src/AppBundle/Admin/ArticleAdmin.php
class ArticleAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('category', 'sonata_type_model_autocomplete', array(
'property' => 'title',
'template' => 'AppBundle:Form/Type:sonata_type_model_autocomplete.html.twig',
))
;
}
}
.. code-block:: jinja
{# src/AppBundle/Resources/views/Form/Type/sonata_type_model_autocomplete.html.twig #}
{% extends 'SonataAdminBundle:Form/Type:sonata_type_model_autocomplete.html.twig' %}
{# change the default selection format #}
{% block sonata_type_model_autocomplete_selection_format %}'<b>'+item.label+'</b>'{% endblock %}
target_admin_access_action
defaults to ``list``.
By default, the user needs the ``LIST`` role (mapped to ``list`` access action)
to get the autocomplete items from the target admin's datagrid.
If you can't give some users this role because they will then have access to the target
admin's datagrid, you have to grant them another role.
In the example below we changed the ``target_admin_access_action`` from ``list`` to ``autocomplete``,
which is mapped in the target admin to ``AUTOCOMPLETE`` role. Please make sure that all valid users
have the ``AUTOCOMPLETE`` role.
.. code-block:: php
<?php
// src/AppBundle/Admin/ArticleAdmin.php
class ArticleAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
// the dropdown autocomplete list will show only Category
// entities that contain specified text in "title" attribute
$formMapper
->add('category', 'sonata_type_model_autocomplete', array(
'property' => 'title',
'target_admin_access_action' => 'autocomplete'
))
;
}
}
.. code-block:: php
<?php
// src/AppBundle/Admin/CategoryAdmin.php
class CategoryAdmin extends AbstractAdmin
{
protected $accessMapping = array(
'autocomplete' => 'AUTOCOMPLETE',
);
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
// this text filter will be used to retrieve autocomplete fields
// only the users with role AUTOCOMPLETE will be able to get the items
$datagridMapper
->add('title')
;
}
}
sonata_choice_field_mask
^^^^^^^^^^^^^^^^^^^^^^^^
Setting a field type of ``sonata_choice_field_mask`` will use an instance of
``ChoiceFieldMaskType`` to render choice field.
According the choice made only associated fields are displayed. The others fields are hidden.
.. code-block:: php
<?php
// src/AppBundle/Admin/AppMenuAdmin.php
class AppMenuAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('linkType', 'sonata_type_choice_field_mask', array(
'choices' => array(
'uri' => 'uri',
'route' => 'route',
),
'map' => array(
'route' => array('route', 'parameters'),
'uri' => array('uri'),
),
'placeholder' => 'Choose an option',
'required' => false
))
->add('route', 'text')
->add('uri', 'text')
->add('parameters')
;
}
}
map
Associative array. Describes the fields that are displayed for each choice.
sonata_type_admin
^^^^^^^^^^^^^^^^^
Setting a field type of ``sonata_type_admin`` will embed another Admin class
and use the embedded Admin's configuration when editing this field.
``sonata_type_admin`` fields should only be used when editing a field which
represents a relationship between two model classes.
This Type allows you to embed a complete form for the related element, which
you can configure to allow the creation, editing and (optionally) deletion of
related objects.
For example, lets use a similar example to the one for ``sonata_type_model`` above.
This time, when editing a ``Page`` using ``PageAdmin`` we want to enable the inline
creation (and editing) of new Images instead of just selecting an existing Image
from a list.
First we need to create an ``ImageAdmin`` class and register it as an Admin class
for managing ``Image`` objects. In our admin.yml we have an entry for ``ImageAdmin``
that looks like this:
.. configuration-block::
.. code-block:: yaml
# src/AppBundle/Resources/config/admin.yml
services:
app.admin.image:
class: AppBundle\Admin\ImageAdmin
tags:
- { name: sonata.admin, manager_type: orm, label: "Image" }
arguments:
- ~
- AppBundle\Entity\Image
- 'SonataAdminBundle:CRUD'
calls:
- [ setTranslationDomain, [AppBundle]]
public: true
.. note::
Refer to `Getting started documentation`_ to see how to define your admin.yml file.
To embed ``ImageAdmin`` within ``PageAdmin`` we just need to change the reference
for the ``image1`` field to ``sonata_type_admin`` in our ``PageAdmin`` class:
.. code-block:: php
<?php
// src/AppBundle/Admin/PageAdmin.php
class PageAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('image1', 'sonata_type_admin')
;
}
}
We do not need to define any options since Sonata calculates that the linked class
is of type ``Image`` and the service definition (in admin.yml) defines that ``Image``
objects are managed by the ``ImageAdmin`` class.
The available options (which can be passed as a third parameter to ``FormMapper::add()``) are:
delete
defaults to true and indicates that a 'delete' checkbox should be shown allowing
the user to delete the linked object.
btn_add, btn_list, btn_delete and btn_catalogue:
The labels on the ``add``, ``list`` and ``delete`` buttons can be customized
with these parameters. Setting any of them to ``false`` will hide the
corresponding button. You can also specify a custom translation catalogue
for these labels, which defaults to ``SonataAdminBundle``.
sonata_type_collection
^^^^^^^^^^^^^^^^^^^^^^
The ``CollectionType`` is meant to handle creation and editing of model
collections. Rows can be added and deleted, and your model abstraction layer may
allow you to edit fields inline. You can use ``type_options`` to pass values
to the underlying forms.
.. code-block:: php
<?php
// src/AppBundle/Admin/ProductAdmin.php
class ProductAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('sales', 'sonata_type_collection', array(
'type_options' => array(
// Prevents the "Delete" option from being displayed
'delete' => false,
'delete_options' => array(
// You may otherwise choose to put the field but hide it
'type' => 'hidden',
// In that case, you need to fill in the options as well
'type_options' => array(
'mapped' => false,
'required' => false,
)
)
)
), array(
'edit' => 'inline',
'inline' => 'table',
'sortable' => 'position',
))
// ...
;
}
// ...
}
The available options (which can be passed as a third parameter to ``FormMapper::add()``) are:
btn_add and btn_catalogue:
The label on the ``add`` button can be customized
with this parameters. Setting it to ``false`` will hide the
corresponding button. You can also specify a custom translation catalogue
for this label, which defaults to ``SonataAdminBundle``.
**TIP**: A jQuery event is fired after a row has been added (``sonata-admin-append-form-element``).
You can listen to this event to trigger custom JavaScript (eg: add a calendar widget to a newly added date field)
**TIP**: Setting the 'required' option to true does not cause a requirement of 'at least one' child entity.
Setting the 'required' option to false causes all nested form fields to become not required as well.
.. tip::
You can check / uncheck a range of checkboxes by clicking a first one,
then a second one with shift + click.
sonata_type_native_collection (previously collection)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This bundle handle the native Symfony ``collection`` form type by adding:
* an ``add`` button if you set the ``allow_add`` option to ``true``.
* a ``delete`` button if you set the ``allow_delete`` option to ``true``.
.. tip::
A jQuery event is fired after a row has been added (``sonata-admin-append-form-element``).
You can listen to this event to trigger custom JavaScript (eg: add a calendar widget to a newly added date field)
.. tip::
A jQuery event is fired after a row has been added (``sonata-collection-item-added``)
or before deleted (``sonata-collection-item-deleted``).
A jQuery event is fired after a row has been deleted successfully (``sonata-collection-item-deleted-successful``)
You can listen to these events to trigger custom JavaScript.
.. _form_types_fielddescription_options:
FieldDescription options
^^^^^^^^^^^^^^^^^^^^^^^^
The fourth parameter to FormMapper::add() allows you to pass in ``FieldDescription``
options as an array. The most useful of these is ``admin_code``, which allows you to
specify which Admin to use for managing this relationship. It is most useful for inline
editing in conjunction with the ``sonata_type_admin`` form type.
The value used should be the admin *service* name, not the class name. If you do
not specify an ``admin_code`` in this way, the default admin class for the field's
model type will be used.
For example, to specify the use of the Admin class which is registered as
``sonata.admin.imageSpecial`` for managing the ``image1`` field from our ``PageAdmin``
example above:
.. code-block:: php
<?php
// src/AppBundle/Admin/PageAdmin.php
class PageAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('image1', 'sonata_type_admin', array(), array(
'admin_code' => 'sonata.admin.imageSpecial'
))
// ...
;
}
// ...
}
Other specific field configuration options are detailed in the related
abstraction layer documentation.
Adding a FormBuilderInterface
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
You can add Symfony ``FormBuilderInterface`` instances to the ``FormMapper``. This allows you to
re-use a model form type. When adding a field using a ``FormBuilderInterface``, the type is guessed.
Given you have a ``PostType`` like this:
.. code-block:: php
<?php
// src/AppBundle/Form/PostType.php
class PostType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('author', EntityType::class, [
'class' => User::class
])
->add('title', TextType::class)
->add('body', TextareaType::class)
;
}
}
you can reuse it like this:
.. code-block:: php
<?php
// src/AppBundle/Admin/Post.php
class Post extend AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$builder = $formMapper->getFormBuilder()->getFormFactory()->createBuilder(PostType::class);
$formMapper
->with('Post')
->add($builder->get('title'))
->add($builder->get('body'))
->end()
->with('Author')
->add($builder->get('author'))
->end()
;
}
}
Types options
-------------
General
^^^^^^^
- ``label``: You can set the ``label`` option to ``false`` if you don't want to show it.
.. code-block:: php
<?php
// src/AppBundle/Admin/PageAdmin.php
class PageAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('status', null, array(
'label' => false
))
// ...
;
}
// ...
}
ChoiceType
^^^^^^^^^^
- ``sortable``: This option can be added for multiple choice widget to activate select2 sortable.
.. code-block:: php
<?php
// src/AppBundle/Admin/PageAdmin.php
class PageAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('multiChoices', 'choice', array(
'multiple' => true,
'sortable' => true,
))
// ...
;
}
// ...
}
.. _`Symfony field types`: http://symfony.com/doc/current/book/forms.html#built-in-field-types
.. _`Symfony choice Field Type docs`: http://symfony.com/doc/current/reference/forms/types/choice.html
.. _`Symfony PropertyPath`: http://api.symfony.com/2.0/Symfony/Component/Form/Util/PropertyPath.html
.. _`Getting started documentation`: https://sonata-project.org/bundles/admin/master/doc/reference/getting_started.html#importing-it-in-the-main-config-yml

View File

@@ -0,0 +1,279 @@
Getting started with SonataAdminBundle
======================================
If you followed the installation instructions, SonataAdminBundle should be installed
but inaccessible. You first need to configure it for your models before you can
start using it. Here is a quick checklist of what is needed to quickly setup
SonataAdminBundle and create your first admin interface for the models of your application:
* Step 1: Create an Admin class
* Step 2: Create an Admin service
* Step 3: Configuration
Create an Admin class
---------------------
SonataAdminBundle helps you manage your data using a graphic interface that
will let you create, update or search your model's instances. Those actions need to
be configured, which is done using an Admin class.
An Admin class represents the mapping of your model to each administration action.
In it, you decide which fields to show on a listing, which to use as filters or what
to show in a creation or edition form.
The easiest way to create an Admin class for your model is to extend
the ``Sonata\AdminBundle\Admin\AbstractAdmin`` class.
Suppose your ``AppBundle`` has a ``Post`` entity.
This is how a basic Admin class for it could look like:
.. code-block:: php
<?php
// src/AppBundle/Admin/PostAdmin.php
namespace AppBundle\Admin;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Show\ShowMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
class PostAdmin extends AbstractAdmin
{
// Fields to be shown on create/edit forms
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('title', 'text', array(
'label' => 'Post Title'
))
->add('author', 'entity', array(
'class' => 'AppBundle\Entity\User'
))
// if no type is specified, SonataAdminBundle tries to guess it
->add('body')
// ...
;
}
// Fields to be shown on filter forms
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('title')
->add('author')
;
}
// Fields to be shown on lists
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('title')
->add('slug')
->add('author')
;
}
// Fields to be shown on show action
protected function configureShowFields(ShowMapper $showMapper)
{
$showMapper
->add('title')
->add('slug')
->add('author')
;
}
}
Implementing these four functions is the first step to creating an Admin class.
Other options are available, that will let you further customize the way your model
is shown and handled. Those will be covered in more advanced chapters of this manual.
Create an Admin service
-----------------------
Now that you have created your Admin class, you need to create a service for it. This
service needs to have the ``sonata.admin`` tag, which is your way of letting
SonataAdminBundle know that this particular service represents an Admin class:
Create either a new ``admin.xml`` or ``admin.yml`` file inside the ``src/AppBundle/Resources/config/`` folder:
.. configuration-block::
.. code-block:: xml
<!-- src/AppBundle/Resources/config/admin.xml -->
<service id="app.admin.post" class="AppBundle\Admin\PostAdmin">
<tag name="sonata.admin" manager_type="orm" group="Content" label="Post" />
<argument />
<argument>AppBundle\Entity\Post</argument>
<argument />
<call method="setTranslationDomain">
<argument>AppBundle</argument>
</call>
</service>
.. code-block:: yaml
# src/AppBundle/Resources/config/admin.yml
services:
app.admin.post:
class: AppBundle\Admin\PostAdmin
tags:
- { name: sonata.admin, manager_type: orm, group: "Content", label: "Post" }
arguments:
- ~
- AppBundle\Entity\Post
- ~
calls:
- [ setTranslationDomain, [AppBundle]]
public: true
The example above assumes that you're using ``SonataDoctrineORMAdminBundle``.
If you're using ``SonataDoctrineMongoDBAdminBundle``, ``SonataPropelAdminBundle`` or ``SonataDoctrinePhpcrAdminBundle`` instead, set ``manager_type`` option to ``doctrine_mongodb``, ``propel`` or ``doctrine_phpcr`` respectively.
The basic configuration of an Admin service is quite simple. It creates a service
instance based on the class you specified before, and accepts three arguments:
1. The Admin service's code (defaults to the service's name)
2. The model which this Admin class maps (required)
3. The controller that will handle the administration actions (defaults to ``SonataAdminBundle:CRUDController()``)
Usually you just need to specify the second argument, as the first and third's default
values will work for most scenarios.
The ``setTranslationDomain`` call lets you choose which translation domain to use when
translating labels on the admin pages. If you don't call ``setTranslationDomain``, SonataAdmin uses ``messages`` as translation domain.
More info on the `Symfony translations page`_.
Now that you have a configuration file with your admin service, you just need to tell
Symfony to load it. There are two ways to do so:
Have your bundle load it
^^^^^^^^^^^^^^^^^^^^^^^^
Inside your bundle's extension file, using the ``load()`` method as described in the `Symfony cookbook`_.
For ``admin.xml`` use:
.. code-block:: php
<?php
// src/AppBundle/DependencyInjection/AppExtension.php
namespace AppBundle\DependencyInjection;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\Config\FileLocator;
class AppExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container) {
// ...
$loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
// ...
$loader->load('admin.xml');
}
}
and for ``admin.yml``:
.. code-block:: php
<?php
// src/AppBundle/DependencyInjection/AppExtension.php
namespace AppBundle\DependencyInjection;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\Config\FileLocator;
class AppExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
// ...
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
// ...
$loader->load('admin.yml');
}
}
Importing it in the main config.yml
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
We recommend the to load the file in the Extension, but this way is possible, too.
You can include your new configuration file in the main ``config.yml`` (make sure that you
use the correct file extension):
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
imports:
# for xml
- { resource: "@AppBundle/Resources/config/admin.xml" }
# for yaml
- { resource: "@AppBundle/Resources/config/admin.yml" }
Configuration
-------------
At this point you have basic administration actions for your model. If you visit ``http://yoursite.local/admin/dashboard`` again, you should now see a panel with
your mapped model. You can start creating, listing, editing and deleting instances.
You probably want to put your own project's name and logo on the top bar.
Put your logo file here ``src/AppBundle/Resources/public/images/fancy_acme_logo.png``
Install your assets:
.. code-block:: bash
$ php app/console assets:install
Now you can change your project's main config.yml file:
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
title: Acme
title_logo: bundles/app/images/fancy_acme_logo.png
Next steps - Security
---------------------
As you probably noticed, you were able to access your dashboard and data by just
typing in the URL. By default, the SonataAdminBundle does not come with any user
management for ultimate flexibility. However, it is most likely that your application
requires such a feature. The Sonata Project includes a ``SonataUserBundle`` which
integrates the very popular ``FOSUserBundle``. Please refer to the :doc:`security` section of
this documentation for more information.
Congratulations! You are ready to start using SonataAdminBundle. You can now map
additional models or explore advanced functionalities. The following sections will
each address a specific section or functionality of the bundle, giving deeper
details on what can be configured and achieved with SonataAdminBundle.
.. _`Symfony cookbook`: http://symfony.com/doc/master/cookbook/bundles/extension.html#using-the-load-method
.. _`Symfony translations page`: http://symfony.com/doc/current/book/translation.html#using-message-domains

View File

@@ -0,0 +1,171 @@
Installation
============
SonataAdminBundle can be installed at any moment during a project's lifecycle,
whether it's a clean Symfony installation or an existing project.
Downloading the code
--------------------
Use composer to manage your dependencies and download SonataAdminBundle:
.. code-block:: bash
$ php composer.phar require sonata-project/admin-bundle
You'll be asked to type in a version constraint. 'dev-master' will get you the latest
version, compatible with the latest Symfony version. Check `packagist <https://packagist.org/packages/sonata-project/admin-bundle>`_
for older versions:
.. code-block:: bash
Please provide a version constraint for the sonata-project/admin-bundle requirement: dev-master
Selecting and downloading a storage bundle
------------------------------------------
SonataAdminBundle is storage agnostic, meaning it can work with several storage
mechanisms. Depending on which you are using on your project, you'll need to install
one of the following bundles. In the respective links you'll find simple installation
instructions for each of them:
- `SonataDoctrineORMAdminBundle <https://sonata-project.org/bundles/doctrine-orm-admin/master/doc/reference/installation.html>`_
- `SonataDoctrineMongoDBAdminBundle <https://github.com/sonata-project/SonataDoctrineMongoDBAdminBundle/blob/master/Resources/doc/reference/installation.rst>`_
- `SonataPropelAdminBundle <https://sonata-project.org/bundles/propel-admin/master/doc/reference/installation.html>`_
- `SonataDoctrinePhpcrAdminBundle <https://github.com/sonata-project/SonataDoctrinePhpcrAdminBundle/blob/master/Resources/doc/reference/installation.rst>`_
.. note::
Don't know which to choose? Most new users prefer SonataDoctrineORMAdmin, to interact with traditional relational databases (MySQL, PostgreSQL, etc)
Enabling SonataAdminBundle and its dependencies
-----------------------------------------------
SonataAdminBundle relies on other bundles to implement some features.
Besides the storage layer mentioned on step 2, there are other bundles needed
for SonataAdminBundle to work:
- `SonataBlockBundle <https://sonata-project.org/bundles/block/master/doc/reference/installation.html>`_
- `KnpMenuBundle <https://github.com/KnpLabs/KnpMenuBundle/blob/master/Resources/doc/index.md#installation>`_ (Version 2.*)
These bundles are automatically downloaded by composer as a dependency of SonataAdminBundle.
However, you have to enable them in your ``AppKernel.php``, and configure them manually. Don't
forget to enable SonataAdminBundle too:
.. code-block:: php
<?php
// app/AppKernel.php
public function registerBundles()
{
return array(
// ...
// The admin requires some twig functions defined in the security
// bundle, like is_granted
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
// Add your dependencies
new Sonata\CoreBundle\SonataCoreBundle(),
new Sonata\BlockBundle\SonataBlockBundle(),
new Knp\Bundle\MenuBundle\KnpMenuBundle(),
//...
// If you haven't already, add the storage bundle
// This example uses SonataDoctrineORMAdmin but
// it works the same with the alternatives
new Sonata\DoctrineORMAdminBundle\SonataDoctrineORMAdminBundle(),
// Then add SonataAdminBundle
new Sonata\AdminBundle\SonataAdminBundle(),
// ...
);
}
.. note::
Since version 2.3 SonatajQueryBundle is not required anymore as assets are available in this
bundle. The bundle is also registered in `bower.io <https://github.com/sonata-project/SonataAdminBundle>`_ so
you can use bower to handle your assets. To make sure you get the dependencies
that match the version of SonataAdminBundle you are using, you can make bower
use the local bower dependency file, like this : ``bower install ./vendor/sonata-project/admin-bundle/bower.json``
.. note::
You must enable translator service in `config.yml`.
.. code-block:: yaml
framework:
translator: { fallbacks: ["%locale%"] }
For more information: http://symfony.com/doc/current/translation.html#configuration
Configuring SonataAdminBundle dependencies
------------------------------------------
You will need to configure SonataAdminBundle's dependencies. For each of the above
mentioned bundles, check their respective installation/configuration instructions
files to see what changes you have to make to your Symfony configuration.
SonataAdminBundle provides a SonataBlockBundle block that's used on the administration
dashboard. To be able to use it, make sure it's enabled on SonataBlockBundle's configuration:
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_block:
default_contexts: [cms]
blocks:
# enable the SonataAdminBundle block
sonata.admin.block.admin_list:
contexts: [admin]
.. note::
Don't worry too much if, at this point, you don't yet understand fully
what a block is. SonataBlockBundle is a useful tool, but it's not vital
that you understand it right now.
Cleaning up
-----------
Now, install the assets from the bundles:
.. code-block:: bash
$ php bin/console assets:install
Usually, when installing new bundles, it is a good practice to also delete your cache:
.. code-block:: bash
$ php bin/console cache:clear
At this point, your Symfony installation should be fully functional, with no errors
showing up from SonataAdminBundle or its dependencies. SonataAdminBundle is installed
but not yet configured (more on that in the next section), so you won't be able to
use it yet.
If, at this point or during the installation, you come across any errors, don't panic:
- Read the error message carefully. Try to find out exactly which bundle is causing the error. Is it SonataAdminBundle or one of the dependencies?
- Make sure you followed all the instructions correctly, for both SonataAdminBundle and its dependencies.
- Odds are that someone already had the same problem, and it's documented somewhere. Check Google_, `Sonata Users Group`_ or `Symfony Support`_ to see if you can find a solution.
- Still no luck? Try checking the project's `open issues on GitHub`_.
After you have successfully installed the above bundles you need to configure
SonataAdminBundle for administering your models. All that is needed to quickly
set up SonataAdminBundle is described in the :doc:`getting_started` chapter.
.. _Google: http://www.google.com
.. _`Sonata Users Group`: https://groups.google.com/group/sonata-users
.. _`Symfony Support`: http://symfony.com/support
.. _`open issues on GitHub`: https://github.com/sonata-project/SonataAdminBundle/issues

View File

@@ -0,0 +1,138 @@
Preview Mode
============
Preview Mode is an optional view of an object before it is persisted or updated.
The preview step can be enabled for an admin entity by overriding the public property
``$supportsPreviewMode`` and setting it to true.
.. code-block:: php
<?php
// src/AppBundle/AdminPostAdmin.php
class PostAdmin extends AbstractAdmin
{
public $supportsPreviewMode = true;
/ ..
}
This will show a new button during create/edit mode named preview.
.. figure:: ../images/preview_mode_button.png
:align: center
:alt: Preview Button
While in preview mode two buttons will be shown to approve or decline persistence of the
entity. Decline will send you back to the edit mode with all your changes unpersisted but
still in the form so no data is lost and the entity can be further adjusted.
Accepting the preview will store the entity as if the preview step was never there.
.. figure:: ../images/preview_show.png
:align: center
:alt: Preview Button
Simulating front-end rendering
------------------------------
Preview can be used to render how the object would look like in your front-end environment.
However by default it uses a template similar to the one of the show action and works with
the fields configured to be shown in the show view.
Overriding the preview template ``SonataAdminBundle:CRUD:preview.html.twig`` can be done either
globally through the template configuration for the key 'preview':
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
templates:
preview: AppBundle:CRUD:preview.html.twig
Or per admin entity by overriding the ``getTemplate($name)`` and returning the appropriate template when the key
matches 'preview':
.. code-block:: php
<?php
// src/AppBundle/Admin/PostAdmin.php
// ...
public function getTemplate($name)
{
switch ($name) {
case 'preview':
return 'AppBundle:CRUD:preview.html.twig';
break;
default:
return parent::getTemplate($name);
break;
}
}
In either way the template should be extending your own layout, injecting the form in it
and finally overriding the action buttons to show the approve/decline buttons like the
default preview.html.twig.
The entity is passed to the view in a variable called **object**. If your original view expects
a different object you can just set your own variables prior to calling parent().
.. code-block:: jinja
{# 'AppBundle:CRUD:preview.html.twig #}
{% extends 'AppBundle::layout.html.twig' %}
{% use 'SonataAdminBundle:CRUD:base_edit_form.html.twig' with form as parentForm %}
{% import 'SonataAdminBundle:CRUD:base_edit_form_macro.html.twig' as form_helper %}
{# a block in 'AppBundle::layout.html.twig' expecting article #}
{% block templateContent %}
{% set article = object %}
{{ parent() }}
<div class="sonata-preview-form-container">
{{ block('parentForm') }}
</div>
{% endblock %}
{% block formactions %}
<button class="btn btn-success" type="submit" name="btn_preview_approve">
<i class="fa fa-check"></i>
{{ 'btn_preview_approve'|trans({}, 'SonataAdminBundle') }}
</button>
<button class="btn btn-danger" type="submit" name="btn_preview_decline">
<i class="fa fa-times"></i>
{{ 'btn_preview_decline'|trans({}, 'SonataAdminBundle') }}
</button>
{% endblock %}
Keep in mind that the whole edit form will now appear in your view.
Hiding the fieldset tags with css ``display:none`` will be enough to only show the buttons
(which still have to be styled according to your wishes) and create a nice preview-workflow:
.. code-block:: css
.sonata-preview-form-container .row {
display: none;
};
Or if you prefer less:
.. code-block:: scss
div.sonata-preview-form-container {
.row {
display: none;
};
}

View File

@@ -0,0 +1,407 @@
Routing
=======
The default routes used in the CRUD controller are accessible through the
``Admin`` class.
The ``Admin`` class contains two routing methods:
* ``getRoutes()``: Returns the available routes;
* ``generateUrl($name, $options)``: Generates the related routes.
Routing Definition
------------------
Route names
^^^^^^^^^^^
You can set a ``baseRouteName`` property inside your ``Admin`` class. This
represents the route prefix, to which an underscore and the action name will
be added to generate the actual route names.
.. note::
This is the internal *name* given to a route (it has nothing to do with the route's visible *URL*).
.. code-block:: php
<?php
// src/AppBundle/Admin/PostAdmin.php
class PostAdmin extends AbstractAdmin
{
protected $baseRouteName = 'sonata_post';
// will result in routes named:
// sonata_post_list
// sonata_post_create
// etc..
// ...
}
If no ``baseRouteName`` is defined then the Admin will generate one for you,
based on the following format: 'admin_vendor_bundlename_entityname' so you will have
route names for your actions like 'admin_vendor_bundlename_entityname_list'.
If the Admin fails to find a baseRouteName for your Admin class a ``RuntimeException``
will be thrown with a related message.
If the admin class is a child of another admin class the route name will be prefixed by the parent route name, example :
.. code-block:: php
<?php
// The parent admin class
class PostAdmin extends AbstractAdmin
{
protected $baseRouteName = 'sonata_post';
// ...
}
// The child admin class
class CommentAdmin extends AbstractAdmin
{
protected $baseRouteName = 'comment'
// will result in routes named :
// sonata_post_comment_list
// sonata_post_comment_create
// etc..
// ...
}
Route patterns (URLs)
^^^^^^^^^^^^^^^^^^^^^
You can use ``baseRoutePattern`` to set a custom URL for a given ``Admin`` class.
For example, to use ``http://yourdomain.com/admin/foo`` as the base URL for
the ``FooAdmin`` class (instead of the default of ``http://yourdomain.com/admin/vendor/bundle/foo``)
use the following code:
.. code-block:: php
<?php
// src/AppBundle/Admin/FooAdmin.php
class FooAdmin extends AbstractAdmin
{
protected $baseRoutePattern = 'foo';
}
You will then have route URLs like ``http://yourdomain.com/admin/foo/list`` and
``http://yourdomain.com/admin/foo/1/edit``
If the admin class is a child of another admin class the route pattern will be prefixed by the parent route pattern, example :
.. code-block:: php
<?php
// The parent admin class
class PostAdmin extends AbstractAdmin
{
protected $baseRoutePattern = 'post';
// ...
}
// The child admin class
class CommentAdmin extends AbstractAdmin
{
protected $baseRoutePattern = 'comment'
// ...
}
For comment you will then have route URLs like ``http://yourdomain.com/admin/post/{postId}/comment/list`` and
``http://yourdomain.com/admin/post/{postId}/comment/{commentId}/edit``
Routing usage
-------------
Inside a CRUD template, a route for the current ``Admin`` class can be generated via
the admin variable's ``generateUrl()`` command:
.. code-block:: html+jinja
<a href="{{ admin.generateUrl('list') }}">List</a>
<a href="{{ admin.generateUrl('list', params|merge('page': 1)) }}">List</a>
Note that you do not need to provide the Admin's route prefix (``baseRouteName``) to
generate a URL for the current Admin, just the action name.
To generate a URL for a different Admin you just use the Route Name with the usual
Twig helpers:
.. code-block:: html+jinja
<a href="{{ path('admin_app_post_list') }}">Post List</a>
Create a route
--------------
You can register new routes by defining them in your ``Admin`` class. Only Admin
routes should be registered this way.
The routes you define in this way are generated within your Admin's context, and
the only required parameter to ``add()`` is the action name. The second parameter
can be used to define the URL format to append to ``baseRoutePattern``, if not set
explicitly this defaults to the action name.
.. code-block:: php
<?php
// src/AppBundle/Admin/MediaAdmin.php
use Sonata\AdminBundle\Route\RouteCollection;
class MediaAdmin extends AbstractAdmin
{
protected function configureRoutes(RouteCollection $collection)
{
$collection->add('myCustom'); // Action gets added automatically
$collection->add('view', $this->getRouterIdParameter().'/view');
}
}
Make use of all route parameters
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
As the ``add`` method create a Symfony ``Route``, you can use all constructor arguments of the ``Route`` as parameters
in the ``add`` method to set additional settings like this:
.. code-block:: php
<?php
// src/AppBundle/Admin/MediaAdmin.php
use Sonata\AdminBundle\Route\RouteCollection;
class MediaAdmin extends AbstractAdmin
{
protected function configureRoutes(RouteCollection $collection)
{
$collection->add('custom_action', $this->getRouterIdParameter().'/custom-action', array(), array(), array(), '', array('https'), array('GET', 'POST'));
}
}
Other steps needed to create your new action
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In addition to defining the route for your new action you also need to create a
handler for it in your Controller. By default Admin classes use ``SonataAdminBundle:CRUD``
as their controller, but this can be changed by altering the third argument when defining
your Admin service (in your admin.yml file).
For example, lets change the Controller for our MediaAdmin class to AppBundle:MediaCRUD:
.. configuration-block::
.. code-block:: yaml
# src/AppBundle/Resources/config/admin.yml
app.admin.media:
class: AppBundle\Admin\MediaAdmin
tags:
- { name: sonata.admin, manager_type: orm, label: "Media" }
arguments:
- ~
- AppBundle\Entity\Page
- 'AppBundle:MediaCRUD' # define the new controller via the third argument
public: true
We now need to create our Controller, the easiest way is to extend the basic Sonata CRUD controller:
.. code-block:: php
<?php
// src/AppBundle/Controller/MediaCRUDController.php
namespace AppBundle\Controller;
use Sonata\AdminBundle\Controller\CRUDController;
class MediaCRUDController extends CRUDController
{
public function myCustomAction()
{
// your code here ...
}
}
Removing a route
----------------
Extending ``Sonata\AdminBundle\Admin\AbstractAdmin`` will give your Admin classes the following
default routes:
* batch
* create
* delete
* export
* edit
* list
* show
You can view all of the current routes defined for an Admin class by using the console to run
.. code-block:: bash
$ php app/console sonata:admin:explain <<admin.service.name>>
for example if your Admin is called sonata.admin.foo you would run
.. code-block:: bash
$ php app/console sonata:admin:explain app.admin.foo
Sonata internally checks for the existence of a route before linking to it. As a result, removing a
route will prevent links to that action from appearing in the administrative interface. For example,
removing the 'create' route will prevent any links to "Add new" from appearing.
Removing a single route
^^^^^^^^^^^^^^^^^^^^^^^
Any single registered route can be easily removed by name:
.. code-block:: php
<?php
// src/AppBundle/Admin/MediaAdmin.php
use Sonata\AdminBundle\Route\RouteCollection;
class MediaAdmin extends AbstractAdmin
{
protected function configureRoutes(RouteCollection $collection)
{
$collection->remove('delete');
}
}
Removing all routes except named ones
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If you want to disable all default Sonata routes except few whitelisted ones, you can use
the ``clearExcept()`` method. This method accepts an array of routes you want to keep active.
.. code-block:: php
<?php
// src/AppBundle/Admin/MediaAdmin.php
use Sonata\AdminBundle\Route\RouteCollection;
class MediaAdmin extends AbstractAdmin
{
protected function configureRoutes(RouteCollection $collection)
{
// Only `list` and `edit` route will be active
$collection->clearExcept(array('list', 'edit'));
// You can also pass a single string argument
$collection->clearExcept('list');
}
}
Removing all routes
^^^^^^^^^^^^^^^^^^^
If you want to remove all default routes, you can use ``clear()`` method.
.. code-block:: php
<?php
// src/AppBundle/Admin/MediaAdmin.php
use Sonata\AdminBundle\Route\RouteCollection;
class MediaAdmin extends AbstractAdmin
{
protected function configureRoutes(RouteCollection $collection)
{
// All routes are removed
$collection->clear();
}
}
Removing routes only when an Admin is embedded
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To prevent some routes from being available when one Admin is embedded inside another one
(e.g. to remove the "add new" option when you embed ``TagAdmin`` within ``PostAdmin``) you
can use ``hasParentFieldDescription()`` to detect this case and remove the routes.
.. code-block:: php
<?php
// src/AppBundle/Admin/TagAdmin.php
use Sonata\AdminBundle\Route\RouteCollection;
class TagAdmin extends AbstractAdmin
{
protected function configureRoutes(RouteCollection $collection)
{
// prevent display of "Add new" when embedding this form
if ($this->hasParentFieldDescription()) {
$collection->remove('create');
}
}
}
Persistent parameters
---------------------
In some cases, the interface might be required to pass the same parameters
across the different ``Admin``'s actions. Instead of setting them in the
template or doing other weird hacks, you can define a ``getPersistentParameters``
method. This method will be used when a link is being generated.
.. code-block:: php
<?php
// src/AppBundle/Admin/MediaAdmin.php
class MediaAdmin extends AbstractAdmin
{
public function getPersistentParameters()
{
if (!$this->getRequest()) {
return array();
}
return array(
'provider' => $this->getRequest()->get('provider'),
'context' => $this->getRequest()->get('context', 'default'),
);
}
}
If you then call ``$admin->generateUrl('create')`` somewhere, the generated URL looks like this: ``/admin/module/create?context=default``
Changing the default route in a List Action
-------------------------------------------
Usually the identifier column of a list action links to the edit screen. To change the
list action's links to point to a different action, set the ``route`` option in your call to
``ListMapper::addIdentifier()``. For example, to link to show instead of edit:
.. code-block:: php
<?php
// src/AppBundle/Admin/PostAdmin.php
class PostAdmin extends AbstractAdmin
{
public function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('name', null, array(
'route' => array(
'name' => 'show'
)
));
}
}

View File

@@ -0,0 +1,137 @@
Saving hooks
============
When a SonataAdmin is submitted for processing, there are some events called. One
is before any persistence layer interaction and the other is afterward. Also between submitting
and validating for edit and create actions ``preValidate`` event called. The
events are named as follows:
- new object : ``preValidate($object)`` / ``prePersist($object)`` / ``postPersist($object)``
- edited object : ``preValidate($object)`` / ``preUpdate($object)`` / ``postUpdate($object)``
- deleted object : ``preRemove($object)`` / ``postRemove($object)``
It is worth noting that the update events are called whenever the Admin is successfully
submitted, regardless of whether there are any actual persistence layer events. This
differs from the use of ``preUpdate`` and ``postUpdate`` events in DoctrineORM and perhaps some
other persistence layers.
For example: if you submit an edit form without changing any of the values on the form
then there is nothing to change in the database and DoctrineORM would not fire the **Entity**
class's own ``preUpdate`` and ``postUpdate`` events. However, your **Admin** class's
``preUpdate`` and ``postUpdate`` methods *are* called and this can be used to your
advantage.
.. note::
When embedding one Admin within another, for example using the ``sonata_type_admin``
field type, the child Admin's hooks are **not** fired.
Example used with the FOS/UserBundle
------------------------------------
The ``FOSUserBundle`` provides authentication features for your Symfony Project,
and is compatible with Doctrine ORM, Doctrine ODM and Propel. See
`FOSUserBundle on GitHub`_ for more information.
The user management system requires to perform specific calls when the user
password or username are updated. This is how the Admin bundle can be used to
solve the issue by using the ``preUpdate`` saving hook.
.. code-block:: php
<?php
namespace FOS\UserBundle\Admin\Entity;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use FOS\UserBundle\Model\UserManagerInterface;
class UserAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->with('General')
->add('username')
->add('email')
->add('plainPassword', 'text')
->end()
->with('Groups')
->add('groups', 'sonata_type_model', array('required' => false))
->end()
->with('Management')
->add('roles', 'sonata_security_roles', array( 'multiple' => true))
->add('locked', null, array('required' => false))
->add('expired', null, array('required' => false))
->add('enabled', null, array('required' => false))
->add('credentialsExpired', null, array('required' => false))
->end()
;
}
public function preUpdate($user)
{
$this->getUserManager()->updateCanonicalFields($user);
$this->getUserManager()->updatePassword($user);
}
public function setUserManager(UserManagerInterface $userManager)
{
$this->userManager = $userManager;
}
/**
* @return UserManagerInterface
*/
public function getUserManager()
{
return $this->userManager;
}
}
The service declaration where the ``UserManager`` is injected into the Admin class.
.. configuration-block::
.. code-block:: xml
<service id="fos.user.admin.user" class="%fos.user.admin.user.class%">
<tag name="sonata.admin" manager_type="orm" group="fos_user" />
<argument />
<argument>%fos.user.admin.user.entity%</argument>
<argument />
<call method="setUserManager">
<argument type="service" id="fos_user.user_manager" />
</call>
</service>
Hooking in the Controller
-------------------------
You may have noticed that the hooks present in the **Admin** do not allow you
to interact with the process of deletion: you can't cancel it. To achieve this
you should be aware that there is also a way to hook on actions in the Controller.
If you define a custom controller that inherits from ``CRUDController``, you can
redefine the following methods:
- new object : ``preCreate($object)``
- edited object : ``preEdit($object)``
- deleted object : ``preDelete($object)``
- show object : ``preShow($object)``
- list objects : ``preList($object)``
If these methods return a **Response**, the process is interrupted and the response
will be returned as is by the controller (if it returns null, the process continues). You
can generate easily a redirection to the object show page by using the method
``redirectTo($object)``.
.. note::
Use case: you need to prohibit the deletion of a specific item. You may do a simple
check in the ``preDelete($object)`` method.
.. _FOSUserBundle on GitHub: https://github.com/FriendsOfSymfony/FOSUserBundle/

View File

@@ -0,0 +1,87 @@
Search
======
The admin comes with a basic global search available in the upper navigation menu. The search iterates over admin classes
and look for filter with the option ``global_search`` set to true. If you are using the ``SonataDoctrineORMBundle``
any text filter will be set to ``true`` by default.
Customization
-------------
The main action is using the template ``SonataAdminBundle:Core:search.html.twig``. And each search is handled by a
``block``, the template for the block is ``SonataAdminBundle:Block:block_search_result.html.twig``.
The default template values can be configured in the configuration section
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
templates:
# other configuration options
search: SonataAdminBundle:Core:search.html.twig
search_result_block: SonataAdminBundle:Block:block_search_result.html.twig
You also need to configure the block in the sonata block config
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_block:
blocks:
sonata.admin.block.search_result:
contexts: [admin]
You can also configure the block template per admin while defining the admin:
.. configuration-block::
.. code-block:: xml
<service id="app.admin.post" class="AppBundle\Admin\PostAdmin">
<tag name="sonata.admin" manager_type="orm" group="Content" label="Post" />
<argument />
<argument>AppBundle\Entity\Post</argument>
<argument />
<call method="setTemplate">
<argument>search_result_block</argument>
<argument>SonataPostBundle:Block:block_search_result.html.twig</argument>
</call>
</service>
Configure the default search result action
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In general the search result generates a link to the edit action of an item or is using the show action, if the edit
route is disabled or you haven't the required permission. You can change this behaviour by overriding the
``searchResultActions`` property. The defined action list will we checked successive until a route with the required
permissions exists. If no route is found, the item will be displayed as a text.
.. code-block:: php
<?php
// src/AppBundle/Admin/PersonAdmin.php
class PersonAdmin extends AbstractAdmin
{
// ...
protected $searchResultActions = array('edit', 'show');
// ...
}
Performance
-----------
The current implementation can be expensive if you have a lot of entities as the resulting query does a ``LIKE %query% OR LIKE %query%``...
.. note::
There is a work in progress to use an async JavaScript solution to better load data from the database.

View File

@@ -0,0 +1,770 @@
Security
========
User management
---------------
By default, the SonataAdminBundle does not come with any user management,
however it is most likely the application requires such a feature. The Sonata
Project includes a ``SonataUserBundle`` which integrates the ``FOSUserBundle``.
The ``FOSUserBundle`` adds support for a database-backed user system in Symfony.
It provides a flexible framework for user management that aims to handle common
tasks such as user login, registration and password retrieval.
The ``SonataUserBundle`` is just a thin wrapper to include the ``FOSUserBundle``
into the ``AdminBundle``. The ``SonataUserBundle`` includes:
* A default login area
* A default ``user_block`` template which is used to display the current user
and the logout link
* 2 Admin classes: User and Group
* A default class for User and Group.
There is a little magic in the ``SonataAdminBundle``: if the bundle detects the
``SonataUserBundle`` class, then the default ``user_block`` template will be
changed to use the one provided by the ``SonataUserBundle``.
The install process is available on the dedicated
`SonataUserBundle's documentation area`_.
Security handlers
-----------------
The security part is managed by a ``SecurityHandler``, the bundle comes with 3 handlers:
- ``sonata.admin.security.handler.role``: ROLES to handle permissions
- ``sonata.admin.security.handler.acl``: ACL and ROLES to handle permissions
- ``sonata.admin.security.handler.noop``: always returns true, can be used
with the Symfony firewall
The default value is ``sonata.admin.security.handler.noop``, if you want to
change the default value you can set the ``security_handler`` to
``sonata.admin.security.handler.acl`` or ``sonata.admin.security.handler.role``.
To quickly secure an admin the role security can be used. It allows to specify
the actions a user can do with the admin. The ACL security system is more advanced
and allows to secure the objects. For people using the previous ACL
implementation, you can switch the ``security_handler`` to the role security handler.
Configuration
~~~~~~~~~~~~~
Only the security handler is required to determine which type of security to use.
The other parameters are set as default, change them if needed.
Using roles:
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
security:
handler: sonata.admin.security.handler.role
Using ACL:
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
security:
handler: sonata.admin.security.handler.acl
# acl security information
information:
GUEST: [VIEW, LIST]
STAFF: [EDIT, LIST, CREATE]
EDITOR: [OPERATOR, EXPORT]
ADMIN: [MASTER]
# permissions not related to an object instance and also to be available when objects do not exist
# the DELETE admin permission means the user is allowed to batch delete objects
admin_permissions: [CREATE, LIST, DELETE, UNDELETE, EXPORT, OPERATOR, MASTER]
# permission related to the objects
object_permissions: [VIEW, EDIT, DELETE, UNDELETE, OPERATOR, MASTER, OWNER]
Later, we will explain how to set up ACL with the ``FriendsOfSymfony/UserBundle``.
Role handler
------------
The ``sonata.admin.security.handler.role`` allows you to operate finely on the
actions that can be done (depending on the entity class), without requiring to set up ACL.
Configuration
~~~~~~~~~~~~~
First, activate the role security handler as described above.
Each time a user tries to do an action in the admin, Sonata checks if he is
either a super admin (``ROLE_SUPER_ADMIN``) **or** has the permission.
The permissions are:
========== ==================================================
Permission Description
========== ==================================================
LIST view the list of objects
VIEW view the detail of one object
CREATE create a new object
EDIT update an existing object
DELETE delete an existing object
EXPORT (for the native Sonata export links)
ALL grants LIST, VIEW, CREATE, EDIT, DELETE and EXPORT
========== ==================================================
Each permission is relative to an admin: if you try to get a list in FooAdmin (declared as ``app.admin.foo``
service), Sonata will check if the user has the ``ROLE_APP_ADMIN_FOO_EDIT`` or ``ROLE_APP_ADMIN_FOO_ALL`` roles.
The role name will be based on the name of your admin service. For instance, ``acme.blog.post.admin`` will become ``ROLE_ACME_BLOG_POST_ADMIN_{ACTION}``.
.. note::
If your admin service is named like ``my.blog.admin.foo_bar`` (note the underscore ``_``) it will become: ``ROLE_MY_BLOG_ADMIN_FOO_BAR_{ACTION}``
So our ``security.yml`` file may look something like this:
.. configuration-block::
.. code-block:: yaml
# app/config/security.yml
security:
# ...
role_hierarchy:
# for convenience, I decided to gather Sonata roles here
ROLE_SONATA_FOO_READER:
- ROLE_SONATA_ADMIN_DEMO_FOO_LIST
- ROLE_SONATA_ADMIN_DEMO_FOO_VIEW
ROLE_SONATA_FOO_EDITOR:
- ROLE_SONATA_ADMIN_DEMO_FOO_CREATE
- ROLE_SONATA_ADMIN_DEMO_FOO_EDIT
ROLE_SONATA_FOO_ADMIN:
- ROLE_SONATA_ADMIN_DEMO_FOO_DELETE
- ROLE_SONATA_ADMIN_DEMO_FOO_EXPORT
# those are the roles I will use (less verbose)
ROLE_STAFF: [ROLE_USER, ROLE_SONATA_FOO_READER]
ROLE_ADMIN: [ROLE_STAFF, ROLE_SONATA_FOO_EDITOR, ROLE_SONATA_FOO_ADMIN]
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
# you could alternatively use for an admin who has all rights
ROLE_ALL_ADMIN: [ROLE_STAFF, ROLE_SONATA_FOO_ALL]
# set access_strategy to unanimous, else you may have unexpected behaviors
access_decision_manager:
strategy: unanimous
Note that we also set ``access_strategy`` to unanimous.
It means that if one voter (for example Sonata) refuses access, access will be denied.
For more information on this subject, please see `changing the access decision strategy`_
in the Symfony documentation.
Usage
~~~~~
You can now test if a user is authorized from an Admin class:
.. code-block:: php
if ($this->hasAccess('list')) {
// ...
}
From a controller extending ``Sonata\AdminBundle\Controller\CRUDController``:
.. code-block:: php
if ($this->admin->hasAccess('list')) {
// ...
}
Or from a Twig template:
.. code-block:: jinja
{% if is_granted('VIEW') %}
<p>Hello there!</p>
{% endif %}
Note that you do not have to re-specify the prefix.
Sonata checks those permissions for the action it handles internally.
Of course you will have to recheck them in your own code.
Yon can also create your own permissions, for example ``EMAIL``
(which will turn into role ``ROLE_APP_ADMIN_FOO_EMAIL``).
Going further
~~~~~~~~~~~~~
Because Sonata role handler supplements Symfony security, but does not override it, you are free to do more advanced operations.
For example, you can `create your own voter`_
Customizing the handler behavior
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you want to change the handler behavior (for example, to pass the current object to voters), extend
``Sonata\AdminBundle\Security\Handler\RoleSecurityHandler``, and override the ``isGranted`` method.
Then declare your handler as a service:
.. configuration-block::
.. code-block:: xml
<service id="app.security.handler.role" class="AppBundle\Security\Handler\RoleSecurityHandler" public="false">
<argument type="service" id="security.context" on-invalid="null" />
<argument type="collection">
<argument>ROLE_SUPER_ADMIN</argument>
</argument>
</service>
And specify it as Sonata security handler on your configuration:
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
security:
handler: app.security.handler.role
ACL and FriendsOfSymfony/UserBundle
-----------------------------------
If you want an easy way to handle users, please use:
- `FOSUserBundle <https://github.com/FriendsOfSymfony/FOSUserBundle>`_: handles
users and groups stored in RDBMS or MongoDB
- `SonataUserBundle <https://github.com/sonata-project/SonataUserBundle>`_: integrates the
``FriendsOfSymfony/UserBundle`` with the ``AdminBundle``
The security integration is a work in progress and has some known issues:
- ACL permissions are immutables
- A listener must be implemented that creates the object Access Control List
with the required rules if objects are created outside the Admin
Configuration
~~~~~~~~~~~~~
Before you can use ``FriendsOfSymfony/FOSUserBundle`` you need to set it up as
described in the documentation of the bundle. In step 4 you need to create a
User class (in a custom UserBundle). Do it as follows:
.. code-block:: php
<?php
// src/AppBundle/Entity/User.php
namespace AppBundle\Entity;
use Sonata\UserBundle\Entity\BaseUser as BaseUser;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="fos_user")
*/
class User extends BaseUser
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
public function __construct()
{
parent::__construct();
// your own logic
}
}
In your ``app/config/config.yml`` you then need to put the following:
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
fos_user:
db_driver: orm
firewall_name: main
user_class: AppBundle\Entity\User
The following configuration for the SonataUserBundle defines:
- the ``FriendsOfSymfony/FOSUserBundle`` as a security provider
- the login form for authentication
- the access control: resources with related required roles, the important
part is the admin configuration
- the ``acl`` option to enable the ACL
- the ``AdminPermissionMap`` defines the permissions of the Admin class
.. configuration-block::
.. code-block:: yaml
# src/AppBundle/Resources/config/services.yml
# Symfony >= 3
services:
security.acl.permission.map:
class: Sonata\AdminBundle\Security\Acl\Permission\AdminPermissionMap
# optionally use a custom MaskBuilder
#sonata.admin.security.mask.builder:
# class: Sonata\AdminBundle\Security\Acl\Permission\MaskBuilder
# Symfony < 3
parameters:
security.acl.permission.map.class: Sonata\AdminBundle\Security\Acl\Permission\AdminPermissionMap
# optionally use a custom MaskBuilder
#sonata.admin.security.mask.builder.class: Sonata\AdminBundle\Security\Acl\Permission\MaskBuilder
In ``app/config/security.yml``:
.. configuration-block::
.. code-block:: yaml
# app/config/security.yml
security:
providers:
fos_userbundle:
id: fos_user.user_manager
firewalls:
main:
pattern: .*
form-login:
provider: fos_userbundle
login_path: /login
use_forward: false
check_path: /login_check
failure_path: null
logout: true
anonymous: true
access_control:
# The WDT has to be allowed to anonymous users to avoid requiring the login with the AJAX request
- { path: ^/wdt/, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/profiler/, role: IS_AUTHENTICATED_ANONYMOUSLY }
# AsseticBundle paths used when using the controller for assets
- { path: ^/js/, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/css/, role: IS_AUTHENTICATED_ANONYMOUSLY }
# URL of FOSUserBundle which need to be available to anonymous users
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/login_check$, role: IS_AUTHENTICATED_ANONYMOUSLY } # for the case of a failed login
- { path: ^/user/new$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/user/check-confirmation-email$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/user/confirm/, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/user/confirmed$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/user/request-reset-password$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/user/send-resetting-email$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/user/check-resetting-email$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/user/reset-password/, role: IS_AUTHENTICATED_ANONYMOUSLY }
# Secured part of the site
# This config requires being logged for the whole site and having the admin role for the admin part.
# Change these rules to adapt them to your needs
- { path: ^/admin/, role: ROLE_ADMIN }
- { path: ^/.*, role: IS_AUTHENTICATED_ANONYMOUSLY }
role_hierarchy:
ROLE_ADMIN: [ROLE_USER, ROLE_SONATA_ADMIN]
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
acl:
connection: default
- Install the ACL tables ``php app/console init:acl``
- Create a new root user:
.. code-block:: bash
$ php app/console fos:user:create --super-admin
Please choose a username:root
Please choose an email:root@domain.com
Please choose a password:root
Created user root
If you have Admin classes, you can install or update the related CRUD ACL rules:
.. code-block:: bash
$ php app/console sonata:admin:setup-acl
Starting ACL AdminBundle configuration
> install ACL for sonata.media.admin.media
- add role: ROLE_SONATA_MEDIA_ADMIN_MEDIA_GUEST, permissions: ["VIEW","LIST"]
- add role: ROLE_SONATA_MEDIA_ADMIN_MEDIA_STAFF, permissions: ["EDIT","LIST","CREATE"]
- add role: ROLE_SONATA_MEDIA_ADMIN_MEDIA_EDITOR, permissions: ["OPERATOR","EXPORT"]
- add role: ROLE_SONATA_MEDIA_ADMIN_MEDIA_ADMIN, permissions: ["MASTER"]
... skipped ...
If you already have objects, you can generate the object ACL rules for each
object of an admin:
.. code-block:: bash
$ php app/console sonata:admin:generate-object-acl
Optionally, you can specify an object owner, and step through each admin. See
the help of the command for more information.
If you try to access to the admin class you should see the login form, just
log in with the ``root`` user.
An Admin is displayed in the dashboard (and menu) when the user has the role
``LIST``. To change this override the ``showIn`` method in the Admin class.
Roles and Access control lists
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A user can have several roles when working with an application. Each Admin class
has several roles, and each role specifies the permissions of the user for the
``Admin`` class. Or more specifically, what the user can do with the domain object(s)
the ``Admin`` class is created for.
By default each ``Admin`` class contains the following roles, override the
property ``$securityInformation`` to change this:
- ``ROLE_SONATA_..._GUEST``
a guest that is allowed to ``VIEW`` an object and a ``LIST`` of objects;
- ``ROLE_SONATA_..._STAFF``
probably the biggest part of the users, a staff user has the same permissions
as guests and is additionally allowed to ``EDIT`` and ``CREATE`` new objects;
- ``ROLE_SONATA_..._EDITOR``
an editor is granted all access and, compared to the staff users, is allowed to ``DELETE``;
- ``ROLE_SONATA_..._ADMIN``
an administrative user is granted all access and on top of that, the user is allowed to grant other users access.
Owner:
- when an object is created, the currently logged in user is set as owner for
that object and is granted all access for that object;
- this means the user owning the object is always allowed to ``DELETE`` the
object, even when they only have the staff role.
Vocabulary used for Access Control Lists:
- **Role:** a user role;
- **ACL:** a list of access rules, the Admin uses 2 types;
- **Admin ACL:** created from the Security information of the Admin class
for each admin and shares the Access Control Entries that specify what
the user can do (permissions) with the admin;
- **Object ACL:** also created from the security information of the ``Admin``
class however created for each object, it uses 2 scopes:
- **Class-Scope:** the class scope contains the rules that are valid
for all object of a certain class;
- **Object-Scope:** specifies the owner;
- **Sid:** Security identity, an ACL role for the Class-Scope ACL and the
user for the Object-Scope ACL;
- **Oid:** Object identity, identifies the ACL, for the admin ACL this is
the admin code, for the object ACL this is the object id;
- **ACE:** a role (or sid) and its permissions;
- **Permission:** this tells what the user is allowed to do with the Object
identity;
- **Bitmask:** a permission can have several bitmasks, each bitmask
represents a permission. When permission ``VIEW`` is requested and it
contains the ``VIEW`` and ``EDIT`` bitmask and the user only has the
``EDIT`` permission, then the permission ``VIEW`` is granted.
- **PermissionMap:** configures the bitmasks for each permission, to change
the default mapping create a voter for the domain class of the Admin.
There can be many voters that may have different permission maps. However,
prevent that multiple voters vote on the same class with overlapping bitmasks.
See the cookbook article "`Advanced ACL concepts
<http://symfony.com/doc/current/cookbook/security/acl_advanced.html#pre-authorization-decisions.>`_"
for the meaning of the different permissions.
How is access granted?
~~~~~~~~~~~~~~~~~~~~~~
In the application the security context is asked if access is granted for a role
or a permission (``admin.isGranted``):
- **Token:** a token identifies a user between requests;
- **Voter:** sort of judge that returns whether access is granted or denied, if the
voter should not vote for a case, it returns abstain;
- **AccessDecisionManager:** decides whether access is granted or denied according
a specific strategy. It grants access if at least one (affirmative strategy),
all (unanimous strategy) or more then half (consensus strategy) of the
counted votes granted access;
- **RoleVoter:** votes for all attributes stating with ``ROLE_`` and grants
access if the user has this role;
- **RoleHierarchyVoter:** when the role ``ROLE_SONATA_ADMIN`` is voted for,
it also votes "granted" if the user has the role ``ROLE_SUPER_ADMIN``;
- **AclVoter:** grants access for the permissions of the ``Admin`` class if
the user has the permission, the user has a permission that is included in
the bitmasks of the permission requested to vote for or the user owns the
object.
Create a custom voter or a custom permission map
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In some occasions you need to create a custom voter or a custom permission map
because for example you want to restrict access using extra rules:
- create a custom voter class that extends the ``AclVoter``
.. code-block:: php
<?php
// src/AppBundle/Security/Authorization/Voter/UserAclVoter.php
namespace AppBundle\Security\Authorization\Voter;
use FOS\UserBundle\Model\UserInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Acl\Voter\AclVoter;
class UserAclVoter extends AclVoter
{
public function supportsClass($class)
{
// support the Class-Scope ACL for votes with the custom permission map
// return $class === 'Sonata\UserBundle\Admin\Entity\UserAdmin' || is_subclass_of($class, 'FOS\UserBundle\Model\UserInterface');
// if you use php >=5.3.7 you can check the inheritance with is_a($class, 'Sonata\UserBundle\Admin\Entity\UserAdmin');
// support the Object-Scope ACL
return is_subclass_of($class, 'FOS\UserBundle\Model\UserInterface');
}
public function supportsAttribute($attribute)
{
return $attribute === 'EDIT' || $attribute === 'DELETE';
}
public function vote(TokenInterface $token, $object, array $attributes)
{
if (!$this->supportsClass(get_class($object))) {
return self::ACCESS_ABSTAIN;
}
foreach ($attributes as $attribute) {
if ($this->supportsAttribute($attribute) && $object instanceof UserInterface) {
if ($object->isSuperAdmin() && !$token->getUser()->isSuperAdmin()) {
// deny a non super admin user to edit a super admin user
return self::ACCESS_DENIED;
}
}
}
// use the parent vote with the custom permission map:
// return parent::vote($token, $object, $attributes);
// otherwise leave the permission voting to the AclVoter that is using the default permission map
return self::ACCESS_ABSTAIN;
}
}
- optionally create a custom permission map, copy to start the
``Sonata\AdminBundle\Security\Acl\Permission\AdminPermissionMap.php`` to
your bundle
- declare the voter and permission map as a service
.. configuration-block::
.. code-block:: xml
<!-- src/AppBundle/Resources/config/services.xml -->
<!-- <service id="security.acl.user_permission.map" class="AppBundle\Security\Acl\Permission\UserAdminPermissionMap" public="false"></service> -->
<service id="security.acl.voter.user_permissions" class="AppBundle\Security\Authorization\Voter\UserAclVoter" public="false">
<tag name="monolog.logger" channel="security" />
<argument type="service" id="security.acl.provider" />
<argument type="service" id="security.acl.object_identity_retrieval_strategy" />
<argument type="service" id="security.acl.security_identity_retrieval_strategy" />
<argument type="service" id="security.acl.permission.map" />
<argument type="service" id="logger" on-invalid="null" />
<tag name="security.voter" priority="255" />
</service>
- change the access decision strategy to ``unanimous``
.. configuration-block::
.. code-block:: yaml
# app/config/security.yml
security:
access_decision_manager:
# strategy value can be: affirmative, unanimous or consensus
strategy: unanimous
- to make this work the permission needs to be checked using the Object ACL
- modify the template (or code) where applicable:
.. code-block:: html+jinja
{% if admin.hasAccess('edit', user_object) %}
{# ... #}
{% endif %}
- because the object ACL permission is checked, the ACL for the object must
have been created, otherwise the ``AclVoter`` will deny ``EDIT`` access
for a non super admin user trying to edit another non super admin user.
This is automatically done when the object is created using the Admin.
If objects are also created outside the Admin, have a look at the
``createSecurityObject`` method in the ``AclSecurityHandler``.
Usage
~~~~~
Every time you create a new ``Admin`` class, you should start with the command
``php app/console sonata:admin:setup-acl`` so the ACL database will be updated
with the latest roles and permissions.
In the templates, or in your code, you can use the Admin method ``hasAccess()``:
- check for an admin that the user is allowed to ``EDIT``:
.. code-block:: html+jinja
{# use the admin security method #}
{% if admin.hasAccess('edit') %}
{# ... #}
{% endif %}
{# or use the default is_granted Symfony helper, the following will give the same result #}
{% if is_granted('ROLE_SUPER_ADMIN') or is_granted('EDIT', admin) %}
{# ... #}
{% endif %}
- check for an admin that the user is allowed to ``DELETE``, the object is added
to also check if the object owner is allowed to ``DELETE``:
.. code-block:: html+jinja
{# use the admin security method #}
{% if admin.hasAccess('delete', object) %}
{# ... #}
{% endif %}
{# or use the default is_granted Symfony helper, the following will give the same result #}
{% if is_granted('ROLE_SUPER_ADMIN') or is_granted('DELETE', object) %}
{# ... #}
{% endif %}
List filtering
~~~~~~~~~~~~~~
List filtering using ACL is available as a third party bundle:
`CoopTilleulsAclSonataAdminExtensionBundle <https://github.com/coopTilleuls/CoopTilleulsAclSonataAdminExtensionBundle>`_.
When enabled, the logged in user will only see the objects for which it has the `VIEW` right (or superior).
ACL editor
----------
SonataAdminBundle provides a user-friendly ACL editor
interface.
It will be automatically available if the ``sonata.admin.security.handler.acl``
security handler is used and properly configured.
The ACL editor is only available for users with `OWNER` or `MASTER` permissions
on the object instance.
The `OWNER` and `MASTER` permissions can only be edited by an user with the
`OWNER` permission on the object instance.
.. figure:: ../images/acl_editor.png
:align: center
:alt: The ACL editor
:width: 700px
User list customization
~~~~~~~~~~~~~~~~~~~~~~~
By default, the ACL editor allows to set permissions for all users managed by
``FOSUserBundle``.
To customize displayed user override
``Sonata\AdminBundle\Controller\CRUDController::getAclUsers()``. This method must
return an iterable collection of users.
.. code-block:: php
protected function getAclUsers()
{
$userManager = $container->get('fos_user.user_manager');
// Display only kevin and anne
$users[] = $userManager->findUserByUsername('kevin');
$users[] = $userManager->findUserByUsername('anne');
return new \ArrayIterator($users);
}
Role list customization
~~~~~~~~~~~~~~~~~~~~~~~
By default, the ACL editor allows to set permissions for all roles.
To customize displayed role override
``Sonata\AdminBundle\Controller\CRUDController::getAclRoles()``. This method must
return an iterable collection of roles.
.. code-block:: php
protected function getAclRoles()
{
// Display only ROLE_BAPTISTE and ROLE_HELENE
$roles = array(
'ROLE_BAPTISTE',
'ROLE_HELENE'
);
return new \ArrayIterator($roles);
}
Custom user manager
~~~~~~~~~~~~~~~~~~~
If your project does not use `FOSUserBundle`, you can globally configure another
service to use when retrieving your users.
- Create a service with a method called ``findUsers()`` returning an iterable
collection of users
- Update your admin configuration to reference your service name
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
security:
# the name of your service
acl_user_manager: my_user_manager
.. _`SonataUserBundle's documentation area`: https://sonata-project.org/bundles/user/master/doc/reference/installation.html
.. _`changing the access decision strategy`: http://symfony.com/doc/2.2/cookbook/security/voters.html#changing-the-access-decision-strategy
.. _`create your own voter`: http://symfony.com/doc/2.2/cookbook/security/voters.html

View File

@@ -0,0 +1,198 @@
Templates
=========
``SonataAdminBundle`` comes with a significant amount of ``twig`` files used to display the
different parts of each ``Admin`` action's page. If you read the ``Templates`` part of the
:doc:`architecture` section of this guide, you should know by now how these are organized in
the ``views`` folder. If you haven't, now would be a good time to do it.
Besides these, some other views files are included from the storage layer. As their content and
structure are specific to each implementation, they are not discussed here, but it's important
that you keep in mind that they exist and are as relevant as the view files included
directly in ``SonataAdminBundle``.
Global Templates
----------------
``SonataAdminBundle`` views are implemented using ``twig`` files, and take full advantage of its
inheritance capabilities. As such, even the most simple page is actually rendered using many
different ``twig`` files. At the end of that ``twig`` inheritance hierarchy is always one of two files:
* layout: SonataAdminBundle::standard_layout.html.twig
* ajax: SonataAdminBundle::ajax_layout.html.twig
As you might have guessed from their names, the first is used in 'standard' request and the other
for AJAX calls. The ``SonataAdminBundle::standard_layout.html.twig`` contains several elements which
exist across the whole page, like the logo, title, upper menu and menu. It also includes the base CSS
and JavaScript files and libraries used across the whole administration section. The AJAX template
doesn't include any of these elements.
Dashboard Template
------------------
The template used for rendering the dashboard can also be configured. See the :doc:`dashboard` page
for more information
CRUDController Actions Templates
--------------------------------
As seen before, the ``CRUDController`` has several actions that allow you to manipulate your
model instances. Each of those actions uses a specific template file to render its content.
By default, ``SonataAdminBundle`` uses the following templates for their matching action:
* ``list`` : SonataAdminBundle:CRUD:list.html.twig
* ``show`` : SonataAdminBundle:CRUD:show.html.twig
* ``edit`` : SonataAdminBundle:CRUD:edit.html.twig
* ``history`` : SonataAdminBundle:CRUD:history.html.twig
* ``preview`` : SonataAdminBundle:CRUD:preview.html.twig
* ``delete`` : SonataAdminBundle:CRUD:delete.html.twig
* ``batch_confirmation`` : SonataAdminBundle:CRUD:batch_confirmation.html.twig
* ``acl`` : SonataAdminBundle:CRUD:acl.html.twig
Notice that all these templates extend other templates, and some do only that. This inheritance
architecture is designed to help you easily make customizations by extending these templates
in your own bundle, rather than rewriting everything.
If you look closely, all of these templates ultimately extend the ``base_template`` variable that's
passed from the controller. This variable will always take the value of one of the above mentioned
global templates, and this is how changes made to those files affect all the ``SonataAdminBundle``
interface.
Row Templates
-------------
It is possible to completely change how each row of results is rendered in the
list view, by customizing the ``inner_list_row`` and ``base_list_field`` templates.
For more information about this, see the :doc:`../cookbook/recipe_row_templates`
cookbook entry.
Other Templates
---------------
There are several other templates that can be customized, enabling you to fine-tune
``SonataAdminBundle``:
* ``user_block`` : customizes the Twig block rendered by default in the top right
corner of the admin interface, containing user information.
Empty by default, see ``SonataUserBundle`` for a real example.
* ``add_block`` : customizes the Twig block rendered by default in the top right
corner of the admin interface, providing quick access to create operations on
available admin classes.
* ``history_revision_timestamp:`` customizes the way timestamps are rendered when
using history related actions.
* ``action`` : a generic template you can use for your custom actions
* ``short_object_description`` : used by the ``getShortObjectDescriptionAction``
action from the ``HelperController``, this template displays a small
description of a model instance.
* ``list_block`` : the template used to render the dashboard's admin mapping lists.
More info on the :doc:`dashboard` page.
* batch: template used to render the checkboxes that precede each instance on list views.
* ``select`` : when loading list views as part of sonata_admin form types, this
template is used to create a button that allows you to select the matching line.
* ``pager_links`` : renders the list of pages displayed at the end of the list view
(when more than one page exists)
* ``pager_results`` : renders the dropdown that lets you choose the number of
elements per page on list views
Configuring templates
---------------------
Like said before, the main goal of this template structure is to make it easy for you
to customize the ones you need. You can simply extend the ones you want in your own bundle,
and tell ``SonataAdminBundle`` to use your templates instead of the default ones. You can do so
in several ways.
You can specify your templates in the config.yml file, like so:
.. configuration-block::
.. code-block:: yaml
# app/config/config.yml
sonata_admin:
templates:
layout: SonataAdminBundle::standard_layout.html.twig
ajax: SonataAdminBundle::ajax_layout.html.twig
list: SonataAdminBundle:CRUD:list.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
preview: SonataAdminBundle:CRUD:preview.html.twig
delete: SonataAdminBundle:CRUD:delete.html.twig
batch: SonataAdminBundle:CRUD:list__batch.html.twig
acl: SonataAdminBundle:CRUD:acl.html.twig
action: SonataAdminBundle:CRUD:action.html.twig
select: SonataAdminBundle:CRUD:list__select.html.twig
filter: SonataAdminBundle:Form:filter_admin_fields.html.twig
dashboard: SonataAdminBundle:Core:dashboard.html.twig
search: SonataAdminBundle:Core:search.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
list_block: SonataAdminBundle:Block:block_admin_list.html.twig
user_block: SonataAdminBundle:Core:user_block.html.twig
add_block: SonataAdminBundle:Core:add_block.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
history_revision_timestamp: SonataAdminBundle:CRUD:history_revision_timestamp.html.twig
short_object_description: SonataAdminBundle:Helper:short-object-description.html.twig
search_result_block: SonataAdminBundle:Block:block_search_result.html.twig
action_create: SonataAdminBundle:CRUD:dashboard__action_create.html.twig
button_acl: SonataAdminBundle:Button:acl_button.html.twig
button_create: SonataAdminBundle:Button:create_button.html.twig
button_edit: SonataAdminBundle:Button:edit_button.html.twig
button_history: SonataAdminBundle:Button:history_button.html.twig
button_list: SonataAdminBundle:Button:list_button.html.twig
button_show: SonataAdminBundle:Button:show_button.html.twig
Notice that this is a global change, meaning it will affect all model mappings automatically,
both for ``Admin`` mappings defined by you and by other bundles.
If you wish, you can specify custom templates on a per ``Admin`` mapping basis. Internally,
the ``CRUDController`` fetches this information from the ``Admin`` class instance, so you can
specify the templates to use in the ``Admin`` service definition:
.. configuration-block::
.. code-block:: xml
<service id="app.admin.post" class="AppBundle\Admin\PostAdmin">
<tag name="sonata.admin" manager_type="orm" group="Content" label="Post" />
<argument />
<argument>AppBundle\Entity\Post</argument>
<argument />
<call method="setTemplate">
<argument>edit</argument>
<argument>AppBundle:PostAdmin:edit.html.twig</argument>
</call>
</service>
.. code-block:: yaml
services:
app.admin.post:
class: AppBundle\Admin\PostAdmin
tags:
- { name: sonata.admin, manager_type: orm, group: "Content", label: "Post" }
arguments:
- ~
- AppBundle\Entity\Post
- ~
calls:
- [ setTemplate, [edit, AppBundle:PostAdmin:edit.html.twig]]
public: true
.. note::
A ``setTemplates(array $templates)`` (notice the plural) function also exists, that allows
you to set multiple templates at once. Notice that, if used outside of the service definition
context, ``setTemplates(array $templates)`` will replace the whole template list for that
``Admin`` class, meaning you have to explicitly pass the full template list in the
``$templates`` argument.
Changes made using the ``setTemplate()`` and ``setTemplates()`` functions override the customizations
made in the configuration file, so you can specify a global custom template and then override that
customization on a specific ``Admin`` class.

View File

@@ -0,0 +1,187 @@
Translation
===========
There are two main catalogue names in an Admin class:
* ``SonataAdminBundle``: this catalogue is used to translate shared messages
across different Admins
* ``messages``: this catalogue is used to translate the messages for the current
Admin
Ideally the ``messages`` catalogue should be changed to avoid any issues with
other Admin classes.
You have two options to configure the catalogue for the Admin class:
* override the ``$translationDomain`` property
.. code-block:: php
<?php
class PageAdmin extends AbstractAdmin
{
protected $translationDomain = 'SonataPageBundle'; // default is 'messages'
}
* inject the value through the container
.. configuration-block::
.. code-block:: xml
<service id="sonata.page.admin.page" class="Sonata\PageBundle\Admin\PageAdmin">
<tag name="sonata.admin" manager_type="orm" group="sonata_page" label="Page" />
<argument />
<argument>Application\Sonata\PageBundle\Entity\Page</argument>
<argument />
<call method="setTranslationDomain">
<argument>SonataPageBundle</argument>
</call>
</service>
An Admin instance always gets the ``translator`` instance, so it can be used to
translate messages within the ``configureFields`` method or in templates.
.. code-block:: jinja
{# the classical call by using the twig trans helper #}
{{ 'message_create_snapshots'|trans({}, 'SonataPageBundle') }}
{# by using the admin trans method with hardcoded catalogue #}
{{ 'message_create_snapshots'|trans({}, 'SonataPageBundle') }}
{# by using the admin trans with the configured catalogue #}
{{ 'message_create_snapshots'|trans({}, admin.translationdomain) }}
The last solution is most flexible, as no catalogue parameters are hardcoded, and is the recommended one to use.
Translate field labels
----------------------
The Admin bundle comes with a customized form field template. The most notable
change from the original one is the use of the translation domain provided by
either the Admin instance or the field description to translate labels.
Overriding the translation domain
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The translation domain (message catalog) can be overridden at either the form
group or individual field level.
If a translation domain is set at the group level it will cascade down to all
fields within the group.
Overriding the translation domain is of particular use when using
:doc:`extensions`, where the extension and the translations would
be defined in one bundle, but implemented in many different Admin instances.
Setting the translation domain on an individual field:
.. code-block:: php
$formMapper
->with('form.my_group')
->add('publishable', 'checkbox', array(), array(
'translation_domain' => 'MyTranslationDomain',
))
->end()
;
The following example sets the default translation domain on a form group and
over-rides that setting for one of the fields:
.. code-block:: php
$formMapper
->with('form.my_group', array('translation_domain' => 'MyDomain'))
->add('publishable', 'checkbox', array(), array(
'translation_domain' => 'AnotherDomain',
))
->add('start_date', 'date', array(), array())
->end()
;
Translation can also be disabled on a specific field by setting
``translation_domain`` to ``false``.
Setting the label name
^^^^^^^^^^^^^^^^^^^^^^
By default, the label is set to a sanitized version of the field name. A custom
label can be defined as the third argument of the ``add`` method:
.. code-block:: php
class PageAdmin extends AbstractAdmin
{
public function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('isValid', null, array(
'required' => false,
'label' => 'label.is_valid',
))
;
}
}
Label strategies
^^^^^^^^^^^^^^^^
There is another option for rapid prototyping or to avoid spending too much time
adding the ``label`` key to all option fields: **Label Strategies**. By default
labels are generated by using a simple rule:
``isValid => Is Valid``
The ``AdminBundle`` comes with different key label generation strategies:
* ``sonata.admin.label.strategy.native``: DEFAULT - Makes the string human readable
``isValid`` => ``Is Valid``
* ``sonata.admin.label.strategy.form_component``: The default behavior from the Form Component
``isValid`` => ``Isvalid``
* ``sonata.admin.label.strategy.underscore``: Changes the name into a token suitable
for translation by prepending "form.label" to an underscored version of the field name
``isValid`` => ``form.label_is_valid``
* ``sonata.admin.label.strategy.noop``: does not alter the string
``isValid`` => ``isValid``
``sonata.admin.label.strategy.underscore`` will be better for i18n applications
and ``sonata.admin.label.strategy.native`` will be better for native (single) language
apps based on the field name. It is reasonable to start with the ``native`` strategy
and then, when the application needs to be translated using generic keys, the
configuration can be switched to ``underscore``.
The strategy can be quickly configured when the Admin class is registered in
the Container:
.. configuration-block::
.. code-block:: xml
<service id="app.admin.project" class="AppBundle\Admin\ProjectAdmin">
<tag
name="sonata.admin"
manager_type="orm"
group="Project"
label="Project"
label_translator_strategy="sonata.admin.label.strategy.native"
/>
<argument />
<argument>AppBundle\Entity\Project</argument>
<argument />
</service>
.. note::
In all cases the label will be used by the ``Translator``. The strategy is
just a quick way to generate translatable keys. It all depends on the
project's requirements.
.. note::
When the strategy method is called, ``context`` (breadcrumb, datagrid, filter,
form, list, show, etc.) and ``type`` (usually link or label) arguments are passed.
For example, the call may look like: ``getLabel($label_key, 'breadcrumb', 'link')``

View File

@@ -0,0 +1,80 @@
Troubleshooting
===============
The toString method
-------------------
Sometimes the bundle needs to display your model objects, in order to do it,
objects are converted to string by using the `__toString`_ magic method.
Take care to never return anything else than a string in this method.
For example, if your method looks like that :
.. code-block:: php
<?php
// src/AppBundle/Entity/Post.php
class Post
{
// ...
public function __toString()
{
return $this->getTitle();
}
// ...
}
You cannot be sure your object will *always* have a title when the bundle will want to convert it to a string.
So in order to avoid any fatal error, you must return an empty string
(or anything you prefer) for when the title is missing, like this :
.. code-block:: php
<?php
// src/AppBundle/Entity/Post.php
class Post
{
// ...
public function __toString()
{
return $this->getTitle() ?: '';
}
// ...
}
.. _`__toString`: http://www.php.net/manual/en/language.oop5.magic.php#object.tostring
Large filters and long URLs problem
-----------------------------------
If you will try to add hundreds of filters to a single admin class, you will get a problem - very long generated filter form URL.
In most cases you will get server response like *Error 400 Bad Request* OR *Error 414 Request-URI Too Long*. According to
`a StackOverflow discussion <http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers>`_
"safe" URL length is just around 2000 characters.
You can fix this issue by adding a simple JQuery piece of code on your edit template :
.. code-block:: javascript
$(function() {
// Add class 'had-value-on-load' to inputs/selects with values.
$(".sonata-filter-form input").add(".sonata-filter-form select").each(function(){
if($(this).val()) {
$(this).addClass('had-value-on-load');
}
});
// REMOVE ALL EMPTY INPUT FROM FILTER FORM (except inputs, which has class 'had-value-on-load')
$(".sonata-filter-form").submit(function() {
$(".sonata-filter-form input").add(".sonata-filter-form select").each(function(){
if(!$(this).val() && !$(this).hasClass('had-value-on-load')) {
$(this).remove()
};
});
});
});

View File

@@ -0,0 +1,3 @@
Sphinx
git+https://github.com/fabpot/sphinx-php.git
sphinx_rtd_theme

View File

@@ -0,0 +1,60 @@
The MIT License
Copyright (c) 2010-2015 thomas.rabaix@sonata-project.org
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
///////////////////////////////////////////////////////////////////////////////
Silk icon set 1.3
_________________________________________
Mark James
http://www.famfamfam.com/lab/icons/silk/
_________________________________________
This work is licensed under a
Creative Commons Attribution 2.5 License.
[ http://creativecommons.org/licenses/by/2.5/ ]
This means you may use it for any purpose,
and make any changes you like.
All I ask is that you include a link back
to this page in your credits.
Are you using this icon set? Send me an email
(including a link or picture if available) to
mjames@gmail.com
Any other questions about this icon set please
contact mjames@gmail.com
///////////////////////////////////////////////////////////////////////////////
jQuery Form Plugin
version: 2.64 (25-FEB-2011)
@requires jQuery v1.3.2 or later
Examples and documentation at: http://malsup.com/jquery/form/
Dual licensed under the MIT and GPL licenses:
http://www.opensource.org/licenses/mit-license.php
http://www.gnu.org/licenses/gpl.html
Note : MIT license is applied in this bundle
///////////////////////////////////////////////////////////////////////////////

View File

@@ -0,0 +1,691 @@
/*
This file is part of the Sonata package.
(c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
*/
var Admin = {
collectionCounters: [],
/**
* This function must be called when an ajax call is done, to ensure
* the retrieved html is properly setup
*
* @param subject
*/
shared_setup: function(subject) {
Admin.log("[core|shared_setup] Register services on", subject);
Admin.set_object_field_value(subject);
Admin.add_filters(subject);
Admin.setup_select2(subject);
Admin.setup_icheck(subject);
Admin.setup_checkbox_range_selection(subject);
Admin.setup_xeditable(subject);
Admin.setup_form_tabs_for_errors(subject);
Admin.setup_inline_form_errors(subject);
Admin.setup_tree_view(subject);
Admin.setup_collection_counter(subject);
Admin.setup_sticky_elements(subject);
Admin.setup_readmore_elements(subject);
// Admin.setup_list_modal(subject);
},
setup_list_modal: function(modal) {
Admin.log('[core|setup_list_modal] configure modal on', modal);
// this will force relation modal to open list of entity in a wider modal
// to improve readability
jQuery('div.modal-dialog', modal).css({
width: '90%', //choose your width
height: '85%',
padding: 0
});
jQuery('div.modal-content', modal).css({
'border-radius':'0',
height: '100%',
padding: 0
});
jQuery('.modal-body', modal).css({
width: 'auto',
height: '90%',
padding: 15,
overflow: 'auto'
});
jQuery(modal).trigger('sonata-admin-setup-list-modal');
},
setup_select2: function(subject) {
if (window.SONATA_CONFIG && window.SONATA_CONFIG.USE_SELECT2) {
Admin.log('[core|setup_select2] configure Select2 on', subject);
jQuery('select:not([data-sonata-select2="false"])', subject).each(function() {
var select = jQuery(this);
var allowClearEnabled = false;
var popover = select.data('popover');
select.removeClass('form-control');
if (select.find('option[value=""]').length || select.attr('data-sonata-select2-allow-clear')==='true') {
allowClearEnabled = true;
} else if (select.attr('data-sonata-select2-allow-clear')==='false') {
allowClearEnabled = false;
}
select.select2({
width: function(){
// Select2 v3 and v4 BC. If window.Select2 is defined, then the v3 is installed.
// NEXT_MAJOR: Remove Select2 v3 support.
return Admin.get_select2_width(window.Select2 ? this.element : select);
},
dropdownAutoWidth: true,
minimumResultsForSearch: 10,
allowClear: allowClearEnabled
});
if (undefined !== popover) {
select
.select2('container')
.popover(popover.options)
;
}
});
}
},
setup_icheck: function(subject) {
if (window.SONATA_CONFIG && window.SONATA_CONFIG.USE_ICHECK) {
Admin.log('[core|setup_icheck] configure iCheck on', subject);
jQuery("input[type='checkbox']:not('label.btn>input'), input[type='radio']:not('label.btn>input')", subject).iCheck({
checkboxClass: 'icheckbox_square-blue',
radioClass: 'iradio_square-blue'
});
}
},
/**
* Setup checkbox range selection
*
* Clicking on a first checkbox then another with shift + click
* will check / uncheck all checkboxes between them
*
* @param {string|Object} subject The html selector or object on which function should be applied
*/
setup_checkbox_range_selection: function(subject) {
Admin.log('[core|setup_checkbox_range_selection] configure checkbox range selection on', subject);
var previousIndex,
useICheck = window.SONATA_CONFIG && window.SONATA_CONFIG.USE_ICHECK
;
// When a checkbox or an iCheck helper is clicked
jQuery('tbody input[type="checkbox"], tbody .iCheck-helper', subject).click(function (event) {
var input;
if (useICheck) {
input = jQuery(this).prev('input[type="checkbox"]');
} else {
input = jQuery(this);
}
if (input.length) {
var currentIndex = input.closest('tr').index();
if (event.shiftKey && previousIndex >= 0) {
var isChecked = jQuery('tbody input[type="checkbox"]:nth(' + currentIndex + ')', subject).prop('checked');
// Check all checkbox between previous and current one clicked
jQuery('tbody input[type="checkbox"]', subject).each(function (i, e) {
if (i > previousIndex && i < currentIndex || i > currentIndex && i < previousIndex) {
if (useICheck) {
jQuery(e).iCheck(isChecked ? 'check' : 'uncheck');
return;
}
jQuery(e).prop('checked', isChecked);
}
});
}
previousIndex = currentIndex;
}
});
},
setup_xeditable: function(subject) {
Admin.log('[core|setup_xeditable] configure xeditable on', subject);
jQuery('.x-editable', subject).editable({
emptyclass: 'editable-empty btn btn-sm btn-default',
emptytext: '<i class="fa fa-pencil"></i>',
container: 'body',
placement: 'auto',
success: function(response) {
var html = jQuery(response);
Admin.setup_xeditable(html);
jQuery(this)
.closest('td')
.replaceWith(html);
},
error: function(xhr, statusText, errorThrown) {
return xhr.responseText;
}
});
},
/**
* render log message
* @param mixed
*/
log: function() {
var msg = '[Sonata.Admin] ' + Array.prototype.join.call(arguments,', ');
if (window.console && window.console.log) {
window.console.log(msg);
} else if (window.opera && window.opera.postError) {
window.opera.postError(msg);
}
},
/**
* NEXT_MAJOR: remove this function.
*
* @deprecated in version 3.0
*/
add_pretty_errors: function() {
console.warn('Admin.add_pretty_errors() was deprecated in version 3.0');
},
stopEvent: function(event) {
event.preventDefault();
return event.target;
},
add_filters: function(subject) {
Admin.log('[core|add_filters] configure filters on', subject);
jQuery('a.sonata-toggle-filter', subject).on('click', function(e) {
e.preventDefault();
e.stopPropagation();
if (jQuery(e.target).attr('sonata-filter') == 'false') {
return;
}
Admin.log('[core|add_filters] handle filter container: ', jQuery(e.target).attr('filter-container'))
var filters_container = jQuery('#' + jQuery(e.currentTarget).attr('filter-container'));
if (jQuery('div[sonata-filter="true"]:visible', filters_container).length == 0) {
jQuery(filters_container).slideDown();
}
var targetSelector = jQuery(e.currentTarget).attr('filter-target'),
target = jQuery('div[id="' + targetSelector + '"]', filters_container),
filterToggler = jQuery('i', '.sonata-toggle-filter[filter-target="' + targetSelector + '"]')
;
if (jQuery(target).is(":visible")) {
filterToggler
.removeClass('fa-check-square-o')
.addClass('fa-square-o')
;
target.hide();
} else {
filterToggler
.removeClass('fa-square-o')
.addClass('fa-check-square-o')
;
target.show();
}
if (jQuery('div[sonata-filter="true"]:visible', filters_container).length > 0) {
jQuery(filters_container).slideDown();
} else {
jQuery(filters_container).slideUp();
}
});
jQuery('.sonata-filter-form', subject).on('submit', function () {
jQuery(this).find('[sonata-filter="true"]:hidden :input').val('');
});
/* Advanced filters */
if (jQuery('.advanced-filter :input:visible', subject).filter(function () { return jQuery(this).val() }).length === 0) {
jQuery('.advanced-filter').hide();
};
jQuery('[data-toggle="advanced-filter"]', subject).click(function() {
jQuery('.advanced-filter').toggle();
});
},
/**
* Change object field value
* @param subject
*/
set_object_field_value: function(subject) {
Admin.log('[core|set_object_field_value] set value field on', subject);
this.log(jQuery('a.sonata-ba-edit-inline', subject));
jQuery('a.sonata-ba-edit-inline', subject).click(function(event) {
Admin.stopEvent(event);
var subject = jQuery(this);
jQuery.ajax({
url: subject.attr('href'),
type: 'POST',
success: function(response) {
var elm = jQuery(subject).parent();
elm.children().remove();
// fix issue with html comment ...
elm.html(jQuery(response.replace(/<!--[\s\S]*?-->/g, "")).html());
elm.effect("highlight", {'color' : '#57A957'}, 2000);
Admin.set_object_field_value(elm);
},
error: function(xhr, statusText, errorThrown) {
jQuery(subject).parent().effect("highlight", {'color' : '#C43C35'}, 2000);
}
});
});
},
setup_collection_counter: function(subject) {
Admin.log('[core|setup_collection_counter] setup collection counter', subject);
// Count and save element of each collection
var highestCounterRegexp = new RegExp('_([0-9]+)[^0-9]*$');
jQuery(subject).find('[data-prototype]').each(function() {
var collection = jQuery(this);
var counter = 0;
collection.children().each(function() {
var matches = highestCounterRegexp.exec(jQuery('[id^="sonata-ba-field-container"]', this).attr('id'));
if (matches && matches[1] && matches[1] > counter) {
counter = parseInt(matches[1], 10);
}
});
Admin.collectionCounters[collection.attr('id')] = counter;
});
},
setup_collection_buttons: function(subject) {
jQuery(subject).on('click', '.sonata-collection-add', function(event) {
Admin.stopEvent(event);
var container = jQuery(this).closest('[data-prototype]');
var counter = ++Admin.collectionCounters[container.attr('id')];
var proto = container.attr('data-prototype');
var protoName = container.attr('data-prototype-name') || '__name__';
// Set field id
var idRegexp = new RegExp(container.attr('id')+'_'+protoName,'g');
proto = proto.replace(idRegexp, container.attr('id')+'_'+counter);
// Set field name
var parts = container.attr('id').split('_');
var nameRegexp = new RegExp(parts[parts.length-1]+'\\]\\['+protoName,'g');
proto = proto.replace(nameRegexp, parts[parts.length-1]+']['+counter);
jQuery(proto)
.insertBefore(jQuery(this).parent())
.trigger('sonata-admin-append-form-element')
;
jQuery(this).trigger('sonata-collection-item-added');
});
jQuery(subject).on('click', '.sonata-collection-delete', function(event) {
Admin.stopEvent(event);
jQuery(this).trigger('sonata-collection-item-deleted');
jQuery(this).closest('.sonata-collection-row').remove();
jQuery(document).trigger('sonata-collection-item-deleted-successful');
});
},
setup_per_page_switcher: function(subject) {
Admin.log('[core|setup_per_page_switcher] setup page switcher', subject);
jQuery('select.per-page').change(function(event) {
jQuery('input[type=submit]').hide();
window.top.location.href=this.options[this.selectedIndex].value;
});
},
setup_form_tabs_for_errors: function(subject) {
Admin.log('[core|setup_form_tabs_for_errors] setup form tab\'s errors', subject);
// Switch to first tab with server side validation errors on page load
jQuery('form', subject).each(function() {
Admin.show_form_first_tab_with_errors(jQuery(this), '.sonata-ba-field-error');
});
// Switch to first tab with HTML5 errors on form submit
jQuery(subject)
.on('click', 'form [type="submit"]', function() {
Admin.show_form_first_tab_with_errors(jQuery(this).closest('form'), ':invalid');
})
.on('keypress', 'form [type="text"]', function(e) {
if (13 === e.which) {
Admin.show_form_first_tab_with_errors(jQuery(this), ':invalid');
}
})
;
},
show_form_first_tab_with_errors: function(form, errorSelector) {
Admin.log('[core|show_form_first_tab_with_errors] show first tab with errors', form);
var tabs = form.find('.nav-tabs a'), firstTabWithErrors;
tabs.each(function() {
var id = jQuery(this).attr('href'),
tab = jQuery(this),
icon = tab.find('.has-errors');
if (jQuery(id).find(errorSelector).length > 0) {
// Only show first tab with errors
if (!firstTabWithErrors) {
tab.tab('show');
firstTabWithErrors = tab;
}
icon.removeClass('hide');
} else {
icon.addClass('hide');
}
});
},
setup_inline_form_errors: function(subject) {
Admin.log('[core|setup_inline_form_errors] show first tab with errors', subject);
var deleteCheckboxSelector = '.sonata-ba-field-inline-table [id$="_delete"][type="checkbox"]';
jQuery(deleteCheckboxSelector, subject).each(function() {
Admin.switch_inline_form_errors(jQuery(this));
});
jQuery(subject).on('change', deleteCheckboxSelector, function() {
Admin.switch_inline_form_errors(jQuery(this));
});
},
/**
* Disable inline form errors when the row is marked for deletion
*/
switch_inline_form_errors: function(subject) {
Admin.log('[core|switch_inline_form_errors] switch_inline_form_errors', subject);
var row = subject.closest('.sonata-ba-field-inline-table'),
errors = row.find('.sonata-ba-field-error-messages')
;
if (subject.is(':checked')) {
row
.find('[required]')
.removeAttr('required')
.attr('data-required', 'required')
;
errors.hide();
} else {
row
.find('[data-required]')
.attr('required', 'required')
;
errors.show();
}
},
setup_tree_view: function(subject) {
Admin.log('[core|setup_tree_view] setup tree view', subject);
jQuery('ul.js-treeview', subject).treeView();
},
/** Return the width for simple and sortable select2 element **/
get_select2_width: function(element){
var ereg = /width:(auto|(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc)))/i;
// this code is an adaptation of select2 code (initContainerWidth function)
var style = element.attr('style');
//console.log("main style", style);
if (style !== undefined) {
var attrs = style.split(';');
for (var i = 0, l = attrs.length; i < l; i = i + 1) {
var matches = attrs[i].replace(/\s/g, '').match(ereg);
if (matches !== null && matches.length >= 1)
return matches[1];
}
}
style = element.css('width');
if (style.indexOf("%") > 0) {
return style;
}
return '100%';
},
setup_sortable_select2: function(subject, data) {
var transformedData = [];
for (var i = 0 ; i < data.length ; i++) {
transformedData[i] = {id: data[i].data, text: data[i].label};
}
subject.select2({
width: function(){
// Select2 v3 and v4 BC. If window.Select2 is defined, then the v3 is installed.
// NEXT_MAJOR: Remove Select2 v3 support.
return Admin.get_select2_width(window.Select2 ? this.element : subject);
},
dropdownAutoWidth: true,
data: transformedData,
multiple: true
});
subject.select2("container").find("ul.select2-choices").sortable({
containment: 'parent',
start: function () {
subject.select2("onSortStart");
},
update: function () {
subject.select2("onSortEnd");
}
});
// On form submit, transform value to match what is expected by server
subject.parents('form:first').submit(function (event) {
var values = subject.val().trim();
if (values !== '') {
var baseName = subject.attr('name');
values = values.split(',');
baseName = baseName.substring(0, baseName.length-1);
for (var i=0; i<values.length; i++) {
jQuery('<input>')
.attr('type', 'hidden')
.attr('name', baseName+i+']')
.val(values[i])
.appendTo(subject.parents('form:first'));
}
}
subject.remove();
});
},
setup_sticky_elements: function(subject) {
if (window.SONATA_CONFIG && window.SONATA_CONFIG.USE_STICKYFORMS) {
Admin.log('[core|setup_sticky_elements] setup sticky elements on', subject);
var topNavbar = jQuery(subject).find('.navbar-static-top');
var wrapper = jQuery(subject).find('.content-wrapper');
var navbar = jQuery(wrapper).find('nav.navbar');
var footer = jQuery(wrapper).find('.sonata-ba-form-actions');
if (navbar.length) {
new Waypoint.Sticky({
element: navbar[0],
offset: function() {
Admin.refreshNavbarStuckClass(topNavbar);
return jQuery(topNavbar).outerHeight();
},
handler: function( direction ) {
if (direction == 'up') {
jQuery(navbar).width('auto');
} else {
jQuery(navbar).width(jQuery(wrapper).outerWidth());
}
Admin.refreshNavbarStuckClass(topNavbar);
}
});
}
if (footer.length) {
new Waypoint({
element: wrapper[0],
offset: 'bottom-in-view',
handler: function(direction) {
var position = jQuery('.sonata-ba-form form > .row').outerHeight() + jQuery(footer).outerHeight() - 2;
if (position < jQuery(footer).offset().top) {
jQuery(footer).removeClass('stuck');
}
if (direction == 'up') {
jQuery(footer).addClass('stuck');
}
}
});
}
Admin.handleScroll(footer, navbar, wrapper);
}
},
handleScroll: function(footer, navbar, wrapper) {
if (footer.length && jQuery(window).scrollTop() + jQuery(window).height() != jQuery(document).height()) {
jQuery(footer).addClass('stuck');
}
jQuery(window).scroll(
Admin.debounce(function() {
if (footer.length && jQuery(window).scrollTop() + jQuery(window).height() == jQuery(document).height()) {
jQuery(footer).removeClass('stuck');
}
if (navbar.length && jQuery(window).scrollTop() === 0) {
jQuery(navbar).removeClass('stuck');
}
}, 250)
);
jQuery('body').on('expanded.pushMenu collapsed.pushMenu', function() {
setTimeout(function() {
Admin.handleResize(footer, navbar, wrapper);
}, 350); // the animation takes 0.3s to execute, so we have to take the width, just after the animation ended
});
jQuery(window).resize(
Admin.debounce(function() {
Admin.handleResize(footer, navbar, wrapper);
}, 250)
);
},
handleResize: function(footer, navbar, wrapper) {
if (navbar.length && jQuery(navbar).hasClass('stuck')) {
jQuery(navbar).width(jQuery(wrapper).outerWidth());
}
if (footer.length && jQuery(footer).hasClass('stuck')) {
jQuery(footer).width(jQuery(wrapper).outerWidth());
}
},
refreshNavbarStuckClass: function(topNavbar) {
var stuck = jQuery('#navbar-stuck');
if (!stuck.length) {
stuck = jQuery('<style id="navbar-stuck">')
.prop('type', 'text/css')
.appendTo('head')
;
}
stuck.html('body.fixed .content-header .navbar.stuck { top: ' + jQuery(topNavbar).outerHeight() + 'px; }');
},
// http://davidwalsh.name/javascript-debounce-function
debounce: function (func, wait, immediate) {
var timeout;
return function() {
var context = this,
args = arguments;
var later = function() {
timeout = null;
if (!immediate) {
func.apply(context, args);
}
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
}
};
},
setup_readmore_elements: function(subject) {
Admin.log('[core|setup_readmore_elements] setup readmore elements on', subject);
jQuery(subject).find('.sonata-readmore').each(function(i, ui){
jQuery(this).readmore({
collapsedHeight: parseInt(jQuery(this).data('readmore-height')),
moreLink: '<a href="#">'+jQuery(this).data('readmore-more')+'</a>',
lessLink: '<a href="#">'+jQuery(this).data('readmore-less')+'</a>'
});
});
},
handle_top_navbar_height: function() {
jQuery('.content-wrapper').css('padding-top', jQuery('.navbar-static-top').outerHeight());
}
};
jQuery(document).ready(function() {
Admin.handle_top_navbar_height();
});
jQuery(window).resize(function() {
Admin.handle_top_navbar_height();
});
jQuery(document).ready(function() {
jQuery('html').removeClass('no-js');
if (window.SONATA_CONFIG && window.SONATA_CONFIG.CONFIRM_EXIT) {
jQuery('.sonata-ba-form form').each(function () { jQuery(this).confirmExit(); });
}
Admin.setup_per_page_switcher(document);
Admin.setup_collection_buttons(document);
Admin.shared_setup(document);
});
jQuery(document).on('sonata-admin-append-form-element', function(e) {
Admin.setup_select2(e.target);
Admin.setup_icheck(e.target);
Admin.setup_collection_counter(e.target);
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

View File

@@ -0,0 +1,41 @@
table.sonata-ba-list th {
background-image: -moz-linear-gradient(-90deg, #f8f8f8 , #e2e2e2);
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#f8f8f8), to(#e2e2e2));
}
table.sonata-ba-list tfoot td {
background-image: -moz-linear-gradient(-90deg, #f8f8f8 , #e2e2e2);
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#f8f8f8), to(#e2e2e2));
border-top: 1px solid #DDD;
}
a.sonata-ba-collapsed {
color: #404040;
}
/* Form */
textarea.title {
font-size: 1em;
width: 500px;
}
input.title {
font-size: 1em;
width: 500px;
}
div.sonata-ba-field-error input{
border: 1px solid #f79992;
}
div.sonata-ba-field-error textarea{
border: 1px solid #f79992;
}
div.sonata-ba-field-error select{
border: 1px solid #f79992;
}
div.sonata-ba-field-error .field-short-description {
border: 1px solid #B94A48;
}

View File

@@ -0,0 +1,396 @@
/*body{*/
/*padding-top: 50px;*/
/*}*/
/*@media (max-width: 978px) {*/
/*body{*/
/*padding-top: 0;*/
/*}*/
/*}*/
div.border {
border: 1px solid #DDDDDD;
border-radius: 6px 6px 6px 6px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
}
div.connection {
position: absolute;
left: 50%;
top: 35%;
width: 460px;
margin: -130px 0 0 -250px;
box-shadow: 2px 2px 10px #ccc;
border: 1px solid #ddd;
border-radius: 6px;
}
div.connection .control-group {
padding: 0 20px 0 20px;
}
div.connection .alert {
margin: 0 20px 20px 20px;
}
div.connection form {
padding-top: 15px;
margin-bottom: 0;
}
div.connection form .form-actions {
margin-bottom: 0;
}
div.connection .form-actions {
padding-left: 20px;
}
div.connection div input.big {
height: 35px;
font-size: 25px;
}
.sonata-bc.sonata-ba-no-side-menu div.container-fluid > div.content {
margin-left: 0;
}
div.sonata-ba-field-inline-table input.title {
width: 100px;
}
div.sonata-ba-field-inline-table textarea.title {
width: 150px;
height: 50px;
}
h4.filter_legend table {
margin: 10px 0;
}
.table-striped tbody tr.sonata-ba-list-row-selected td, .table-striped tbody tr.sonata-ba-list-row-selected th {
background-color: #E3F7FE;
}
table.sonata-ba-list td img {
vertical-align: bottom
}
td.pager ul {
float: left;
list-style: none;
margin: 2px;
margin-left: auto;
margin-right: auto;
}
td.pager ul li {
float: left;
}
td.pager ul li a {
border: 1px solid #cccccc;
line-height: 25px;
padding: 1px 8px 1px 8px;
margin: 2px;
}
div.sonata-actions {
/*margin-top: 18px;*/
float: right
}
.sonata-ba-action.btn:not(:hover) {
background: none;
color: inherit;
}
.sonata-ba-list td.sonata-ba-list-field a.sonata-link-identifier {
font-weight: bold;
}
td.sonata-ba-list-field.sonata-ba-list-field-boolean i {
margin-right: 1ex;
}
td.sonata-ba-list-field.sonata-ba-list-field-boolean a:hover {
text-decoration: none;
}
td.sonata-ba-list-field.sonata-ba-list-field-currency,
td.sonata-ba-list-field.sonata-ba-list-field-percent,
td.sonata-ba-list-field.sonata-ba-list-field-integer {
text-align: right;
}
td.sonata-ba-list-field.sonata-ba-list-field-select {
text-align: center;
}
div.sonata-ba-modal-edit-one-to-one td.sonata-ba-list-field-batch,
div.sonata-ba-modal-edit-one-to-one div.sonata-ba-list-actions,
div.sonata-ba-modal-edit-one-to-one th.sonata-ba-list-field-header-batch {
display: none;
}
div.sonata-ba-modal-edit-one-to-one div.sonata-ba-list-actions {
display: none;
}
th.sonata-ba-list-field-header-order-desc a,
th.sonata-ba-list-field-header-order-asc a {
position: relative;
margin-right: 10px;
}
th.sonata-ba-list-field-header-order-asc a:hover:after,
th.sonata-ba-list-field-header-order-asc.sonata-ba-list-field-order-active a:after,
th.sonata-ba-list-field-header-order-desc.sonata-ba-list-field-order-active a:hover:after {
content: "";
display: block;
border-top: 4px solid black;
border-right: 4px solid transparent;
border-left: 4px solid transparent;
border-bottom: 4px solid transparent;
position: absolute;
top: 50%;
right: -10px;
margin-top: -1px;
}
th.sonata-ba-list-field-header-order-desc a:hover:after,
th.sonata-ba-list-field-header-order-desc.sonata-ba-list-field-order-active a:after,
th.sonata-ba-list-field-header-order-asc.sonata-ba-list-field-order-active a:hover:after {
content: "";
display: block;
border-bottom: 4px solid black;
border-right: 4px solid transparent;
border-left: 4px solid transparent;
border-top: 4px solid transparent;
position: absolute;
top: 50%;
right: -10px;
margin-top: -5px;
}
.sonata-ba-list-field-header-label-icon {
margin-right: 2px;
}
em.sonata-ba-field-help {
display: block;
color: #999;
margin-bottom: 10px;
}
fieldset legend {
padding-left: 0;
}
select.sonata-medium, textarea.sonata-medium, input.sonata-medium {
width: 400px;
}
textarea.sonata-medium {
height: 125px;
}
input[type="file"] {
height: 34px;
}
.sonata-ba-field-standard-natural .field-actions {
display: block;
margin-top: 5px;
}
.sonata-ba-field-inline-table select.sonata-medium,
.sonata-ba-field-inline-table textarea.sonata-medium,
.sonata-ba-field-inline-table input.sonata-medium {
width: 150px;
}
.sonata-ba-view-title {
font-size: 19px;
line-height: 1;
color: #404040;
*padding: 0 0 5px 0;
*line-height: 1.5;
}
.sonata-ba-view-title td, .sonata-ba-view-title th {
border: 0;
}
.sonata-ba-view-container th {
width: 130px;
}
.sonata-ba-view-container td, .sonata-ba-view-container th {
border-bottom: 0;
border-top: 1px solid #eee;
}
.sonata-ba-view-container:nth-child(2n) td, .sonata-ba-view-container:nth-child(2n) th {
background-color: #f9f9f9;
}
.sonata-ba-view-container:nth-child(2n):hover td, .sonata-ba-view-container:nth-child(2n):hover th {
background-color: #f5f5f5;
}
.sonata-ba-view-container.history-audit-compare th {
width: 10%;
}
.sonata-ba-view-container.history-audit-compare td {
width: 40%;
}
.sonata-ba-view-container.history-audit-compare th.diff {
background: pink;
}
.container-fluid > .sidebar {
top: auto;
}
.sonata-action-element.btn-group {
display: inline-block;
padding: 4px 10px 4px;
vertical-align: middle;
}
.sonata-collection-add, .sonata-collection-delete {
box-shadow: none;
}
.no-js .sonata-collection-add, .no-js .sonata-collection-delete {
display: none;
}
ul.inputs-list {
padding-left: 150px;
}
legend + .sonata-ba-collapsed-fields {
margin-top: 18px;
-webkit-margin-top-collapse: separate;
}
legend.sonata-ba-fieldset-collapsed-description + .sonata-ba-collapsed-fields {
margin-top: 0;
}
.sonata-ba-collapsed-fields > p {
margin-bottom: 18px;
}
.bordered-table tbody.ui-sortable tr {
cursor: move;
}
.sonata-ba-fieldset-collapsed legend:before {
content: '+ ';
}
.sonata-ba-collapsed-fields-close legend:before {
content: '- ';
padding-left: 5px;
}
.sonata-preview-form-container fieldset, .sonata-preview-form-container .tabbable {
display: none;
}
.pagination {
margin: 0;
}
.field-short-description {
min-width: 250px;
min-height: 18px;
display: block;
float: left;
background: #fefefe;
border: 1px solid #e9e9e9;
border-radius: 4px 4px 4px 4px;
list-style: none outside none;
margin: 0 15px 0 0;
padding: 4px 15px;
}
.inner-field-short-description {
}
.required:after {
content: '*';
}
.form-horizontal .control-group {
margin-bottom: 10px;
}
.noscript-warning {
background-color: #C70A0A;
color: #FFFFFF;
font-size: 14px;
font-weight: bold;
padding: 4px 0;
text-align: center;
width: 100%;
}
body.fixed .content-header .navbar.stuck {
position:fixed;
top:50px;
width: 100%;
margin-left: -15px;
z-index: 5;
border-radius: 0;
}
.sonata-search-result-list > li {
word-wrap: break-word;
}
.form-actions.stuck {
position:fixed;
bottom:0;
width: 100%;
margin-left: -15px;
margin-bottom: 0;
z-index: 5;
border-radius: 0;
}
@media(max-width:768px) {
body.fixed .main-header {
position: relative;
}
body.fixed .content-wrapper,
body.fixed .right-side {
padding-top: 0;
}
body.fixed .content-header .navbar.stuck {
top: 0;
position: relative;
margin: 0;
width: 100%;
}
body.fixed .main-sidebar {
position: absolute;
}
/* disable slimScroll */
body.fixed .main-sidebar .slimScrollDiv,
body.fixed .main-sidebar .sidebar {
overflow: visible !important;
height: auto !important;
}
.navbar-custom-menu > .navbar-nav > li >.dropdown-menu {
width: auto !important;
min-width: 230px; /* width of the left sidebar */
}
}

View File

@@ -0,0 +1,519 @@
/**
* SonataAdminBundle Theme based on SB Admin v2.0
* http://startbootstrap.com/templates/sb-admin-v2/
*/
html {
position: relative;
min-height: 100%;
}
footer {
position: absolute;
bottom: 0;
width: 100%;
height: 20px;
background-color: #333;
}
footer p {
margin: 0;
}
footer a {
color: #f6f6f6;
}
body > .header .logo {
font-family: 'Source Sans Pro', sans-serif;
}
.main-header {
height: 50px;
}
.logo img {
display: inline;
padding-bottom: 4px;
max-height: 100%;
max-width: 60px;
}
.logo span {
display: inline-block;
line-height: 1;
vertical-align: middle;
width: 200px;
}
.logo img + span { width: 140px; }
.open > .dropdown-menu {
animation-duration: .3s;
-webkit-animation-duration: .3s;
-moz-animation-duration: .3s;
}
/* Buttons */
.btn.btn-outline {
color: inherit;
background-color: transparent;
transition: all .5s;
}
.btn.btn-primary.btn-outline:hover,
.btn.btn-success.btn-outline:hover,
.btn.btn-info.btn-outline:hover,
.btn.btn-warning.btn-outline:hover,
.btn.btn-danger.btn-outline:hover {
color: #fff;
}
/* navigation */
.navbar-static-side ul li {
border-bottom: 1px solid #e7e7e7;
}
.navbar-brand {
padding-right: 20px;
}
.navbar-brand img {
height: 28px;
margin: 0;
padding: 0 5px 0 0;
vertical-align: middle;
}
.navbar-text .navbar-link {
padding: 0 10px;
}
.right-side > .content-header {
padding-bottom: 0;
}
.content-header .navbar {
margin-bottom: 0;
}
.content-header .navbar-nav.navbar-right:last-child {
margin-right: 0;
}
/* breadcrumb */
.sonata-bc .breadcrumb {
padding: 0;
margin: 0;
background: inherit;
float:left;
}
.sonata-bc .breadcrumb li a {
display: inline-block;
}
/* MEGA MENU STYLE
********************************/
.dropdown-menu.multi-column .dropdown-menu {
display: block !important;
position: static !important;
margin: 0 !important;
border: none !important;
box-shadow: none !important;
min-width:100px;
}
.dropdown-add .dropdown-menu > li > a {
white-space: normal;
overflow: hidden;
}
/* top right */
.navbar-static-top {
margin-bottom: 0;
}
.navbar-top-links > p,
.navbar-top-links > ul {
float: right;
}
.navbar-top-links li {
display: inline-block;
}
.navbar-top-links li a,
.navbar-top-links li span {
padding: 15px;
min-height: 50px;
}
.navbar-top-links li a:hover {
text-decoration: none;
}
.skin-black .navbar .breadcrumb > li > a:hover {
color: #444;
background: #f5f5f5;
}
.skin-black .navbar .dropdown-menu > li > a:hover {
background-color: #f5f5f5;
}
.navbar-top-links .dropdown-menu li {
display: block;
}
.navbar-top-links .dropdown-menu li:last-child {
margin-right: 0;
}
.navbar-top-links .dropdown-menu li a {
padding: 3px 20px;
min-height: 0;
}
.navbar-top-links .dropdown-menu li a div {
white-space: normal;
}
.navbar-top-links .dropdown-messages,
.navbar-top-links .dropdown-tasks,
.navbar-top-links .dropdown-alerts {
width: 310px;
min-width: 0;
}
.navbar-top-links .dropdown-messages {
margin-left: 5px;
}
.navbar-top-links .dropdown-tasks {
margin-left: -59px;
}
.navbar-top-links .dropdown-alerts {
margin-left: -123px;
}
.navbar-top-links .dropdown-user {
right: 0;
left: auto;
}
/* Content navbar */
body.fixed .content-header .navbar {
position: relative;
}
/* sidebar menu styles */
.sidebar-search {
padding: 15px;
}
.sidebar-menu li.keep-open > .treeview-menu {
display: block !important;
height: auto !important;
}
.arrow {
float: right;
}
.fa.arrow:before {
content: "\f104";
}
.active > a > .fa.arrow:before {
content: "\f107";
}
.nav-second-level li,
.nav-third-level li {
border-bottom: none !important;
}
.nav-second-level li a {
padding-left: 37px;
}
.nav-third-level li a {
padding-left: 52px;
}
@media(min-width:768px) {
.navbar-static-side {
z-index: 1;
position: absolute;
width: 250px;
}
.navbar-top-links .dropdown-messages,
.navbar-top-links .dropdown-tasks,
.navbar-top-links .dropdown-alerts {
margin-left: auto;
}
}
/* Admin table */
table.sonata-ba-list {
font-size: 14px;
}
table.sonata-ba-list img {
max-width: 100%;
height: auto;
}
table.sonata-ba-list td {
overflow: auto;
}
td.sonata-ba-list-label {
color: #565656;
font-weight: bold;
text-align: right;
vertical-align: middle !important;
}
/* side filter */
.box .box-header h4.box-title.filter_legend {
position: relative;
padding-left: 20px;
cursor: pointer;
}
h4.filter_legend:before {
content: "";
display: block;
width: 0;
height: 0;
border-top: 4px solid black;
border-right: 4px solid transparent;
border-left: 4px solid transparent;
position: absolute;
top: 50%;
left: 2px;
margin-top: -2px;
margin-left: 5px;
}
h4.filter_legend.active,
tr.filter.active * {
font-weight: bold;
color: #000;
}
form.sonata-filter-form.form-stacked {
padding-left: 0;
}
body.fixed .sonata-list-table {
position: relative;
margin-left: 15px;
margin-right: 15px;
margin-bottom: 5px;
}
.navbar-nav.navbar-right:last-child {
margin-right: 0;
}
/* Overrides */
/* x-editable */
td.sonata-ba-list-field .editable {
cursor: pointer;
}
td.sonata-ba-list-field .btn.editable {
border-bottom: solid 1px #BBB;
}
td.sonata-ba-list-field .editable-empty:not(.editable-open) {
display: none;
}
.sonata-ba-list tr:hover .editable-empty {
display: inline-block;
}
.editable-pre-wrapped {
white-space: normal;
}
.editable-container .prev:before {
content: "\2190 ";
}
.editable-container .next:before {
content: "\2192 ";
}
/* bootstrap */
.input-group-addon {
width: auto; /* See https://github.com/sonata-project/SonataAdminBundle/issues/2950 */
}
/**
* Make checkbox / radio label consistant with other labels
*/
.checkbox label,
.radio label {
font-weight: 700;
margin-left: -20px;
}
/**
* The iCheck checkboxes & radios have 0 margin by default,
* add some space for the label text.
*/
.checkbox div[class^="icheckbox"],
.checkbox-inline div[class^="icheckbox"],
.radio div[class^="iradio"],
.radio-inline div[class^="iradio"] {
position: relative;
margin-top: 4px \9;
margin-right: 5px;
margin-top: -3px;
}
.form-inline .checkbox div[class^="icheckbox"],
.form-inline .checkbox-inline div[class^="icheckbox"],
.form-inline .radio div[class^="iradio"],
.form-inline .radio-inline div[class^="iradio"] {
position: relative;
margin-top: 4px \9;
margin-left: 0;
margin-right: 5px;
margin-top: -3px;
}
/* Hide Delete checkbox on sonata_type_collection tables */
.sonata-ba-field-inline-table td > div.checkbox > label > .control-label__text {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
}
/* select2 */
.select2-choice, .select2-choices, .select2-drop {
border-radius: 0 !important;
}
/* Used for the mosaic view */
td > div.row {
padding: 0;
margin: 0;
}
div.mosaic-box {
padding: 2px;
border-radius: 3px;
}
div.mosaic-inner-box {
position: relative;
}
div.mosaic-inner-box-hover {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 4px 4px 0 0;
background-color: rgba(255, 255, 255, .8);
transition: .25s opacity;
padding: 5px 10px;
}
div.mosaic-inner-box:hover > div.mosaic-inner-box-hover {
opacity: 1;
}
div.mosaic-inner-box > div.mosaic-inner-box-hover {
opacity: 0;
}
div.mosaic-inner-box img {
width: 100%;
height: auto;
border-radius: 4px 4px 0 0;
}
div.mosaic-box-outter {
background-size: 100% auto;
border: 1px solid #ddd;
border-radius: 5px;
background-color: #fff;
}
div.mosaic-inner-box {
height: 100px;
border-bottom: none;
overflow: hidden;
}
div.mosaic-inner-text {
background: white;
border-top: none;
padding: 3px;
border-radius: 0 0 5px 5px;
z-index: 2;
position: relative;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mosaic-inner-link {
vertical-align: middle;
}
.mosaic-box-label {
position: absolute;
top: 10px;
right: 10px;
}
div.mosaic-box.sonata-ba-list-row-selected > div.mosaic-inner-box {
border: 1px solid #333;
border-bottom: none;
}
div.mosaic-box.sonata-ba-list-row-selected > div.mosaic-inner-text {
border: 1px solid #333;
border-top: none;
}
div.sonata-filters-box div.form-group div.form-group {
margin: 0;
}
div.sonata-filters-box div.form-group span.input-group-addon {
padding: 3px 10px;
font-size: 13px;
}
.sonata-search-result-hide {
display: none;
}
.sonata-search-result-fade {
opacity: 0.6;
}
.sonata-search-result-show {
display: block;
}

View File

@@ -0,0 +1,134 @@
/********************************************************************\
Page tree
\********************************************************************/
.sonata-tree {
list-style: none;
padding-left: 0;
margin-left: 15px;
margin-right: 15px;
overflow: hidden;
padding-bottom: 10px;
}
.sonata-tree ul {
list-style: none;
padding-left: 30px;
}
.sonata-tree__item {
display: block;
padding: 7px 15px 7px 7px;
border: 1px solid #ddd;
border-radius: 2px;
position: relative;
margin-bottom: 5px;
margin-right: 10px;
color: #444;
background: #fff;
}
.sonata-tree__item .label {
font-size: 12px;
margin-top: 2px;
border-radius: 2px;
}
.sonata-tree__item .label-warning {
margin-right: 5px;
}
.sonata-tree__item .fa-caret-right {
position: absolute;
top: 10px;
left: -20px;
color: #3c8dbc;
}
.sonata-tree__item:hover {
background: #eee;
color: #000;
}
.sonata-tree__item__is-hybrid {
margin-right: 5px;
}
.sonata-tree__item.is-active {
border: 1px solid #3c8dbc;
}
.sonata-tree__item.is-active:after,
.sonata-tree__item.is-active:before {
left: 100%;
top: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
.sonata-tree__item.is-active:after {
border-color: rgba(255, 255, 255, 0);
border-left-color: #fff;
border-width: 8px;
margin-top: -8px;
}
.sonata-tree__item.is-active:before {
border-color: rgba(255, 255, 255, 0);
border-left-color: #3c8dbc;
border-width: 9px;
margin-top: -9px;
}
.sonata-tree__item.is-active:hover:after {
border-left-color: #eee;
}
.sonata-tree__item.is-toggled .fa-caret-right {
-webkit-transform: rotate(90deg);
-moz-transform: rotate(90deg);
-o-transform: rotate(90deg);
-ms-transform: rotate(90deg);
transform: rotate(90deg);
}
.sonata-tree__item__edit {
font-weight: bold;
}
.sonata-tree__item__edit:hover {
text-decoration: underline;
}
/**
* Toggleable tree
*/
.sonata-tree--toggleable li > ul {
display: none;
}
.sonata-tree--toggleable .sonata-tree__item {
margin-left: 20px;
}
.sonata-tree--toggleable .sonata-tree__item .fa-caret-right {
cursor: pointer;
}
.sonata-tree--toggleable .sonata-tree__item:last-child .fa-caret-right {
display: none;
}
.sonata-tree--toggleable .sonata-tree__item .fa-caret-right:after {
content: '';
position: absolute;
top: -5px;
bottom: -5px;
left: -10px;
right: -10px;
}
/**
* Smaller tree
*/
.sonata-tree--small {
margin-left: 0;
}
.sonata-tree--small .sonata-tree__item__edit {
font-size: 12px;
}
.sonata-tree--small .sonata-tree__item {
padding: 3px 15px 4px 5px;
}
.sonata-tree--small .sonata-tree__item .fa-caret-right {
top: 7px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 781 B

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