I want to make a custom page twig in Sonata admin bundle (clone for example ) :
I use this tutorial :
http://symfony.com/doc/current/bundles/SonataAdminBundle/cookbook/recipe_custom_action.html
this is my controller CRUDController.php:
<?php
// src/AppBundle/Controller/CRUDController.php
namespace AppBundle\Controller;
use Sonata\AdminBundle\Controller\CRUDController as Controller;
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', $this->admin->getFilterParameters()));
}
}
but when i click in clone i show this error :
can you help me ..?
I feel like you forgot to configure your admin service for this page in the right way, please check http://symfony.com/doc/current/bundles/SonataAdminBundle/cookbook/recipe_custom_action.html#register-the-admin-as-a-service
cause sonata uses the SonataAdmin:CRUD controller by default and you should specify a custom one if you'd like to override the controller.
#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 #put it here
You forget to configure Route for your controller.
Sonata Admin has to know about your new Action in order to generate for it route. For this purposes you have to configure configureRoutes method in you admin class:
class CarAdmin extends AbstractAdmin // For Symfony version > 3.1
{
// ...
/**
* #param RouteCollection $collection
*/
protected function configureRoutes(RouteCollection $collection)
{
$collection->add('clone', $this->getRouterIdParameter().'/clone');
}
}
As you can see the name of the route matches the name of the action (but without action!) in your CRUDController.
You had name of the action : 'cloneAction' so the name of the route is "clone".
In my case i forgot to add baseControllerName in app/config/admin.yml, before it was '~'
Related
I am new to symfony. I want to be able to cofigure administrator role name to my application. I need to do something like: (in controller)
if($this->getUser()->isAdmin()) {
//..
}
In User Entity I could define isAdmin as:
function isAdmin()
{
$this->hasRole('ROLE_ADMIN');
}
but that way, ROLE_ADMIN can't be configured. Note that I don't want to pass 'a role name' as param (or default param) to isAdmin function. I want it like i can pass object to User Entity:
public function __construct(AuthConfiguration $config)
{
$this->config = $config;
}
public function isAdmin()
{
return $this->hasRole($this->config->getAdminRoleName());
}
But how can I pass object to user entity since user creation is handled by the repository ?
You can set up custom Doctrine DBAL ENUM Type for roles using this bundle: https://github.com/fre5h/DoctrineEnumBundle
<?php
namespace AppBundle\DBAL\Types;
use Fresh\Bundle\DoctrineEnumBundle\DBAL\Types\AbstractEnumType;
class RoleType extends AbstractEnumType
{
const ROLE_USER = 'ROLE_USER';
const ROLE_ADMIN = 'ROLE_ADMIN';
const ROLE_SUPER_ADMIN = 'ROLE_SUPER_ADMIN';
const ROLE_PROJECT_OWNER = 'ROLE_PROJECT_OWNER';
/**
* #var array Readable choices
* #static
*/
protected static $choices = [
self::ROLE_USER => 'role.user',
self::ROLE_ADMIN => 'role.administrator',
self::ROLE_SUPER_ADMIN => 'role.super_administrator',
self::ROLE_PROJECT_OWNER => 'role.project_owner',
];
}
Register new type in config.yml:
doctrine:
dbal:
mapping_types:
enum: string
types:
RoleType: AppBundle\DBAL\Types\RoleType
Configure your user's role field as ENUM RoleType type:
use Fresh\Bundle\DoctrineEnumBundle\Validator\Constraints as DoctrineAssert;
...
/**
* #DoctrineAssert\Enum(entity="AppBundle\DBAL\Types\RoleType")
* #ORM\Column(name="role", type="RoleType")
*/
protected $role = RoleType::ROLE_USER;
And use it in your entity or repository or anywhere else this way:
use AppBundle\DBAL\Types\RoleType;
...
public function isAdmin()
{
$this->hasRole(RoleType::ROLE_ADMIN);
}
The constructor is only called when you create a new instance of the object with the keyword new. Doctrine does not call the constructor even when it hydrates entities.
You could potentially create your own entity hydrator and call the entity's constructor however I haven't tried this solution. It may not be as maintainable.
I want to provide an alternative which I prefer (you may not).
On all my projects, the architecture is as follow:
Controller <-> Service <-> Repository <-> Entity.
The advantage of this architecture is the use of dependency injection with services.
In your services.yml
services:
my.user:
class: Acme\HelloBundle\Service\MyUserService
arguments:
# First argument
# You could also define another service that returns
# a list of roles.
0:
admin: ROLE_ADMIN
user: ROLE_USER
In your service:
namespace Acme\HelloBundle\Service;
use Symfony\Component\Security\Core\User\UserInterface;
class MyUserService {
protected $roles = array();
public function __constructor($roles)
{
$this->roles = $roles;
}
public function isAdmin(UserInterface $user = null)
{
if ($user === null) {
// return current logged-in user
}
return $user->hasRole($this->roles['admin']);
}
}
In your controller:
// Pass a user
$this->get('my.user')->isAdmin($this->getUser());
// Use current logged-in user
$this->get('my.user')->isAdmin();
It's away from the solution you are looking for but in my opinion it seems more inline with what Symfony2 provides.
Another advantage is that you can extend the definition of an admin.
For example in my project, my user service has a isAdmin() method that has extra logic.
I'm using latest (dev-master) sonata admin and I want to create my own createAction() method for sonata admin. I have to do that, because I want to save some user information, while adding to database.
My custom controller is - S\CoreBundle\Controller\NewsAdminConroller.php
<?php
namespace S\CoreBundle\Controller;
use Sonata\AdminBundle\Controller\CRUDController as Controller;
use Symfony\Component\Security\Core\SecurityContextInterface;
class NewsAdminController extends Controller
{
/**
* Set the system user ID
*/
private function updateFields($object)
{
//some code - this is my own method
}
public function createAction(Request $request = null)
{
//code for create ... it's almost the same as default code.
}
}
Default CRUD - Sonata\AdminBundle\Controller\CRUDController.php:
class CRUDController extends Controller
{
public function createAction(Request $request = null)
{
//...
}
}
Both createAction() methods have exactly the same arguments, name ...
And it throw's me an error:
PHP Strict Standards: Declaration of S\CoreBundle\Controller\NewsAdminController::createAction() should be compatible with Sonata\AdminBundle\Controller\CRUDController::createAction(Symfony\Component\HttpFoundation\Request $request = NULL) in /home/mark/dev/project/src/S/CoreBundle/Controller/NewsAdminController.php on line 129
The Sonata\AdminBundle\Controller\CRUDController::createAction(Symfony\Component\HttpFoundation\Request $request = NULL)
Needs a Request Object, but if you don't declare it, it point to S\CoreBundle\Controller\Request
Just add
"use Symfony\Component\HttpFoundation\Request;" in top of file.
Update
Since the commit https://github.com/sonata-project/SonataAdminBundle/commit/49557c302346f57d962b83b31e2931446ff60e9c, there is no need to set the request as parameter.
The create Action is only
Sonata\AdminBundle\Controller\CRUDController::createAction()
Background:
I am trying to conditionally load routes based on the request host. I have a database setup that has hosts in it that map to templates. If a user comes in from the host A and that uses template TA I want to load the routes for that template. If they come in from host B then load the routes for that template (TB).
The reason I have to do this is because each template will share many routes. There are however some unique routes for a given template.
It would be fine to restrict each template routes to a given host, except that there are literally 1000's of hosts.
What I Have Tried:
I have tried a custom route loader as described in the documentation here:
http://symfony.com/doc/current/cookbook/routing/custom_route_loader.html
However when i configure the service and try and inject the "#request" the constructor fails because $request is null
services:
acme_demo.routing_loader:
class: Acme\DemoBundle\Routing\ExtraLoader
arguments: ["#request"]
tags:
- { name: routing.loader }
Class:
<?php
namespace: Acme\DemoBundle\Routing;
use Symfony\Component\HttpFoundation\Request;
class ExtraLoader
{
protected $request;
public function __construct(Request $request)
{
$this->request = $request;
}
// ...
}
This also doesnt work if I try and switch "#request" for "#service_container" then call
$this->container->get('request');
The closest I got to getting this working was following a guide found here:
http://marcjschmidt.de/blog/2013/11/30/symfony-custom-dynamic-router.html
The problem i have with this on is im trying to use annotation on a controller and i cant seem to get the Symfony\Component\Routing\Loader\AnnotationFileLoader working.
Ok I have finally figured out a working solution, Its a mix of several of the above mentioned guides.
Im using a kernel request listener:
services:
website_listener:
class: NameSpace\Bundle\EventListener\WebsiteListener
arguments:
- "#website_service"
- "#template_service"
- "#sensio_framework_extra.routing.loader.annot_dir"
- "#router"
- "%admin_domain%"
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 33 }
The Listener:
<?php
namespace NameSpace\WebsiteBundle\EventListener;
use NameSpace\TemplateBundle\Service\TemplateService;
use NameSpace\WebsiteBundle\Service\WebsiteService;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Loader\AnnotationDirectoryLoader;
use Symfony\Component\HttpKernel\EventListener\RouterListener;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\PropertyAccess\PropertyAccess;
class WebsiteListener
{
/**
* #var WebsiteService
*/
protected $websiteService;
/**
* #var TemplateService
*/
protected $templateService;
/**
* #var AnnotationDirectoryLoader
*/
protected $loader;
/**
* #var Router
*/
protected $router;
/**
* #var string
*/
protected $adminDomain;
public function __construct(WebsiteService $websiteService, TemplateService $templateService, AnnotationDirectoryLoader $loader, Router $router, $adminDomain)
{
$this->websiteService = $websiteService;
$this->templateService = $templateService;
$this->loader = $loader;
$this->router = $router;
$this->adminDomain = $adminDomain;
}
public function loadRoutes()
{
$template = $this->templateService->getTemplateByAlias($this->websiteService->getWebsite()->getTemplate());
$routes = $this->loader->load($template['routes'],'annotation');
$allRoutes = $this->router->getRouteCollection();
$allRoutes->addCollection($routes);
}
public function onKernelRequest(GetResponseEvent $event)
{
try {
$this->websiteService->handleRequest($event->getRequest());
$this->loadRoutes();
} catch(NotFoundHttpException $e) {
if($event->getRequest()->getHost() !== $this->adminDomain){
throw $e;
}
}
}
}
The Key parts of this are:
The Loader - I found "#sensio_framework_extra.routing.loader.annot_dir" in the source code. That the annotation directory loader that symfony uses by default so thats the one that I want to use too. But if you want to use a different loader there are others available.
The Router - This is what i use to get all of the current routes. NOTE that the $allRoutes->addCollection($routes) call is on a seperate line. Im not sure why it makes a difference but calling it all in 1 like was not working.
$template['routes'] is just a namespaces controller reference like you would use to add routing in your routing.yml. Something like: "#NamespaceBundle/Controller"
Like the title says, I would like to use the same controller, but different views, based on the HTTP host name. Is this possible? What would be the best architecture to accomplish it?
If the controller returns null then the Symfony 2 request handler will dispatch a KernelEvents::VIEW event.
You can make yourself a view listener (http://symfony.com/doc/current/cookbook/service_container/event_listener.html) to catch the event. Your view listener would then need the logic to determine which view to create based on request parameters such as the host name. The view would then create the response object. The listener then sets the response in the event.
Is this the "best" approach. Hard to say. There is no reason why the controller itself could not create the view. On the other hand, with a view listener you can share views with multiple controllers. Really depends on your application.
Here is an example of a view listener which kicks off different views depending on the _format attribute.
namespace Cerad\Bundle\CoreBundle\EventListener;
use Symfony\Component\DependencyInjection\ContainerAware;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class ViewEventListener extends ContainerAware implements EventSubscriberInterface
{
const ViewEventListenerPriority = -1900;
public static function getSubscribedEvents()
{
return array(
KernelEvents::VIEW => array(
array('onView', self::ViewEventListenerPriority),
),
);
}
/* =================================================================
* Creates and renders a view
*/
public function onView(GetResponseForControllerResultEvent $event)
{
$request = $event->getRequest();
if ($request->attributes->has('_format'))
{
$viewAttrName = '_view_' . $request->attributes->get('_format');
}
else $viewAttrName = '_view';
if (!$request->attributes->has($viewAttrName)) return;
$viewServiceId = $request->attributes->get($viewAttrName);
$view = $this->container->get($viewServiceId);
$response = $view->renderResponse($request);
$event->setResponse($response);
}
# services.yml
cerad_core__view_event_listener:
class: '%cerad_core__view_event_listener__class%'
calls:
- [setContainer, ['#service_container']]
tags:
- { name: kernel.event_subscriber }
# routing.yml
cerad_game__project__schedule_team__show:
path: /project/{_project}/schedule-team.{_format}
defaults:
_controller: cerad_game__project__schedule_team__show_controller:action
_model: cerad_game__project__schedule_team__show_model_factory
_form: cerad_game__project__schedule_team__show_form_factory
_template: '#CeradGame\Project\Schedule\Team\Show\Twig\ScheduleTeamShowPage.html.twig'
_format: html
_view_csv: cerad_game__project__schedule_team__show_view_csv
_view_xls: cerad_game__project__schedule_team__show_view_xls
_view_html: cerad_game__project__schedule_team__show_view_html
requirements:
_format: html|csv|xls|pdf
I am using FOS bundle in my project. I created a new controller ChangePasswordController in my admin bundle. Here is my code.
namespace Pondip\AdminBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\RedirectResponse;
use FOS\UserBundle\Controller\ChangePasswordController as BaseController;
/**
Controller managing the change password *
#author Aman
/
class ChangePasswordController extends BaseController
{
/**
This action is used for change password for admin.
#author Aman
#return render view
#throws AccessDeniedException as exception
#
*/
public function changePasswordAction()
{
$user = $this->container->get('security.context')->getToken()->getUser();
$form = $this->container->get('fos_user.change_password.form');
$formHandler = $this->container->get('fos_user.change_password.form.handler');
$process = $formHandler->process($user);
if ($process) {
$this->setFlash('fos_user_success', 'Password has been changed successfully');
$url = $this->container->get('router')->generate('pondip_admin_changepassword');
return new RedirectResponse($url);
}
return $this->container->get('templating')->renderResponse(
'PondipAdminBundle:ChangePassword:changePassword.html.'.$this->container->getParameter('fos_user.template.engine'),
array('form' => $form->createView())
);
}
}
Now I want to customize error message for current password.
Are you overriding the FOSUserBundle? See Documentation. If you're not, then you should be, if you want to modify default behaviors.
If so, you then have to copy, in your bundle that is overriding the FOSUserBundle, the translation file (and parent directories) found in the FOSUserBundle vendor :
Resources/translations/validators.xx.yml where xx is your locale.
Then change the error messages. For example, to change the current password invalid error, write :
fos_user:
[...]
current_password:
invalid: Your own error message here
[...]
Hope this helps!