Symfony 3 : how to reordering an array collection with route action? - symfony

I have a Product Entity. Each product can have 0 or N pictures.
I have a ProductPhoto Entity which has a order property (0 = first).
On a page, I list all the pictures of my current product. I would like to manage pictures order with 2 up/down arrows.
When the user clicks on an arrow, it moves up/down the picture compared to the others.
So, on each arrow, there is a link that corresponds to a route action in my ProductController.
It's not very complicated to update only the order of the picture that moved, but I don't know how to update the order of other pictures in the ArrayCollection...
/**
* Manage picture order
*
* #Route("/products/{id}/photos/{idphoto}/move-{direction}", name="prod_move_photo")
*/
public function movePhotoAction($id, $idphoto, $direction, Request $request) {
$em = $this->getDoctrine()->getManager();
$photo = $em->getRepository('AppBundle:ProductPhoto')->find($idphoto);
if ($direction == 'up') {
$order = $photo->getOrder() - 1;
if ($order >= 0)
$photo->setOrder($order);
else
$photo->setOrder(0);
} elseif ($direction == 'down') {
$order = $photo->getOrder() + 1;
$photo->setOrder($order);
} else {
throw $this->createNotFoundException("The type of ordering '" . $direction . "' doesn't exist.");
}
$em->flush();
// redirection
return $this->redirectToRoute('prod_photos', array('id' => $id));
}
Maybe using PHP uksort() ?

It looks like you want to update order fields in two ProductPhotos moved by each other, right? So please try this:
/**
* Manage picture order
*
* #Route("/products/{id}/photos/{idphoto}/move-{direction}", name="prod_move_photo")
*/
public function movePhotoAction($id, $idphoto, $direction, Request $request) {
$em = $this->getDoctrine()->getManager();
$photo = $em->getRepository('AppBundle:ProductPhoto')->find($idphoto);
$order = $photo->getOrder();
switch($direction) {
case 'up':
$newOrder = ($order >= 1) ? ($order - 1) : (int) 0;
break;
case 'down':
$newOrder = $order + 1;
break;
default:
throw $this->createNotFoundException("The type of ordering '" . $direction . "' doesn't exist.");
}
$anotherPhoto = $em->getRepository('AppBundle:ProductPhoto')->findOneByOrder($newOrder);
$photo->setOrder($newOrder);
$anotherPhoto->setOrder($order);
$em->flush();
// redirection
return $this->redirectToRoute('prod_photos', array('id' => $id));
}

The solution of #Snegirekk works very well (and I use it), but here is the solution I found, if it can help...
/**
* Manage picture order
*
* #Route("/products/{id}/photos/{idphoto}/move-{direction}", name="prod_move_photo")
*/
public function movePhotoAction($id, $idphoto, $direction, Request $request) {
$em = $this->getDoctrine()->getManager();
$photo = $em->getRepository('AppBundle:ProductPhoto')->find($idphoto);
// Current order of the photo
$currentPos = $photo->getOrder();
// Determine new order
if ($direction == 'up') {
$newPos = ($currentPos > 0) ? $currentPos - 1 : 0;
} elseif ($direction == 'down') {
$newPos = $currentPos + 1;
} else {
throw $this->createNotFoundException("The type of ordering '" . $direction . "' doesn't exist.");
}
$product = $em->getRepository('AppBundle:Product')->find($id);
// Get product photos with actual order (moveElement() needs an array)
$photos = $product->getPhotos()->toArray();
// Reorder photos in ArrayCollection
$this->moveElement($photos, $currentPos, $newPos);
// Reorder photos in database (with keys of formatted ArrayCollection)
foreach ($photos as $order => $p) {
$p->setOrder($order);
}
$em->flush();
// redirection
return $this->redirectToRoute('prod_photos', array('id' => $id));
}
/**
* Move an array element to a new index
*
* #param array $array Array of elements to sort
* #param integer $currentPos Current position of the element to move
* #param integer $newPos New position of the element to move
* #return void
*/
public function moveElement(&$array, $currentPos, $newPos) {
$out = array_splice($array, $currentPos, 1);
array_splice($array, $newPos, 0, $out);
}

