Whats the best way to override object deletion in Sonata Admin? - symfony

I already have a custom CRUD controller. So do I just need to override Controller::deleteAction() and Controller::batchDeleteAction() from Sonata\AdminBundle\Controller ?
Or is it preferable / better practice to override the Admin class's delete methods?
My desired behaviour is that I want to update a record with an archived flag rather than delete the entity.
The docs are incomplete on this subject
Update
The following code in my Entity's Repository class iterates over a query object as per the batchDelete method in the ModelManager
public function batchArchive($class, ProxyQuery $queryProxy)
{
$queryProxy->select('DISTINCT '.$queryProxy->getRootAlias());
try {
$entityManager = $this->getEntityManager();
$batchSize = 20;
$i = 0;
foreach ($queryProxy->getQuery()->iterate() as $pos => $object) {
$this->archiveMyEntity($object); //???
if (($i % $batchSize) == 0) {
$entityManager->flush();
$entityManager->clear();
}
++$i;
}
} catch (\PDOException $e) {
throw new ModelManagerException('', 0, $e);
} catch (DBALException $e) {
throw new ModelManagerException('', 0, $e);
}
}
The problem I have is that the object my archiveMyEntity() method expects is an Entity object not a query object.

I overwrote the delete logic in the admin class than in my custom CRUD controller I overwrote the batchActionDelete logic with the following:
public function batchActionDelete(\Sonata\AdminBundle\Datagrid\ProxyQueryInterface $query)
{
if (false === $this->admin->isGranted('DELETE')) {
throw new AccessDeniedException();
}
$res = $query->execute();
if (count($res)) {
foreach ($res as $sqTeamEntity) {
$this->admin->delete($sqTeamEntity, false);
}
$this->admin->flushDoctrine(); //custom method in the admin class
$this->addFlash('sonata_flash_success', 'flash_batch_delete_success');
}
return new RedirectResponse(
$this->admin->generateUrl('list',
$this->admin->getFilterParameters())
);
}
So I fetch all the entities and just call the delete method from the admin class.
Hope this helps.

If you wish to override the controller logic or view, overriding the methods you indicated is the way to go.
However, if your objective is to perform custom logic before or after the deletion, you may override the Admin::preRemove($object) or Admin::postRemove($object) methods.
You may as well override the whole logic by overriding the Admin::delete($object) method.
Feel free to submit a PR - or comment in the related Github issue - to improve the docs as well.

Related

Symfony DependencyInjection - how to get all services of certain type?

