vendor/bait/poll-bundle/Controller/PollController.php line 22

Open in your IDE?
  1. <?php
  2. namespace Bait\PollBundle\Controller;
  3. use Bait\PollBundle\Entity\Answer;
  4. use Bait\PollBundle\Entity\Question;
  5. use Doctrine\ORM\EntityManagerInterface;
  6. use Symfony\Bundle\FrameworkBundle\Controller\Controller;
  7. use Symfony\Component\HttpFoundation\Response;
  8. use Symfony\Component\HttpFoundation\Request;
  9. use Symfony\Component\HttpFoundation\File\UploadedFile;
  10. use Symfony\Component\HttpFoundation\Cookie;
  11. use Bait\PollBundle\Entity\Poll;
  12. use Bait\PollBundle\Entity\UserAnswer;
  13. use AppBundle\Entity\UserExtraData;
  14. use Bait\PollBundle\Entity\IpAddress;
  15. use Bait\PollBundle\Entity\UserAnswerGroup;
  16. use Bait\PollBundle\Event\PollSubmitCompletedEvent;
  17. class PollController extends Controller
  18. {
  19.     public function renderPollAction($id) {
  20.         $request Request::createFromGlobals();
  21.         $host $this->container->getParameter('site_url');
  22.         /** @var EntityManagerInterface $em */
  23.         $em $this->getDoctrine()->getManager();
  24.         /** @var Poll $poll */
  25.         $poll $em->getRepository('BaitPollBundle:Poll')
  26.             ->findOneById($id);
  27.         $user $this->getUser();
  28.         $viewData['poll'] = $poll;
  29.         $viewData['user'] = $user;
  30.         $viewData['errors'] = [];
  31.         // Personalized error messages for specific inputs
  32.         $viewData['inputErrors'] = [];
  33.         // check if poll exists (fixes case when article content has reference to deleted poll)
  34.         if (empty($viewData['poll'])) {
  35.             return new Response(''Response::HTTP_NO_CONTENT);
  36.         }
  37.         // check answerability of poll
  38.         // if user is logged in, and poll is not multi entry, check by existing user answer
  39.         if ($user && !$poll->getMultiEntry()) {
  40.             $alreadyAnswered $em->getRepository('BaitPollBundle:UserAnswerGroup')->findOneBy([
  41.                 'user' => $user->getId(),
  42.                 'poll' => $poll,
  43.             ]);
  44.             // if user is not present or the poll is multi entry, check by cookie
  45.         } else {
  46.             $cookies $request->cookies;
  47.             $alreadyAnswered $cookies->has('answered_poll_' $id);
  48.         }
  49.         $viewData['alreadyAnswered'] = $alreadyAnswered;
  50.         $viewData['recaptchaSiteKey'] = $this->container->getParameter('recaptcha_site_key');
  51.         if (empty($poll->getVotesLimit())) {
  52.             $votesLimitExceeded false;
  53.         } else {
  54.             $votesLimitExceeded $this->getPollAnswerCounts($poll) >= $poll->getVotesLimit();
  55.         }
  56.         if ($votesLimitExceeded) {
  57.             $poll->setMsgEnded('Maximálny počet hlasov bol naplnený.');
  58.             $poll->setDateEnd(new \DateTime(date('c'strtotime('-1 hour'))));
  59.         }
  60.         $dateEnd $poll->getDateEnd();
  61.         if ($alreadyAnswered || ($dateEnd != null && (int)$dateEnd->format('U') < time())) {
  62.             $ongoingResults $this->getOngoingResults($id);
  63.             $viewData['ongoingResultQuestions'] = $ongoingResults['ongoingResultQuestions'];
  64.             $viewData['ongoingResultAnswers'] = $ongoingResults['ongoingResultAnswers'];
  65.         }
  66.         if ($request->isMethod('POST') && !$alreadyAnswered && !$votesLimitExceeded && $_POST['submit'] == $id) {
  67.             if ($poll->getMultiEntry() && $poll->getMultiEntryInterval() == 'without_limit') {
  68.                 $recaptchaName 'g-recaptcha-response';
  69.                 $recaptchaToken $_POST[$recaptchaName];
  70.                 if (empty($recaptchaToken)) {
  71.                     $errorMsg $this->get('translator')->trans('Please check the reCAPTCHA form');
  72.                     return $this->handleError($viewData$errorMsg$recaptchaName);
  73.                 }
  74.                 $secretKey $this->container->getParameter('recaptcha_secret_key');
  75.                 $verificationUrl 'https://www.google.com/recaptcha/api/siteverify?secret=' urlencode($secretKey) . '&response=' urlencode($recaptchaToken);
  76.                 $verificationResponse file_get_contents($verificationUrl);
  77.                 $verificationResponse json_decode($verificationResponsetrue);
  78.                 if (!$verificationResponse['success']) {
  79.                     $errorMsg $this->get('translator')->trans('reCAPTCHA verification failed!');
  80.                     return $this->handleError($viewData$errorMsg$recaptchaName);
  81.                 }
  82.             }
  83.             $data $request->request->all();
  84.             $data array_merge($data$_FILES);
  85.             $isQuiz count($poll->getResults()) != 0// if poll has mapped results, it's a quiz
  86.             $quizWeights = [];
  87.             // validate if all required fields were set
  88.             /** @var Answer[] $requireds */
  89.             $requireds $em->getRepository('BaitPollBundle:Answer')
  90.                 ->createQueryBuilder('a')
  91.                 ->join('a.question''q')
  92.                 ->join('q.poll''p')
  93.                 ->where('p.id = :pollId')
  94.                 ->andWhere('a.required = true')
  95.                 ->setParameter('pollId'$id)
  96.                 ->getQuery()
  97.                 ->getResult();
  98.             $questionIds = [];
  99.             foreach($data as $a => $val) {
  100.                 $keys explode('-'$a);
  101.                 // this means question was NOT answered
  102.                 if (array_key_exists('3'$keys) && empty($val)) {
  103.                     $questionIds[] = [
  104.                         'qid'  => (int)$keys[3],
  105.                         'name' => $a,
  106.                     ];
  107.                 }
  108.             }
  109.             foreach($requireds as $r) {
  110.                 $qid $r->getQuestion()->getId();
  111.                 $qt $r->getQuestion()->getTitle();
  112.                 $match array_filter($questionIds, function($item) use ($qid) {
  113.                     return $item['qid'] === $qid;
  114.                 });
  115.                 if (!empty($match)) {
  116.                     $inputName reset($match)['name'];
  117.                     $errorMsg $qt ' ' $this->get('translator')->trans('is a required field');
  118.                     return $this->handleError($viewData$errorMsg$inputName);
  119.                 }
  120.             }
  121.             $ipAddress $em->getRepository('BaitPollBundle:IpAddress')->findOneBy(['ip' => $request->getClientIp()]);
  122.             if (empty($ipAddress)) {
  123.                 $ipAddress = new IpAddress();
  124.                 $ipAddress->setIp($request->getClientIp());
  125.                 $em->persist($ipAddress);
  126.                 $em->flush();
  127.             }
  128.             $userAnswerGroup = new UserAnswerGroup();
  129.             $userAnswerGroup->setPoll($poll);
  130.             if ($user) {
  131.                 $userAnswerGroup->setUser($user->getId());
  132.             }
  133.             $userAnswerGroup->setIpReferenceId($ipAddress->getId());
  134.             $em->persist($userAnswerGroup);
  135.             foreach($data as $inputName => $value) {
  136.                 if ($inputName == 'submit') continue;
  137.                 if ($inputName == 'g-recaptcha-response') continue;
  138.                 if (empty($value)) continue;
  139.                 $keys explode('-'$inputName);
  140.                 $pollId $keys[1];
  141.                 $questionId $keys[3];
  142.                 $answerType $keys[4];
  143.                 $userAnswer = new UserAnswer();
  144.                 /** @var Question $question */
  145.                 $question $em->getReference('BaitPollBundle:Question'$questionId);
  146.                 $userAnswer->setUserAnswerGroup($userAnswerGroup);
  147.                 $userAnswer->setQuestion($question);
  148.                 $userAnswer->setAnswerType($answerType);
  149.                 $userAnswer->setCreated(new \DateTime("now"));
  150.                 // FR-91 get email address for eshop confirmation email - FR poll #145
  151.                 if (strpos($host'funradio') !== false && $pollId == 145 && filter_var($valueFILTER_VALIDATE_EMAIL)) {
  152.                     $email $value;
  153.                 }
  154.                 if ($answerType == 'phone') {
  155.                     $answer $em->getReference('BaitPollBundle:Answer'$value);
  156.                     $normalized str_replace(' '''$value);
  157.                     // Validate E.164 phone number
  158.                     if (!preg_match('/^\+\d{7,15}$/'$normalized)) {
  159.                         $errorMsg $this->get('translator')->trans('Invalid phone number format');
  160.                         return $this->handleError($viewData$errorMsg$inputName);
  161.                     }
  162.                 }
  163.                 if ($answerType == 'text') {
  164.                     /** @var Answer $answer */
  165.                     $answerId $keys[5];
  166.                     $answer $em->getReference('BaitPollBundle:Answer'$answerId);
  167.                     // if answer.answer is defined, check if matches submitted value
  168.                     if (!empty($answer->getAnswer())) {
  169.                         $answerSlug $this->createSlug($answer->getAnswer());
  170.                         $valueSlug $this->createSlug($value);
  171.                         if ($answerSlug != $valueSlug) {
  172.                             $errorMsg $this->get('translator')->trans('Incorrectly answered question');
  173.                             return $this->handleError($viewData$errorMsg$inputName);
  174.                         }
  175.                     }
  176.                 }
  177.                 // handle various answer types
  178.                 if ($answerType == 'radio' || $answerType == 'checkbox' || $answerType == 'dropdown') {
  179.                     /** @var Answer $answer */
  180.                     $answer $em->getReference('BaitPollBundle:Answer'$value);
  181.                     // if quiz, mark weight of user's answer
  182.                     if ($isQuiz && $answer->getAnswerValue()) {
  183.                         $quizWeights[] = $answer->getAnswerValue();
  184.                     }
  185.                     $userAnswer->setAnswer($answer);
  186.                     $answerValue $answer->getAnswer();
  187.                 } else if ($answerType == 'upload') {
  188.                     $file = new UploadedFile($value["tmp_name"], $value["name"], $value["type"], $value["size"], $value["error"]);
  189.                     if(!$file->isValid()) {
  190.                         continue; // optional file upload field
  191.                     }
  192.                     if ($value["size"]/1000/1000 10) { // @todo: pull out to config
  193.                         $errorMsg $this->get('translator')->trans('File too large');
  194.                         return $this->handleError($viewData$errorMsg$inputName);
  195.                     }
  196.                     // This is necessary because Safari may label recorded audio files (.mp4) as 'application/octet-stream' by default
  197.                     if ($file->getMimeType() === 'application/octet-stream') {
  198.                         // Attempt to guess based on the file's original name or default to bin
  199.                         $extension $file->getClientOriginalExtension() ?: 'bin';
  200.                     } else {
  201.                         $extension $file->guessExtension();
  202.                     }
  203.                     $fileName md5(uniqid()).'.'.$extension;
  204.                     $allowedTypes = [
  205.                         // images
  206.                         "image/png",
  207.                         "image/jpeg",
  208.                         "image/jpg",
  209.                         "image/gif",
  210.                         "image/heic",
  211.                         // multimedia
  212.                         "audio/mpeg"// .mp3 as per RFC
  213.                         "audio/mp3",  // .mp3 Firefox, see https://stackoverflow.com/a/28021591/282325
  214.                         "audio/wav",
  215.                         "audio/x-wav",
  216.                         "application/ogg",
  217.                         "audio/ogg",
  218.                         "audio/mp4",
  219.                         "audio/x-m4a",
  220.                         "audio/m4a",
  221.                         "audio/webm",
  222.                         // documents
  223.                         "application/pdf",
  224.                         "application/msword"// .doc
  225.                         "application/vnd.openxmlformats-officedocument.wordprocessingml.document"// .docx
  226.                     ];
  227.                     if (!in_array($value["type"], $allowedTypes)) {
  228.                         $errorMsg $this->get('translator')->trans('File type not allowed');
  229.                         return $this->handleError($viewData$errorMsg$inputName);
  230.                     }
  231.                     $file->move(
  232.                         $this->getParameter('upload_path') . '/' 'poll' '/' $pollId,
  233.                         $fileName
  234.                     );
  235.                     $userAnswer->setValue($fileName);
  236.                     $answerValue $fileName;
  237.                 } else {
  238.                     $userAnswer->setValue($value);
  239.                     $answerValue $value;
  240.                 }
  241.                 if ($question->getSaveToUser()) {
  242.                     $extras = new UserExtraData();
  243.                     $extras->setUser($viewData['user']);
  244.                     $extras->setKey($question->getTitle());
  245.                     $extras->setValue($answerValue);
  246.                     $em->persist($extras);
  247.                 }
  248.                 $em->persist($userAnswer);
  249.                 $viewData['alreadyAnswered'] = $userAnswer;
  250.             }
  251.             $em->flush();
  252.             $viewData['answerSaved'] = true;
  253.             $dispatcher $this->get('event_dispatcher');
  254.             $event = new PollSubmitCompletedEvent($poll$user);
  255.             $dispatcher->dispatch(PollSubmitCompletedEvent::NAME$event);
  256.             // handle quiz results
  257.             if ($isQuiz && $poll->getResultsSummary() == 'most_frequent') {
  258.                 $values array_count_values($quizWeights);
  259.                 arsort($values);
  260.                 $values array_keys($values);
  261.                 $viewData['result'] = $em->getRepository('BaitPollBundle:Result')
  262.                     ->findOneBy(['poll' => $id'weight' => $values[0]]);
  263.             }
  264.             if ($isQuiz && $poll->getResultsSummary() == 'sum') {
  265.                 $sum array_sum($quizWeights);
  266.                 $result $em->getRepository('BaitPollBundle:Result')
  267.                     ->findOneBy(['poll' => $id'weight' => $sum]);
  268.                 if (empty($result)) {
  269.                     $result $em->getRepository('BaitPollBundle:Result')
  270.                         ->findOneBy(['poll' => $id'weight' => 'default']);
  271.                 }
  272.                 $viewData['result'] = $result;
  273.             }
  274.             // set a cookie and handle repeated poll entering
  275.             $pollMultiEntry $poll->getMultiEntry();
  276.             $pollMultiEntryInterval $poll->getMultiEntryInterval();
  277.             $pollResetHour $poll->getMultiEntryResetHour();
  278.             $cookieReset strtotime('now + 10 years');
  279.             if ($pollMultiEntry) {
  280.                 switch ($pollMultiEntryInterval) {
  281.                     case 'once_per_day':
  282.                         $day date("H") > $pollResetHour 'tomorrow' 'today';
  283.                         $cookieReset strtotime($day ' ' $pollResetHour ':00');
  284.                         break;
  285.                     case 'once_per_hour':
  286.                         // PHP treats 'today 24:00' as 'tomorrow 00:00'
  287.                         $cookieReset strtotime('today ' . (date("H") + 1) . ':00');
  288.                         break;
  289.                     case 'once_per_week':
  290.                         $cookieReset strtotime('next monday');
  291.                         break;
  292.                 }
  293.             }
  294.             $ongoingResults $this->getOngoingResults($id);
  295.             $viewData['ongoingResultQuestions'] = $ongoingResults['ongoingResultQuestions'];
  296.             $viewData['ongoingResultAnswers'] = $ongoingResults['ongoingResultAnswers'];
  297.             // FR-91 send confirmation and notification email for FR poll #145
  298.             if (strpos($host'funradio') !== false && $poll->getId() === 145) {
  299.                 if (isset($email)) {
  300.                     $this->sendEmail('confirmation'$email);
  301.                 }
  302.                 $this->sendEmail('notification'$this->getParameter('eshop_notification_email'));
  303.             }
  304.             $cookie = new Cookie('answered_poll_' $id'true'$cookieReset);
  305.             $response $this->render('BaitPollBundle:Poll:poll_results.html.twig'$viewData);
  306.             if (!($poll->getMultiEntry() && $poll->getMultiEntryInterval() == 'without_limit')) {
  307.                 $response->headers->setCookie($cookie);
  308.             }
  309.             $response->sendHeaders();
  310.             return $response;
  311.         }
  312.         return $this->render('BaitPollBundle:Poll:poll_results.html.twig'$viewData);
  313.     }
  314.     public function myPollsAction() {
  315.         $this->articleModel $this->get('eemce.appbundle.article');
  316.         $viewData = [];
  317.         $viewData['title'] = "Moje súťaže";
  318.         $viewData['articles'] = $this->articleModel->getArticlesWithUserParticipationPolls($this->getUser());
  319.         return $this->render('BaitPollBundle:Poll:my_polls.html.twig'$viewData);
  320.     }
  321.     public function pollArchiveAction() {
  322.         $this->articleModel $this->get('eemce.appbundle.article');
  323.         $viewData = [];
  324.         $viewData['title'] = "Archív súťaží";
  325.         $viewData['articles'] = $this->articleModel->getArticlesWithEndedPolls();
  326.         return $this->render('BaitPollBundle:Poll:archive.html.twig'$viewData);
  327.     }
  328.     private function handleError($viewData$errorMsg$inputName) {
  329.         $viewData["errors"][] = $errorMsg;
  330.         $viewData["inputErrors"][] = [
  331.             'name'  => $inputName,
  332.             'error' => $errorMsg,
  333.         ];
  334.         $viewData["alreadyAnswered"] = null;
  335.         return $this->render('BaitPollBundle:Poll:poll.html.twig'$viewData);
  336.     }
  337.     private function getOngoingResults($pollId)
  338.     {
  339.         /** @var EntityManagerInterface $em */
  340.         $em $this->getDoctrine()->getManager();
  341.         /** @var UserAnswer[] $ongoingResult */
  342.         $ongoingResult $em->getRepository('BaitPollBundle:UserAnswer')
  343.             ->createQueryBuilder('ua')
  344.             ->join('ua.answer''a')
  345.             ->join('a.question''q')
  346.             ->join('q.poll''p')
  347.             ->where('p.id = :pollId')
  348.             ->andWhere('q.showOngoingResult = true')
  349.             ->andWhere('a.type = :radio OR a.type = :dropdown')
  350.             ->setParameter('pollId'$pollId)
  351.             ->setParameter('radio'"radio")
  352.             ->setParameter('dropdown'"dropdown")
  353.             ->getQuery()
  354.             ->getResult();
  355.         $ongoingResultQuestions = [];
  356.         $ongoingResultAnswers = [];
  357.         foreach ($ongoingResult as $or) {
  358.             $questionId $or->getQuestion()->getId();
  359.             $answerId $or->getAnswer()->getId();
  360.             array_push($ongoingResultQuestions$questionId);
  361.             array_push($ongoingResultAnswers$answerId);
  362.         }
  363.         $ongoingResults = [];
  364.         $ongoingResults['ongoingResultQuestions'] = array_count_values($ongoingResultQuestions);
  365.         $ongoingResults['ongoingResultAnswers'] = array_count_values($ongoingResultAnswers);
  366.         // bonus points
  367.         /** @var Poll $poll */
  368.         $poll $em->getRepository('BaitPollBundle:Poll')->find($pollId);
  369.         /** @var Question $question */
  370.         foreach ($poll->getQuestions() as $question) {
  371.             if ($question->getShowOngoingResult()) {
  372.                 /** @var Answer $answer */
  373.                 foreach ($question->getAnswers() as $answer) {
  374.                     if ($answer->getBonusPoints() > 0) {
  375.                         $ongoingResults['ongoingResultQuestions'][$question->getId()] += $answer->getBonusPoints();
  376.                         $ongoingResults['ongoingResultAnswers'][$answer->getId()] += $answer->getBonusPoints();
  377.                     }
  378.                 }
  379.             }
  380.         }
  381.         return $ongoingResults;
  382.     }
  383.     /**
  384.      * Get answer counts for poll
  385.      *
  386.      * @param Poll $poll
  387.      *
  388.      * @return int
  389.      */
  390.     private function getPollAnswerCounts($poll)
  391.     {
  392.         /** @var EntityManagerInterface $em */
  393.         $em $this->getDoctrine()->getManager();
  394.         $answerGroupCount $em->getRepository('BaitPollBundle:UserAnswerGroup')
  395.             ->createQueryBuilder('uag')
  396.             ->select('COUNT(uag.id)')
  397.             ->where('uag.poll = :poll')
  398.             ->setParameter('poll'$poll)
  399.             ->getQuery()
  400.             ->getSingleScalarResult();
  401.         return $answerGroupCount;
  402.     }
  403.     private function sendEmail($type$email)
  404.     {
  405.         $subject $this->get('translator')->trans($type);
  406.         $message = (new \Swift_Message($subject))
  407.             ->setFrom($this->getParameter('mailer_sender_email_eshop'), $this->getParameter('mailer_sender_name_eshop'))
  408.             ->setTo($email)
  409.             ->setBody(
  410.                 $this->renderView('BaitPollBundle:Emails:'.$type.'.html.twig'),
  411.                 'text/html'
  412.             )
  413.             ->addPart(
  414.                 $this->renderView('BaitPollBundle:Emails:'.$type.'.txt.twig'),
  415.                 'text/plain'
  416.             );
  417.         $this->get('mailer')->send($message);
  418.     }
  419.     /**
  420.      * Create slug
  421.      *
  422.      * @param string $text
  423.      *
  424.      * @return string
  425.      */
  426.     private function createSlug($text)
  427.     {
  428.         $text strtolower(strip_tags($text));
  429.         $accentedChars = ['À''Á''Â''Ã''Ä''Å''Æ''Ç''È''É''Ê''Ë''Ì''Í''Î''Ï''Ð''Ñ''Ò''Ó''Ô''Õ''Ö''Ø''Ù''Ú''Û''Ü''Ý''ß''à''á''â''ã''ä''å''æ''ç''è''é''ê''ë''ì''í''î''ï''ñ''ò''ó''ô''õ''ö''ø''ù''ú''û''ü''ý''ÿ''Ā''ā''Ă''ă''Ą''ą''Ć''ć''Ĉ''ĉ''Ċ''ċ''Č''č''Ď''ď''Đ''đ''Ē''ē''Ĕ''ĕ''Ė''ė''Ę''ę''Ě''ě''Ĝ''ĝ''Ğ''ğ''Ġ''ġ''Ģ''ģ''Ĥ''ĥ''Ħ''ħ''Ĩ''ĩ''Ī''ī''Ĭ''ĭ''Į''į''İ''ı''IJ''ij''Ĵ''ĵ''Ķ''ķ''Ĺ''ĺ''Ļ''ļ''Ľ''ľ''Ŀ''ŀ''Ł''ł''Ń''ń''Ņ''ņ''Ň''ň''ʼn''Ō''ō''Ŏ''ŏ''Ő''ő''Œ''œ''Ŕ''ŕ''Ŗ''ŗ''Ř''ř''Ś''ś''Ŝ''ŝ''Ş''ş''Š''š''Ţ''ţ''Ť''ť''Ŧ''ŧ''Ũ''ũ''Ū''ū''Ŭ''ŭ''Ů''ů''Ű''ű''Ų''ų''Ŵ''ŵ''Ŷ''ŷ''Ÿ''Ź''ź''Ż''ż''Ž''ž''ſ''ƒ''Ơ''ơ''Ư''ư''Ǎ''ǎ''Ǐ''ǐ''Ǒ''ǒ''Ǔ''ǔ''Ǖ''ǖ''Ǘ''ǘ''Ǚ''ǚ''Ǜ''ǜ''Ǻ''ǻ''Ǽ''ǽ''Ǿ''ǿ'];
  430.         $cleanChars = ['A''A''A''A''A''A''AE''C''E''E''E''E''I''I''I''I''D''N''O''O''O''O''O''O''U''U''U''U''Y''s''a''a''a''a''a''a''ae''c''e''e''e''e''i''i''i''i''n''o''o''o''o''o''o''u''u''u''u''y''y''A''a''A''a''A''a''C''c''C''c''C''c''C''c''D''d''D''d''E''e''E''e''E''e''E''e''E''e''G''g''G''g''G''g''G''g''H''h''H''h''I''i''I''i''I''i''I''i''I''i''IJ''ij''J''j''K''k''L''l''L''l''L''l''L''l''l''l''N''n''N''n''N''n''n''O''o''O''o''O''o''OE''oe''R''r''R''r''R''r''S''s''S''s''S''s''S''s''T''t''T''t''T''t''U''u''U''u''U''u''U''u''U''u''U''u''W''w''Y''y''Y''Z''z''Z''z''Z''z''s''f''O''o''U''u''A''a''I''i''O''o''U''u''U''u''U''u''U''u''U''u''A''a''AE''ae''O''o'];
  431.         $slug strtolower(preg_replace(['/[^a-zA-Z0-9 -]/''/[-]+/''/^-|-$/'],
  432.             ['''-'''],
  433.             str_replace($accentedChars,
  434.                 $cleanChars,
  435.                 $text)));
  436.         $slug str_replace(' ',
  437.             '-',
  438.             $slug);
  439.         return $slug;
  440.     }
  441. }