Related

Drupal 8 - Get All Nodes With a Part of Url Alias

I try to find a way to get all nodes by a part of the url alias in the nodes. I know i get a specific node by the complete url alias. For example:
$path = \Drupal::service('path.alias_manager')->getPathByAlias('/this-is-the-alias');
if(preg_match('/node\/(\d+)/', $path, $matches)) {
$node = \Drupal\node\Entity\Node::load($matches[1]);
}
And I know I can find multiple nodes by entity query:
$nids = \Drupal::entityQuery('node')
->condition('type','page')
->condition('status',1)
->sort('created', 'DESC')
->range(0, 20)
->execute();
But I want to know how I can implement an additional condition to filter nodes by a part of the URL alias for example "/news/*". I need all nodes with this url fragment.
Can try this-
Reference https://www.jonkamke.com/blog/2019/5/load-a-node-via-url-alias
<?php
/** #var Drupal\Core\Url $url */
$url = $variables['url'];
if ($url instanceof Drupal\Core\Url) {
$nid = $url->getRouteParameters()['node'];
/** #var \Drupal\node\Entity\Node $node */
$node = Node::load($nid);
if ($node instanceof Node) {
$type = $node->getType();
}
}
$input = '/this-is-the-alias';
$node_query_result = [];
$path_query = \Drupal::database()->select('path_alias', 'a');
$path_query->addField('a', 'path');
$path_query->condition('a.alias', '%' . $input . '%', 'LIKE');
$path_query_result = str_replace('/node/', '', $path_query->execute()->fetchCol());
if ($path_query_result) {
$node_query = \Drupal::database()->select('node_field_data', 'n');
$node_query->addField('n', 'nid');
$node_query->addField('n', 'type');
$node_query->addField('n', 'title');
$node_query->condition('n.status', NodeInterface::PUBLISHED);
$node_query->condition('nid', $path_query_result, 'IN');
$node_query_result = $node_query->execute()->fetchAll(\PDO::FETCH_ASSOC);
}
return $node_query_result;

Doctrine DQL query produces Error: Expected Literal, got end of string

I am currently trying to build a blog website following a course that uses
Symfony 2.5.2. (PHP -v 7.0)
To retrieve a post I am using a following Controller
/**
* #Route(
* "/{slug}",
* name = "blog_post"
* )
* #Template()
*/
public function postAction($slug)
{
$PostRepo = $this->getDoctrine()->getRepository('AniaBlogBundle:Post');
$Post = $PostRepo->getPublishedPost($slug);
if(null === $Post){
throw $this->createNotFoundException('Post not found');
}
return array(
'post'=> $Post
);
}
and here is my getPublishedPost function :
public function getPublishedPost($slug){
$qb = $this->getQueryBuilder(array(
'status' => 'published'
));
$qb->andWhere('p.slug = :slug')
->setParameter('slug', $slug);
return $qb->getQuery()->getOneOrNullResult();
}
and getQueryBuilder function :
public function getQueryBuilder(array $params = array()){
$qb = $this->createQueryBuilder('p')
->select('p, c, t')
->leftJoin('p.category', 'c')
->leftJoin('p.tags', 't');
if(!empty($params['status'])){
if('published' == $params['status']){
$qb->where('p.publishedDate <= :currDate AND p.publishedDate IS NOT NULL')
->setParameter('currDate', new \DateTime());
}else if('unpublished' == $params['status']) {
$qb->where('p.publishedDate > :currDate OR p.publishedDate IS NULL')
->setParameter('currDate', new \DateTime());
}
}
if(!empty($params['orderBy'])){
$orderDir = !empty($params['orderDir']) ? $params['orderDir'] : NULL;
$qb->orderBy($params['orderBy'], $orderDir);
}
if(!empty($params['categorySlug'])){
$qb->andWhere('c.slug = :categorySlug')
->setParameter('categorySlug', $params['categorySlug']);
}
if(!empty($params['tagSlug'])){
$qb->andWhere('t.slug = :tagSlug')
->setParameter('tagSlug', $params['tagSlug']);
}
if(!empty($params['search'])) {
$searchParam = '%'.$params['search'].'%';
$qb->andWhere('p.title LIKE :searchParam OR p.content LIKE :searchParam')
->setParameter('searchParam', $searchParam);
}
return $qb;
}
}
However i get the 500 error saying : [Syntax Error] line 0, col -1: Error: Expected Literal, got end of string.
Thank you in advance for any suggestions!