Using CompilerPass, I need to add setter to all services that inherit specific abstract class. They're already tagged, but ONLY SOME OF THEM use abstract class.
Something like that:
$abstractServices = $containerBuilder->findServicesByType('MyAbstractClass');
$abstractServices->addMethodCall(
'setHelperService',
[new Reference('#service_to_be_set')
);
What would you suggest?
Based on Thomas's answer
$taggedServices = $container->findTaggedServiceIds('your_tag');
foreach ($taggedServices as $id => $tags) {
$service = $container->findDefinition($id);
if (!is_callable($service, 'yourMethodName') {
continue;
// or raise exception if you need
}
$service->addMethodCall(...); //whatever
}
Another way would be
$taggedServices = $container->findTaggedServiceIds('your_tag');
foreach ($taggedServices as $id => $tags) {
$service = $container->findDefinition($id);
if (!$service instance YourInterface) {
continue;
// or raise exception if you need
}
$service->addMethodCall(...); //whatever
}
Of course event that is based on tags and works only with instances or abstract classes (if method you're searching for is abstract in the latter case)
You can simply check if the service class is subclass of the specified abstract class.
foreach ($container->getDefinitions() as $definition) {
if (is_subclass_of($definition->getClass(), 'YourAbstractClass')) {
// do something
}
}

Does a priority concepts exists on voters?

I need to develop on a web portal a voter System with Silex framework (based on Symfony components).
These various voters will check if the current user is in the good country, if he subscibes to which program, if he activate the advertising on the site, ... I use them with the Unanime rule.
But I also would like to use the role system, and I need that this role voter has hight priority over the rest.
That is to say if the role voter abstain, then others voters can decide with a consensus decision, in any other case it's the role consensus I want to get.
Does Symfony provides tool to do it? I already simulate with matrix these case with Affirmative and Unanime decision managment, but I didn't found how to make the Role Voter more important than other.
You can set priority for your voter:
your_voter:
class: # ...
public: false
arguments:
# ...
tags:
- { name: security.voter , priority: 255 }
Actually you have to write your own AccessDecisionManager to perform it:
In my case I need that the RoleHierarchyVoter overwrite other votes, excepts if it abstains. If it abstains I use an unanimous strategy:
class AccessDecisionManager implements AccessDecisionManagerInterface {
private $voters;
public function __construct(array $voters) {
$this->voters = $voters;
}
public function decide(TokenInterface $token, array $attributes, $object = null) {
$deny = 0;
foreach ($this->voters as $voter) {
$result = $voter->vote($token, $object, $attributes);
if ($voter instanceof RoleHierarchyVoter) {
if ($result === VoterInterface::ACCESS_GRANTED)
return true;
elseif ($result === VoterInterface::ACCESS_DENIED)
return false;
else
continue;
}else {
if ($result === VoterInterface::ACCESS_DENIED)
$deny++;
}
}
if ($deny > 0)
return false;
else
return true;
}
}
To register my custom AccessDecisionManager:
$app['security.access_manager'] = $app->share(function (Application $app) {
return new AccessDecisionManager($app['security.voters']);
});

Laravel 4 Model Events don't work with PHPUnit

I build a model side validation in Laravel 4 with the creating Model Event :
class User extends Eloquent {
public function isValid()
{
return Validator::make($this->toArray(), array('name' => 'required'))->passes();
}
public static function boot()
{
parent::boot();
static::creating(function($user)
{
echo "Hello";
if (!$user->isValid()) return false;
});
}
}
It works well but I have issues with PHPUnit. The two following tests are exactly the same but juste the first one pass :
class UserTest extends TestCase {
public function testSaveUserWithoutName()
{
$count = User::all()->count();
$user = new User;
$saving = $user->save();
assertFalse($saving); // pass
assertEquals($count, User::all()->count()); // pass
}
public function testSaveUserWithoutNameBis()
{
$count = User::all()->count();
$user = new User;
$saving = $user->save();
assertFalse($saving); // fail
assertEquals($count, User::all()->count()); // fail, the user is created
}
}
If I try to create a user twice in the same test, it works, but it's like if the binding event is present only in the first test of my test class. The echo "Hello"; is printed only one time, during the first test execution.
I simplify the case for my question but you can see the problem : I can't test several validation rules in different unit tests. I try almost everything since hours but I'm near to jump out the windows now ! Any idea ?
The issue is well documented in Github. See comments above that explains it further.
I've modified one of the 'solutions' in Github to automatically reset all model events during the tests. Add the following to your TestCase.php file.
app/tests/TestCase.php
public function setUp()
{
parent::setUp();
$this->resetEvents();
}
private function resetEvents()
{
// Get all models in the Model directory
$pathToModels = '/app/models'; // <- Change this to your model directory
$files = File::files($pathToModels);
// Remove the directory name and the .php from the filename
$files = str_replace($pathToModels.'/', '', $files);
$files = str_replace('.php', '', $files);
// Remove "BaseModel" as we dont want to boot that moodel
if(($key = array_search('BaseModel', $files)) !== false) {
unset($files[$key]);
}
// Reset each model event listeners.
foreach ($files as $model) {
// Flush any existing listeners.
call_user_func(array($model, 'flushEventListeners'));
// Reregister them.
call_user_func(array($model, 'boot'));
}
}
I have my models in subdirectories so I edited #TheShiftExchange code a bit
//Get all models in the Model directory
$pathToModels = '/path/to/app/models';
$files = File::allFiles($pathToModels);
foreach ($files as $file) {
$fileName = $file->getFileName();
if (!ends_with($fileName, 'Search.php') && !starts_with($fileName, 'Base')) {
$model = str_replace('.php', '', $fileName);
// Flush any existing listeners.
call_user_func(array($model, 'flushEventListeners'));
// Re-register them.
call_user_func(array($model, 'boot'));
}
}

Symfony2 set class variable with init or construct methods

Have recently been using Symfony2 after using ZF for some time.
I am having problems trying to do something relatively simple, I think.
The following code is within a controller:
private $current_setid = "";
public function __construct() {
$current_set = $this->getCurrentSet();
if ($current_set == "") {
return $this->redirect($this->generateUrl('selectset'));
}
$this->current_setid = $current_set;
}
public function getCurrentSet() {
$session = $this->get("session");
$set = $session->get('set');
return $set;
}
public function setCurrentSet($setid) {
$session = $this->get("session");
$session->set('set', "$setid");
}
If I use __construct() I get errors like:
Fatal error: Call to a member function get() on a non-object in
I have tried using __init() and init() both of which do not seem to get called.
Can anyone point me in the right direction? Is there a simple way to do this or do I have to look into event listeners?
Have you tried getting your session like they do in official documentation?
$session = $this->getRequest()->getSession();
$foo = $session->get('foo');
Basically get fetch dependencies from container and container in the Controller is injected using setter dependency injection. You just not have container in the time of __construct yet.
Just ended up opting for placing a check in every method in the class. Seems silly to have to do that but I find I often have to do that in Symfony2 with the lack of init, postDispatch type methods like ZF has.
Even trying to remove the check to another method was counter productive as I still had to check the return from that method as $this->redirect does not seem to work unless it is within an Action method. For example:
public function isSetSet() {
$current_set = $this->getCurrentSet();
if ($current_set == "") {
$url = $this->generateUrl('selectset');
return $this->redirect($url);
}
return TRUE;
}
public function someAction() {
$check = $this->isSetSet();
if($check != TRUE){
return $check;
}
...
}
So each method needs that 4 line check but the whole check can be done in 4 lines anyway so no need for that extra method:
public function anotherAction() {
$current_setid = $this->getCurrentSet();
if ($current_setid == "") {
return $this->redirect($this->generateUrl('selectset'));
}
...
}

Symfony2 rejecting my custom findBy function in my model class

I followed the example of setting up a custom findOneByJoinedToCategory($id) function in the Doctrine model class as explained in the documentation here:
http://symfony.com/doc/current/book/doctrine.html
In my case, I have a model class called TestVendorCategory containing a bunch of attributes and this function:
public function findOneByNameJoinedToVendorCategoryMappings($vendorCategoryName)
{
$query = $this->getEntityManager()
->createQuery('
SELECT vc, vcm FROM TestCoreBundle:VendorCategory vc
JOIN vcm.vendorCategoryMapping vcm
WHERE vc.name = :name'
)->setParameter('name', $vendorCategoryName);
try
{
return $query->getSingleResult();
}
catch (\Doctrine\ORM\NoResultException $e)
{
return null;
}
}
In my controller, I call it like this:
$vendorCategoryMapping = $this->em->getRepository("TestCoreBundle:VendorCategory")->findOneByNameJoinedToVendorCategoryMappings($vendorCategoryName);
When I go to the browser and execute this action with this call in it, I get the following error message:
Entity 'Test\CoreBundle\Entity\VendorCategory' has no field 'nameJoinedToVendorCategoryMappings'. You can therefore not call 'findOneByNameJoinedToVendorCategoryMappings' on the entities' repository
It looks like Symfony 2.1 wants the findOneBy...() methods to reflect names of existing fields only, no custom "JoinedTo..." kinds of methods. Am I missing something, please? The documentation shows an example like this where it supposedly works. I am using annotations, but this method doesn't have any. Thank you!
You have to put the findOneByNameJoinedToVendorCategoryMappings function in the VendorCategoryRepository class:
<?php
namespace Test\CoreBundle\Entity;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\NoResultException;
class VendorCategoryRepository extends EntityRepository
{
public function findOneByNameJoinedToVendorCategoryMappings($vendorCategoryName)
{
$query = $this->getEntityManager()
->createQuery('
SELECT vc, vcm FROM TestCoreBundle:VendorCategory vc
JOIN vcm.vendorCategoryMapping vcm
WHERE vc.name = :name'
)->setParameter('name', $vendorCategoryName);
try
{
return $query->getSingleResult();
}
catch (NoResultException $e)
{
return null;
}
}
}
and link this repository class in the Entity:
/**
* #ORM\Entity(repositoryClass="Test\CoreBundle\Entity\VendorCategoryRepository")
*/
class VendorCategory

Resources