Twig extension for sorting Doctrine ArrayCollection - symfony

I'm trying to write a Twig filter to be able to sort a Doctrine ArrayCollection, but the returned array is not sorted :( Can you please help me to fix this:
class SortExtension extends \Twig_Extension
{
public function getFilters()
{
return array(
new \Twig_SimpleFilter('sortby', array($this, 'sortByFilter')),
);
}
public function sortbyname( $a, $b )
{
if ($a->getName() === $b->getName()) {
return 0;
}
if ( $a->getName() < $b->getName() ) {
return 1;
}
return -1;
}
public function sortByFilter($collection)
{
$iterator = $collection->getIterator();
$iterator->uasort(array($this, 'sortbyname'));
return $collection;
}
I'm not quite sure if the returned collection in sortByFilter is changed.

This is because you are getting the iterator and sorting it.
The method getIterator creates a new ArrayIterator which makes a copy of the array.
Then, you are returning the collection, which is not sorted.
Here is a little sample of what happens.
You just have to replace
return $collection;
By
return $iterator;

Related

Symfony - ArrayCollection - Update or Create

Symfony 4.3
Goal : import a CSV listing employees/company with doctrine and save it into the DB.
I have the Entity company :
<?php
// src/Entity/Company.php
class Company
{
/**
* #ORM\OneToMany(targetEntity="App\Entity\Employee", mappedBy="company")
*/
private $employees;
public function __construct()
{
$this->employees = new ArrayCollection();
}
public function getEmployees()
{
return $this->employees;
}
and the Employee entity :
<?php
// src/Entity/Employee.php
class Employee
{
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Company", inversedBy="employees")
*/
private $company;
public function getCompany()
{
return $this->company;
}
public function setCompany(?Company $company)
{
$this->company = $company;
return $this;
}
Below is my loop for each line of the CSV being imported :
<?php
// $csv = array made from the CSV file
// e.g. $csv[0]['employee_name'] = "John Doe"
// e.g. $csv[0]['employee_mail'] = "john.doe#bmw.com"
// e.g. $csv[0]['employee_company_name'] = "BMW"
// e.g. $csv[0]['employee_company_id'] = 77
foreach($csv as $key => $value)
{
if($company = $this->em->getRepository(Company::class)->find($value['employee_company_id']))
{
// if the employee doest not exist, create it
// IN MY TESTS, HERE IS MY PROBLEM
// DON'T KNOW HOW TO LOOP INSIDE THE EMPLOYEES LIST USING the MAIL
if ($company->getEmployees()->contains($value['employee_mail']))
{
// This employee for this company exists, let's update it
}
else
{
// This employee for this company does not exist, let's create it
}
}
else
{
// Create the company
}
I don't know how to loop inside the company employees list, in order to decide if I have to edit (employee already exists) or create a new employee. Maybe I should not use the ArrayCollection::contains method ?
As Employees is an Doctrine ArrayCollection, you can use the exists method on it. This method accepts a closure as argument which loops over all elements in the collection and returns true when the condition matches.
if ($company->getEmployees()->exists(function ($key, Employee $employee) use ($value) {
return $employee->getEmail() === $value['employee_mail'];
})) {
// Employee exists, update
} else {
// Employee does not exist
}
Alternatively, if you want to create/update the record right away, you can do the following. This returns the Employee if it exists, or create a new Employee object if it doesn't
$employee = $company
->getEmployees()
->filter(function (Employee $employee) use ($value) {
return $employee->getEmail() === $value['employee_mail'];
})
->first() ?? new Employee();
the naive approach would be to just look in the employee repository.
$employee = $em->getRepository(Employee::class)->findOneBy([
'company' => $company,
'email' => $value['employee_mail'],
]);
if($employee) {
// exists -> update
} else {
// create
}
Depending on the company-employee-ratio, it might be better to cycle through the company's employees instead:
$employee = null;
foreach($company->getEmployees() as $_employee) {
if($_employee->getEmail() == $value['employee_mail']) {
$employee = $_employee;
break; // short circuit the loop
}
}
// rest is the same
If there are is very large number of employees in your csv and/or the database, it might be even better to skip the ORM alltogether and go straight to the database instead. Otherwise, you might want to clear the entity manager once in a while, if you have a huge database / csv.
You should store first your list of employees :
$employees = $company->getEmployees()
then you loop inside $employees :
foreach($employees as $employee ) //supposedly you have 'email' property
{
if ($employee->getEmail() == $value['employee_mail'])
{
//your code here
}
}
don't forget to add this :
* #ORM\OneToMany(targetEntity="App\Entity\Employee", mappedBy="company", fetch="EAGER")
Update :
$qb = $repository->createQueryBuilder('a')
->where(':employee_mail OF a.emplyee_email')
->setParameter('employee_mail ', $value['employee_mail']);
->andWhere(':comapanyId MEMBER OF a.company_id');
->setParameter('comapanyId ', $value['employee_company_id']);
->getQuery()
->execute()
this will return null if the employee does not exist in the company, you should implement this as you need in your code.

Symfony passing request to function to an other controller

I try to give '$_REQUEST['vider']' to an other controller like this :
return $this->forward('TestBundle:Rapport:bo', array('vider' => 'vider'));
//, array ($_REQUEST['vider'] => 'vider) doesn't work too
But in my function Rapport:bo, $_REQUEST['vider'] is null, i give it in the array, where i failed ?
edit :
my Rapport:bo function :
public function boAction(Request $request) {
var_dump($_REQUEST['vider']); // is null
if ( isset($_REQUEST['vider']) ) ) {
var_dump('test');
}
}
Try with:
public function boAction($vider) {
your logic...

Disable action for specific entries

In Sonata Admin, in one of my section, I would like to disable the delete action for 4 specific entries/lines.
I know I can do it for the entire listing with configureRoutes() but couldn't find anything for specific entries.
I tried the following but getSubject() is always empty.
protected function configureRoutes(RouteCollection $collection)
{
$product = $this->getSubject();
if ($product && $product->getIsBase())
{
$collection->clearExcept(array('list', 'edit'));
}
}
Thanks
You have to override the isGranted method in your admin class:
public function isGranted($name, $object = null)
{
if (in_array($name, array('LIST', 'EDIT')) && $object && $object->getIsBase()) {
return false;
}
return parent::isGranted($name, $object);
}

Link own queries symfony2 repository

I would like to factorize some code in my models repositories.
A really basic example
public function getPlayers()
{
$qb = $this->createQueryBuilder('p')
->innerJoin(...) // whatever the request
->where(...)
// I want to factorize this line because a lot of function use it
->andWhere('p.active = true');
return (...);
}
So I create a private function
private function getActivePlayer() {
return $this->andWhere('p.active = true');
}
And I would like to use it like that in any function
$qb = $this->createQueryBuilder('p')
->innerJoin(...)
->where(...)
->getActivePlayer()
But of course I have this error
Attempted to call method "getActivePlayer" on class "Doctrine\ORM\QueryBuilder"
It's possible to achieve this kind of factorization? What will be the syntaxe?
Thanks
You can try something like:
public function getPlayers()
{
$qb = $this->createQueryBuilder('p')
->innerJoin(...)
->where(...);
$qb = $this->getPlayerType($qb);
}
private function getActivePlayer(QueryBuilder $qb)
{
return $qb->andWhere('p.active = true');
}
Using the -> operator in the context of a QueryBuilder object will only call methods within the QueryBuilder class. You must define your own class that extends the QueryBuilder. Something like:
class MyQueryBuilder extends \Doctrine\ORM\QueryBuilder {
public function getActivePlayer() {
return $this->andWhere('p.active = true');
}
}
Then implement that builder instead of the default Query Builder:
$qb = new MyQueryBuilder();
$qb->select('p')
->from(...)
->innerJoin(...)
->where(...)
->getActivePlayer()
// ...
Note that the above code is just simple demonstration to show you what length of effort it takes to achieve exactly what you want to do - in actual practice, you'll need to do more than this.
You are best off starting with a basic QueryBuilder in a particular private method then amending it in your public getters:
private function getPlayerQueryBuilder()
{
$qb = $this->createQueryBuilder('p')
->innerJoin(...) // whatever the request
->where(...)
return $qb;
}
public function getActivePlayers() {
$result = $this->getPlayerQueryBuilder()
->andWhere('p.active = true')
->getQuery()->getResult();
return $result;
}
public function getAllPlayers() {
$result = $this->getPlayerQueryBuilder()
->getQuery()->getResult();
return $result;
}

Adding a button to the CMS in SilverStripe

How do I add a button to the backend of the CMS that fires an action? I can display the button where I want using:
public function getCMSFields()
{
$fields = parent::getCMSFields();
$fields->addFieldsToTab("Root.ButtonTest", array(
FormAction::create('doAction', 'Action button')
)
);
return $fields;
}
public function doAction()
{
//Do something
}
However the button added does nothing when clicked.
I've seen one example of how to put a button on the main action bar (next to save/publish) but that's not what I'm trying to do.
Looking at the only page of documentation I can find, do I need to do something within:
public function getCMSActions()
{
$actions = parent::getCMSActions();
//Something here?
}
It isn't very clear how to create the action that the button calls.
You'll have to extend/decorate LeftAndMain with your own extension and the action you want to call. Here's an example:
<?php
class MyExtension extends LeftAndMainExtension
{
private static $allowed_actions = array(
'doAction'
);
public function doAction($data, $form){
$className = $this->owner->stat('tree_class');
$SQL_id = Convert::raw2sql($data['ID']);
$record = DataObject::get_by_id($className, $SQL_id);
if(!$record || !$record->ID){
throw new SS_HTTPResponse_Exception(
"Bad record ID #" . (int)$data['ID'], 404);
}
// at this point you have a $record,
// which is your page you can work with!
// this generates a message that will show up in the CMS
$this->owner->response->addHeader(
'X-Status',
rawurlencode('Success message!')
);
return $this->owner->getResponseNegotiator()
->respond($this->owner->request);
}
}
Once you have written an extension like this, you'll have to apply it to LeftAndMain by adding the following to your mysite/_config/config.yml:
LeftAndMain:
extensions:
- MyExtension
That's it. Your doAction button should now actually do something!
Not sure if this is helpful, but here's how you can add action-buttons to a ModelAdmin.
(does reload the page)
...in the admin class:
public function getEditForm($id = null, $fields = null)
{
$form = parent::getEditForm($id, $fields);
$form
->Fields()
->fieldByName($this->sanitiseClassName($this->modelClass))
->getConfig()
->getComponentByType('GridFieldDetailForm')
->setItemRequestClass('MyGridFieldDetailForm_ItemRequest');
return $form;
}
MyGridFieldDetailForm_ItemRequest.php
class MyGridFieldDetailForm_ItemRequest extends GridFieldDetailForm_ItemRequest
{
function ItemEditForm()
{
$form = parent::ItemEditForm();
$formActions = $form->Actions();
$button = FormAction::create('myAction');
$button->setTitle('button label');
$button->addExtraClass('ss-ui-action-constructive');
$formActions->push($button);
$form->setActions($formActions);
return $form;
}
public function myAction(){ //do things }
}

Resources