Slim log writer with sqlite

I have configured slim to write logs to log files as the standard way. But this is not effective when we want to search large and all the logs at a given time. So I want to write those logs to a separate sqlite DB.
My question is how can I set the log writer to write the messages (as done in the Zend framework) ?
P S: I know that I can create a PDO object and use the queries. But I don't want to change the existing code. Just prefer to set the writer and let the framework do the job for me.
I managed to do this as follows,
Create the sqlite connection
$sqlite = new PDO('sqlite:./logs/log.db');
Create my own LogWritter similar to the framework
<?php
/**
* Description of LogWritter
*
* #author Ruwantha.Lankathilaka
*/
class LogWritter {
protected $sqliteConnection;
public function __construct($connection) {
$this->sqliteConnection = $connection;
}
/**
* Write function will bypass the slim default LogWriter and will return
* last inserted log id which could be used as a reference
*
* #param type $object will get the error message
* #param type $level will get the error levels of \Slim\Log
* #return mix if successfully logged will return the last insert id, else
* will return false
*/
public function write($object,$level) {
//Determine label
$label = 'DEBUG';
$message = (string) $object;
switch ($level) {
case \Slim\Log::FATAL:
$label = 'FATAL';
break;
case \Slim\Log::ERROR:
$label = 'ERROR';
break;
case \Slim\Log::WARN:
$label = 'WARN';
break;
case \Slim\Log::INFO:
$label = 'INFO';
break;
}
$sqliteQuery = "INSERT INTO logs (lable,message) VALUES (:lable,:message)";
$statement = $this->sqliteConnection->prepare($sqliteQuery);
$result = $statement->execute(array(':lable'=>$label,':message'=>$message));
if(!empty($result)){
return $this->sqliteConnection->lastInsertId();
}else{
return false;
}
}
}
Add the LogWritter to the index
Add the LogWritter to the Slim app
$app = new \Slim\Slim(array(
'log.writer' => $logWriter,
'log.enabled' => true,
'log.level' => \Slim\Log::DEBUG,
'debug' => true
));
now you can get the log from app
$retult = $app->log->error('test error');
$result will have the inserted log id false if the log failed
Hope this will help someone in future.

swiftmail symfony duplicate check for error log / email before sending

I would like to run a duplicate content check just before firing off an email whcih uses swiftmailer inside my symph2 app to send me dev ERROR log entries.
this functionality sits right next to my error log to database function, where it too has a duplicate check, although that one is much easier, it uses sql.
for this one, i want to maintain the last mail sent body for atleast the next 10 emails sent, so that if my error log goes out of control, it wont keep firing me duplicate emails of the same error.
should i just collect this body onto an object that holds last 10 email bodies, and attach this to the swift mailer class? or is there an easier way, like using something that is already embedded in swift mailer for this kind of post sending use? Or maybe a session..
Edit, i call swift mailer from a backend helper class, so think i can pretty much do anything there so long as its atleast semi-elegant.
EDIT this is a refined version of the method that calls both the persist and firing of email
<?php
class someWierdClass
{
public function addLogAction(Request $request, $persist = TRUE, $addEmail = TRUE)
{
$responseAdd = array();
if ($this->getRequest()->request->all() !== null) {
$data = $this->getRequest()->request->get('data') ? $this->getRequest()->request->get('data') : 'no_data';
$duplicate = $this->getRequest()->request->get('duplicate', null);
}
if ($addEmail) {
$responseAdd[] = 'firedIt';
$this->fireEmailString('You have an error log here. <br>' . $data);
}
if ($persist)
{
$responseAdd[] = 'persistedIt';
$this->persistLog($data, $duplicate);
}
if ($responseAdd)
{
$body = implode(', ', $responseAdd);
return new Response($body);
}
}
}
Log emails in a table and check that there it isn't a duplicate every time you send an email.
To do this, you should create a helper function that queries the emails table for entries who's body matches the body you would like to send. If the query returns nothing, then you know that isn't a duplicate. You would then send the email and log it the database. Otherwise, if it returned (a) record(s), you would send a dev ERROR log entry.
If you would like to only check against the last 10 emails, you would do this by querying for both $body == $new_body and $id >= ($total_rows-10)
You would then inject this into the container and call it using something like this
$this->container->get('helper')->sendEmail($body, $subject, $recipients);
Ok, thanks Dan for the idea as to using the database to do the dup check. If you notice, per your suggestion, i was already doing the dup check, but it made me think. It helped me connect the dots.
What i have done is return the answer if its a duplicate on the response when it does the updating the database, then using that response as a flag to determine if email fires or not. (in my case, i go further to check the updated stamp is at least +1 hour old, as opposed to the 'last 10 emails content' idea)
Heres the code.. Enjoy..
<?php
namespace Acme\AcmeBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller,
Acme\AcmeBundle\Entity\Log,
Symfony\Component\HttpFoundation\JsonResponse,
Symfony\Component\HttpFoundation\Response,
Symfony\Component\Config\Definition\Exception\Exception,
Symfony\Component\HttpFoundation\Request,
Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
class someWierdClass
{
/**
* #var array
*/
protected $senderArray = array('no-reply#yorudomain.com' => 'Your Website Name');
/**
* #param Request $request
* #param bool $persist
* #param bool $addEmail
* #return Response
*/
public function addLogAction(Request $request, $persist = TRUE, $addEmail = TRUE)
{
$responseAdd = array();
if ($this->getRequest()->request->all() !== null) {
$data = $this->getRequest()->request->get('data') ? $this->getRequest()->request->get('data') : 'no_data';
$type = $this->getRequest()->request->get('type') ? $this->getRequest()->request->get('type') : 'no_type';
$duplicate = $this->getRequest()->request->get('duplicate', null);
}
if ($addEmail) {
$responseAdd[] = 'firedIt';
$this->fireEmailString('You have an error log here. <br>' . $data);
}
if ($persist) {
$responseAdd[] = 'persistedIt';
$persistResponse = $this->persistLog( $type = $data, $duplicate);
if ($persistResponse) {
// a dup check is done here and results of this is on the response. (e.g. $content->passesCutoff)
$content = json_decode($persistResponse->getContent());
}
}
if ( $addEmail && ( isset($content->passesCutoff) && $content->passesCutoff ))
{
//fire off an email also, because its kind of hard to look in teh logs all the time, sometimes we just want an email.
$successEmail = $this->fireEmailString($data);
if( ! $successEmail )
{
$responseAdd[] = 'firedIt';
}
}
if ($responseAdd) {
$body = implode(', ', $responseAdd);
return new Response($body);
}
}
/**
* #param $emailStringData
* #param null $emailSubject
* #param null $emailTo
* #return mixed
*/
protected function fireEmailString($emailStringData, $emailSubject = null, $emailTo=null){
$templateName = 'AcmeBundle:Default:fireEmailString.html.twig';
if( ! $emailSubject )
{
$emailSubject = 'An email is being fired to you!' ;
}
if( ! $emailTo )
{
$emailTo = 'youremail#gmail.com';
}
$renderedView = $this->renderView(
$templateName, array(
'body' => $emailStringData,
));
$mailer = $this->get('mailer');
$message = $mailer->createMessage()
->setSubject( $emailSubject)
->setBody($emailStringData, 'text/plain')
->addPart($renderedView, 'text/html')
->setFrom($this->senderArray)
->setSender($this->senderArray)
->setTo($emailTo);
$results = $mailer->send($message);
return $results;
}
/**
* #param $type
* #param $data
* #param $duplicate
* #return JsonResponse
*/
protected function persistLog($type, $data, $duplicate) {
$em = $this->getDoctrine()->getManager();
$count = null;
$passesCutoff = null;
$mysqlNow = new \DateTime(date('Y-m-d G:i:s'));
//only two conditions can satisy here, strings '1' and 'true'.
if($duplicate !== '1' && $duplicate !== 'true' /*&& $duplicate != TRUE*/)
{
//in order to check if its unique we need to get the repo
//returns an object (findByData() would return an array)
$existingLog = $em->getRepository('AcmeBundle:Log')->findOneByData(
array('type' => $type, 'data' => $data)
);
if($existingLog)
{
$timeUpdatedString = strtotime($existingLog->getTimeupdated()->format('Y-m-d H:i:s'));
$cutoffStamp = strtotime('+1 hour', $timeUpdatedString); //advance 1 hour (customize this to the amount of time you want to go by before you consider this a duplicate. i think 1 hour is good)
$passesCutoff = time() >= $cutoffStamp ? TRUE : FALSE; //1 hour later
$count = $existingLog->getUpdatedcount();
$existingLog->setUpdatedcount($count + 1); // '2014-10-11 03:52:20' // date('Y-m-d G:i:s')
$em->persist($existingLog);
}
else
{
//this record isnt found, must be unique
$newLog = new Log(); //load our entity
//set in new values
$newLog->setType($type);
$newLog->setData($data);
$newLog->setUpdatedcount(0);
$newLog->setTimeupdated($mysqlNow);
$em->persist($newLog);
}
}
else
{
//we dont care if unique or not, we just want a new row
$newLog = new Log(); //load our entity
$newLog->setType($type);
$newLog->setData($data);
//time updated has been set to auto update to current timestamp in the schema, test first, then remove this
$newLog->setUpdatedcount(0);
$newLog->setTimeupdated($mysqlNow);
$em->persist($newLog);
}
$em->flush();
$response = new JsonResponse();
$response->setData(
array(
'data' => 'persistedIt',
'existingLog' => $count,
'passesCutoff' => $passesCutoff,
));
return $response;
}
}
In hindsight, i would have just passed the last update timestamp back on the response from the persist method, then do the cutoff calculation inside the fire email method obviously, but the above code does work as a starting point.. :-)

In symfony 1.4, using swiftmailer, setTo with parentheses in name breaks message sending

I am having issues sending mail with swift mailer when Recipient name contains parentheses ( and )
example code:
$mail = new Swift_Message();
$mail->setTo('recipient#test.com', 'Recipient (bla bla)');
$mail->setFrom('sender#test.com', 'Sender');
$mail->setSubject('Test email');
$mail->setBody('<html><body><h3>Hello</h3></body></html>', 'text/html');
sfContext::getInstance()->getMailer()->send($mail);
If the recipient is using gmail, he does receive the email but when clicking on the down arrow to get more info, The to: part is empty:
The problem is some other email services does not receive that email.
As for the original message content, it's too much details to black out to show here but basically my question is how can I use parentheses without breaking anything.
Seems like base64_encode can help so I modified the swiftmailer class SimpleMessage.php to be sure this fix is applied every time.
Modification of SimpleMessage from this:
public function setTo($addresses, $name = null)
{
if (!is_array($addresses) && isset($name))
{
$addresses = array($addresses => $name);
}
if (!$this->_setHeaderFieldModel('To', (array) $addresses))
{
$this->getHeaders()->addMailboxHeader('To', (array) $addresses);
}
return $this;
}
To this:
public function setTo($addresses, $name = null)
{
// if $name is set, encode it
if(isset($name))
{
$name = $this->encodeName($name);
}
// if $addresses is an non numeric array (email => name), encode each name.
if(is_array($addresses) && array_keys($addresses) !== range(0, count($addresses) - 1))
{
foreach($addresses as $key => $value)
{
$addresses[$key] = $this->encodeName($value);;
}
}
if (!is_array($addresses) && isset($name))
{
$addresses = array($addresses => $name);
}
if (!$this->_setHeaderFieldModel('To', (array) $addresses))
{
$this->getHeaders()->addMailboxHeader('To', (array) $addresses);
}
return $this;
}
/**
* Encode the name to avoid issues with some characters
*
* #param string $name
* #return string
*/
private function encodeName($name)
{
return "=?UTF-8?B?". base64_encode($name) . '?=';
}
This answer helped me: Base64 encode from name for email

Resources