src/Twig/AppExtension.php line 356

Open in your IDE?
  1. <?php
  2. namespace App\Twig;
  3. use App\Admin\Entity\StdDomainsValues;
  4. use App\Admin\Entity\StdPagesPages;
  5. use App\Service\PageInfoCacheService;
  6. use Twig\Extension\AbstractExtension;
  7. use Twig\Environment;
  8. use Twig\TwigFunction;
  9. use Twig\TwigFilter;
  10. use Twig\TwigTest;
  11. use App\Utils\Functions;
  12. use Doctrine\ORM\EntityManagerInterface;
  13. use Symfony\Component\Security\Core\Security;
  14. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  15. use Symfony\Component\HttpFoundation\RequestStack;
  16. use Symfony\Contracts\Translation\TranslatorInterface;
  17. class AppExtension extends AbstractExtension
  18. {
  19.     protected $entityManager;
  20.     protected $parameterBag;
  21.     private $twig;
  22.     private Security $security;
  23.     private RequestStack $requestStack;
  24.     private TranslatorInterface $translator;
  25.     private PageInfoCacheService $pageInfoCache;
  26.     public function __construct(
  27.         EntityManagerInterface $entityManager,
  28.         ParameterBagInterface $parameterBag,
  29.         Environment $twig,
  30.         Security $security,
  31.         RequestStack $requestStack,
  32.         TranslatorInterface $translator,
  33.         PageInfoCacheService $pageInfoCache
  34.     ) {
  35.         $this->entityManager $entityManager;
  36.         $this->parameterBag $parameterBag;
  37.         $this->security $security;
  38.         $this->twig $twig;
  39.         $this->requestStack $requestStack;
  40.         $this->translator $translator;
  41.         $this->pageInfoCache $pageInfoCache;
  42.     }
  43.     public function getFunctions(): array
  44.     {
  45.         return [
  46.             new TwigFunction('url_content', [$this'getUrlContent']),
  47.             new TwigFunction('file_get_contents',[$this'fileGetContents']),
  48.             new TwigFunction('clean_string', [$this'cleanString']),
  49.             new TwigFunction('get_config', [$this'getConfig']),
  50.             new TwigFunction('get_env', [$this'getVariables']),
  51.             new TwigFunction('generate_barcode', [$this'generateBarcode']),
  52.             new TwigFunction('array_search',  'array_search'),
  53.             new TwigFunction('is_mobile', [$this'isMobile']),
  54.             new TwigFunction('trim_text', [$this'trimText']),
  55.             new TwigFunction('get_page_info', [$this'getPageInfo']),
  56.             new TwigFunction('get_image_size', [$this'getImageSize']),
  57.             new TwigFunction('get_domain_value_description', [$this'getDomainValueDescription']),
  58.             new TwigFunction('get_domain_value', [$this'getDomainValue']),
  59.             new TwigFunction('get_relation_children', [$this'getRelationChildren']),
  60.             new TwigFunction('check_form_permissions', [$this'checkFormPermissions']),
  61.             new TwigFunction('get_content_by_machine_name', [$this'getContentByMachineName']),
  62.             new TwigFunction('twig_file_exists', [$this'twigFileExists']),
  63.             new TwigFunction('eval', [$this'evalFunction']),
  64.             new TwigFunction('is_authenticated_in_studio', [$this'isAuthenticatedInStudio']),
  65.             new TwigFunction('get_studio_user', [$this'getStudioUser']),
  66.             new TwigFunction('time_elapsed', [$this'timeElapsed']),
  67.             new TwigFunction('format_event_range', [$this'formatEventRange']),
  68.             new TwigFunction('format_date_locale', [$this'formatDateLocale']),
  69.         ];
  70.     }
  71.     public function getFilters(): array
  72.     {
  73.         return [
  74.             new TwigFilter('urldecode',  'urldecode'),
  75.             new TwigFilter('format_date',  [$this,'formatDate']),
  76.             new TwigFilter('array_filter',  'array_filter'),
  77.             new TwigFilter('array_unique',  'array_unique'),
  78.             new TwigFilter('json_decode',  'json_decode'),
  79.             new TwigFilter('extension', [$this'extension']),
  80.             new TwigFilter('get_variables', [$this'getVariables']),
  81.             new TwigFilter('get_pdf_cover', [$this'getPDFCover']),
  82.             new TwigFilter('html_entity_decode''html_entity_decode'),
  83.             new TwigFilter('eval', [$this'evalFilter']),
  84.             new TwigFilter('to_lower', [$this'toLower']),
  85.             new TwigFilter('pad', [$this'pad']),
  86.             new TwigFilter('fix_iframes', [$this'fixIframes'], ['is_safe' => ['html']]),
  87.             new TwigFilter('encode_url_path', [$this'encodeUrlPath']),
  88.         ];
  89.     }
  90.     public function getTests(): array
  91.     {
  92.         return [
  93.             new TwigTest('ondisk',[$this,'onDisk']),
  94.             new TwigTest('instanceof', array($this'isInstanceOf'))
  95.         ];
  96.     }
  97.     public function onDisk($file){
  98.         return file_exists($this->parameterBag->get('kernel.project_dir')."/".getenv("PUBLIC_DIR").$file);
  99.     }
  100.     public function isInstanceOf($object$class)
  101.     {
  102.         $reflectionClass = new \ReflectionClass($class);
  103.         return $reflectionClass->isInstance($object);
  104.     }
  105.     public function getUrlContent($url)
  106.     {
  107.         return Functions::getUrlContent($url);
  108.     }
  109.     public function isMobile()
  110.     {
  111.         $useragent=$_SERVER['HTTP_USER_AGENT'];
  112.         return preg_match('/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i',$useragent)||preg_match('/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i',substr($useragent,0,4));
  113.     }
  114.     public function trimText($text,$trimLength,$textToAdd=""){
  115.         $return html_entity_decode($textENT_COMPAT'utf-8');
  116.         $return substr($return,0,$trimLength).$textToAdd;
  117.         return $return;
  118.     }
  119.     public function cleanString($input$replace '-'$remove_words false$words_array = array())
  120.     {
  121.         $return htmlentities($inputENT_COMPAT'utf-8');
  122.         $return preg_replace("`&([a-z])(acute|uml|circ|grave|ring|cedil|slash|tilde|caron|lig|quot|rsquo);`i""\\1"$return);
  123.         $return trim(preg_replace('/(&amp)/'''strtolower($return)));
  124.         $return trim(preg_replace('/[^a-zA-Z0-9\s\-]/'''strtolower($return)));
  125.         $return trim(preg_replace('/ +/'' '$return));
  126.         if ($remove_words) {
  127.             $return remove_words($return$replace$words_array);
  128.         }
  129.         $return str_replace(' '$replace$return);
  130.         $return trim(preg_replace('/' $replace '+/'$replace$return));
  131.         return $return;
  132.     }
  133.     public function getConfig($machineName$lang '')
  134.     {
  135.         return Functions::getConfig($machineName,$lang,$this->entityManager);
  136.     }
  137.     public function extension($file)
  138.     {
  139.         return pathinfo($filePATHINFO_EXTENSION);
  140.     }
  141.     public function formatDate($str){
  142.         return date("d/m/Y h:i A",time($str));
  143.     }
  144.     public function generateBarcode($type,$code,$width,$height,$color='black',$bgcolor='white',$responsetype='html')
  145.     {
  146.         $barcode = new \Com\Tecnick\Barcode\Barcode();
  147.         if($type == "EAN13"){
  148.             //Calcula o CheckDigit
  149.             $digits =(string)$code;
  150.             // Deprecated $even_sum = $digits{1} + $digits{3} + $digits{5} + $digits{7} + $digits{9} + $digits{11};
  151.             $even_sum substr($digits11) + substr($digits31) + substr($digits51) + substr($digits71) + substr($digits91) + substr($digits111);
  152.             $even_sum_three $even_sum 3;
  153.             // Deprecated $odd_sum = $digits{0} + $digits{2} + $digits{4} + $digits{6} + $digits{8} + $digits{10};
  154.             $odd_sum substr($digits01) + substr($digits21) + substr($digits41) + substr($digits61) + substr($digits81) + substr($digits101);
  155.             $total_sum $even_sum_three $odd_sum;
  156.             $next_ten = (ceil($total_sum/10))*10;
  157.             $check_digit $next_ten $total_sum;
  158.             $code substr($code,0,12) . $check_digit;
  159.         }
  160.         $bobj $barcode->getBarcodeObj($type$code$width$height$color,array(0,0,0,0))->setBackgroundColor($bgcolor);
  161.         if($responsetype == "svg")
  162.             return $bobj->getSvgCode();
  163.         else
  164.             return $bobj->getHtmlDiv();
  165.     }
  166.     public function getVariables($variable) {
  167.         return getenv($variable);
  168.     }
  169.     public function getPDFCover($pdffile){
  170.         $pdffile urldecode($pdffile);
  171.         if($this->onDisk($pdffile)){
  172.             $filesignature=md5($pdffile).".jpg";
  173.             $filepath=$this->parameterBag->get('kernel.project_dir')."/public".$pdffile;
  174.             $cachepath=$this->parameterBag->get('kernel.project_dir')."/public/media/".$filesignature;
  175.             if(!$this->onDisk("/media/".$filesignature) || filemtime($filepath)>filemtime($cachepath)){
  176.                 try {
  177.                     $image = new \Imagick();
  178.                     $image->setResolution(200,200);
  179.                     $image->SetColorspace(\Imagick::COLORSPACE_SRGB);
  180.                     $image->readImage($filepath."[0]");
  181.                     $image->setImageAlphaChannel(\Imagick::ALPHACHANNEL_REMOVE );
  182.                     $image->setImageFormat('jpg');
  183.                     $image->writeImage($cachepath);
  184.                     $image->clear();
  185.                     $image->destroy();
  186.                     return "/media/".$filesignature;
  187.                 } catch (\Throwable $th) {
  188.                 }
  189.             }
  190.             else
  191.             {
  192.                 return "/media/".$filesignature;
  193.             }
  194.         }
  195.         $default="/media/default.jpg";
  196.         if(!$this->onDisk($default)){
  197.             $image imagecreate(11);
  198.             $background_color imagecolorallocate($image255255255);
  199.             imagefill($image00$background_color);
  200.             imagejpeg($image$this->parameterBag->get('kernel.project_dir')."/public".$default);
  201.         }
  202.         return $default;
  203.     }
  204.     public function evalFilter($value){
  205.         //create twig string from value
  206.         $template=$this->twig->createTemplate($value);
  207.         return $template->render(array());
  208.     }
  209.     public function evalFunction($value,$params){
  210.         //create twig string from value
  211.         $template=$this->twig->createTemplate($value);
  212.         return $template->render($params);
  213.     }
  214.     public function getPageInfo($locale$id$renderCat true$rootCategory null)
  215.     {
  216.         // Use Redis-cached version for better performance
  217.         return $this->pageInfoCache->getPageInfo($locale$id$renderCat$rootCategory);
  218.     }
  219.     public function getDomainValueDescription($domainMachineName,$machineName,string $locale):?string
  220.     {
  221.         return Functions::getDomainValueDescription($locale,$domainMachineName,$machineName$this->entityManager);
  222.     }
  223.     /**
  224.      * Encodes URL path segments while preserving slashes.
  225.      */
  226.     public function encodeUrlPath(?string $url): string
  227.     {
  228.         if (empty($url)) {
  229.             return '';
  230.         }
  231.         // Parse the URL to get its components
  232.         $parsed parse_url($url);
  233.         
  234.         if (isset($parsed['path'])) {
  235.             // Split path by slash, encode each segment, then rejoin
  236.             $segments explode('/'$parsed['path']);
  237.             $encodedSegments array_map(function($segment) {
  238.                 return rawurlencode(rawurldecode($segment));
  239.             }, $segments);
  240.             $parsed['path'] = implode('/'$encodedSegments);
  241.         }
  242.         // Rebuild the URL
  243.         $result '';
  244.         if (isset($parsed['scheme'])) {
  245.             $result .= $parsed['scheme'] . '://';
  246.         }
  247.         if (isset($parsed['host'])) {
  248.             $result .= $parsed['host'];
  249.         }
  250.         if (isset($parsed['port'])) {
  251.             $result .= ':' $parsed['port'];
  252.         }
  253.         if (isset($parsed['path'])) {
  254.             $result .= $parsed['path'];
  255.         }
  256.         if (isset($parsed['query'])) {
  257.             $result .= '?' $parsed['query'];
  258.         }
  259.         if (isset($parsed['fragment'])) {
  260.             $result .= '#' $parsed['fragment'];
  261.         }
  262.         return $result;
  263.     }
  264.     public function getImageSize(string $filename, array &$image_info null)
  265.     {
  266.         $filename substr(parse_url($filenamePHP_URL_PATH), 1);
  267.         if(file_exists($filename)){
  268.             return getimagesize($filename$image_info);
  269.         }
  270.         return [0,0];
  271.     }
  272.     public function getDomainValue($domainMachineName,$machineName):?StdDomainsValues
  273.     {
  274.         return $this->entityManager->getRepository(StdDomainsValues::class)->findOneByDomainAndValue($domainMachineName,$machineName);
  275.     }
  276.     public function getRelationChildren($relationId,$languagecode$params)
  277.     {
  278.         return $this->entityManager->getRepository(StdPagesPages::class)->getRelationChildren($relationId$languagecode,$params);
  279.     }
  280.     public function checkFormPermissions($form_content)
  281.     {
  282.         return Functions::checkFormPermissions$this->security$this->entityManager$form_content);
  283.     }
  284.     public function toLower($str)
  285.     {
  286.         if (is_array($str)){
  287.             array_walk($str, function(&$item){$item strtolower($item);});
  288.         }
  289.         else{
  290.             $str strtolower($str);
  291.         }
  292.         return $str;
  293.     }
  294.     public function fileGetContents($filepath){
  295.         if($this->onDisk($filepath)){
  296.             return file_get_contents($this->parameterBag->get('kernel.project_dir')."/".getenv("PUBLIC_DIR").$filepath);
  297.         }else{
  298.             return "";
  299.         }
  300.     }
  301.     public function getContentByMachineName($locale$machineName)
  302.     {
  303.         return Functions::getContentByMachineName($locale$machineName$this->entityManager);
  304.     }
  305.     public function twigFileExists($path)
  306.     {
  307.         $loader $this->twig->getLoader();
  308.         return $loader->exists($path);
  309.     }
  310.     public function pad($number$length$padString '0'$side 'left')
  311.     {
  312.         $padType $side === 'left' STR_PAD_LEFT STR_PAD_RIGHT;
  313.         return str_pad($number$length$padString$padType);
  314.     }
  315.     public function isAuthenticatedInStudio(): bool
  316.     {
  317.         $studioToken $this->requestStack->getSession()->get('_security_studio_context');
  318.         if (!$studioToken) {
  319.             return false;
  320.         }
  321.         // If token exists in session, check if it's valid
  322.         try {
  323.             $token unserialize($studioToken);
  324.             return $token && $token->isAuthenticated();
  325.         } catch (\Exception $e) {
  326.             return false;
  327.         }
  328.     }
  329.     public function getStudioUser()
  330.     {
  331.         $studioToken $this->requestStack->getSession()->get('_security_studio_context');
  332.         if (!$studioToken) {
  333.             return null;
  334.         }
  335.         // If token exists in session, check if it's valid
  336.         try {
  337.             $token unserialize($studioToken);
  338.             return $token->getUser();
  339.         } catch (\Exception $e) {
  340.             return null;
  341.         }
  342.     }
  343.     /**
  344.      * Calculate time elapsed since a given date using translations
  345.      *
  346.      * @param mixed $publishedDate The published date (string, DateTime, or null)
  347.      * @return string The formatted time elapsed string
  348.      */
  349.     public function timeElapsed($publishedDate): string
  350.     {
  351.         if (!$publishedDate) {
  352.             return $this->translator->trans('time_recently', [], 'custom');
  353.         }
  354.         try {
  355.             // Convert to DateTime if it's a string
  356.             if (is_string($publishedDate)) {
  357.                 $published = new \DateTime($publishedDate);
  358.             } elseif ($publishedDate instanceof \DateTimeInterface) {
  359.                 $published $publishedDate;
  360.             } else {
  361.                 return $this->translator->trans('time_recently', [], 'custom');
  362.             }
  363.             $now = new \DateTime();
  364.             $diff $now->getTimestamp() - $published->getTimestamp();
  365.             // Calculate time units
  366.             $minutes round($diff 60);
  367.             $hours round($diff 3600);
  368.             $days round($diff 86400);
  369.             $weeks round($diff 604800);
  370.             $months round($diff 2592000); // Approximate month (30 days)
  371.             $years round($diff 31536000); // Approximate year (365 days)
  372.             // Return appropriate format using translations
  373.             if ($diff 60) {
  374.                 return $this->translator->trans('time_recently', [], 'custom');
  375.             } elseif ($minutes 60) {
  376.                 return $this->translator->trans('time_minutes_ago', ['%count%' => $minutes], 'custom');
  377.             } elseif ($hours 24) {
  378.                 return $this->translator->trans('time_hours_ago', ['%count%' => $hours], 'custom');
  379.             } elseif ($days 7) {
  380.                 return $this->translator->trans('time_days_ago', ['%count%' => $days], 'custom');
  381.             } elseif ($weeks 4) {
  382.                 return $this->translator->trans('time_weeks_ago', ['%count%' => $weeks], 'custom');
  383.             } elseif ($months 12) {
  384.                 return $this->translator->trans('time_months_ago', ['%count%' => $months], 'custom');
  385.             } else {
  386.                 return $this->translator->trans('time_years_ago', ['%count%' => $years], 'custom');
  387.             }
  388.         } catch (\Exception $e) {
  389.             return $this->translator->trans('time_recently', [], 'custom');
  390.         }
  391.     }
  392.     // 2) Mapa de meses por idioma
  393.     private function monthsByLocale(string $locale): array
  394.     {
  395.         $map = [
  396.             'pt' => [1=>'janeiro',2=>'fevereiro',3=>'março',4=>'abril',5=>'maio',6=>'junho',7=>'julho',8=>'agosto',9=>'setembro',10=>'outubro',11=>'novembro',12=>'dezembro'],
  397.             'en' => [1=>'January',2=>'February',3=>'March',4=>'April',5=>'May',6=>'June',7=>'July',8=>'August',9=>'September',10=>'October',11=>'November',12=>'December'],
  398.             'es' => [1=>'enero',2=>'febrero',3=>'marzo',4=>'abril',5=>'mayo',6=>'junio',7=>'julio',8=>'agosto',9=>'septiembre',10=>'octubre',11=>'noviembre',12=>'diciembre'],
  399.         ];
  400.         // normaliza ('pt-PT' -> 'pt')
  401.         $short strtolower(substr($locale02));
  402.         return $map[$short] ?? $map['pt'];
  403.     }
  404.     /**
  405.      * Converte para DateTime de forma segura
  406.      */
  407.     private function toDateTime($value): ?\DateTimeInterface
  408.     {
  409.         if ($value instanceof \DateTimeInterface) return $value;
  410.         if (is_string($value) && trim($value) !== '') {
  411.             try { return new \DateTime($value); } catch (\Throwable $e) {}
  412.         }
  413.         return null;
  414.     }
  415.     /**
  416.      * Formata uma data simples no idioma atual.
  417.      * Ex.:  "14 de agosto de 2025" (pt) | "August 14, 2025" (en)
  418.      */
  419.     public function formatDateLocale($date, ?string $locale null): string
  420.     {
  421.         $dt $this->toDateTime($date);
  422.         if (!$dt) return '';
  423.         $loc $locale ?: ($this->requestStack->getCurrentRequest()->getLocale() ?? 'pt');
  424.         $months $this->monthsByLocale($loc);
  425.         $d $dt->format('d');
  426.         $m = (int)$dt->format('n');
  427.         $y $dt->format('Y');
  428.         $short strtolower(substr($loc02));
  429.         if ($short === 'en') {
  430.             // English style
  431.             return sprintf('%s %s, %s'$months[$m], $d$y);
  432.         } elseif ($short === 'es') {
  433.             return sprintf('%s de %s de %s'$d$months[$m], $y);
  434.         }
  435.         // default pt
  436.         return sprintf('%s de %s %s'$d$months[$m], $y);
  437.     }
  438.     /**
  439.      * Formata o intervalo start/end no idioma atual.
  440.      * Regras:
  441.      * - anos diferentes: "dd de Mês AAAA a dd de Mês AAAA"
  442.      * - mesmo ano e mês: "dd a dd de Mês AAAA"
  443.      * - mesmo ano, meses diferentes: "dd de Mês a dd de Mês AAAA"
  444.      * - sem end: data única completa
  445.      */
  446.     public function formatEventRange($start$end null, ?string $locale null): string
  447.     {
  448.         $s $this->toDateTime($start);
  449.         if (!$s) return '';
  450.         $loc $locale ?: ($this->requestStack->getCurrentRequest()->getLocale() ?? 'pt');
  451.         $months $this->monthsByLocale($loc);
  452.         $y1 = (int)$s->format('Y');
  453.         $m1 = (int)$s->format('n');
  454.         $d1 $s->format('d');
  455.         $e $this->toDateTime($end);
  456.         if (!$e) {
  457.             return $this->formatDateLocale($s$loc);
  458.         }
  459.         $y2 = (int)$e->format('Y');
  460.         $m2 = (int)$e->format('n');
  461.         $d2 $e->format('d');
  462.         $short strtolower(substr($loc02));
  463.         // anos diferentes
  464.         if ($y1 !== $y2) {
  465.             if ($short === 'en') {
  466.                 return sprintf('%s %s, %s to %s %s, %s'$months[$m1], $d1$y1$months[$m2], $d2$y2);
  467.             } elseif ($short === 'es') {
  468.                 return sprintf('%s de %s de %s a %s de %s de %s'$d1$months[$m1], $y1$d2$months[$m2], $y2);
  469.             }
  470.             // pt
  471.             return sprintf('%s de %s %s a %s de %s %s'$d1$months[$m1], $y1$d2$months[$m2], $y2);
  472.         }
  473.         // mesmo ano
  474.         if ($m1 === $m2) {
  475.             if ($short === 'en') {
  476.                 return sprintf('%s–%s %s %s'$d1$d2$months[$m2], $y2);
  477.             } elseif ($short === 'es') {
  478.                 return sprintf('%s a %s de %s de %s'$d1$d2$months[$m2], $y2);
  479.             }
  480.             // pt
  481.             return sprintf('%s a %s de %s %s'$d1$d2$months[$m2], $y2);
  482.         }
  483.         // mesmo ano, meses diferentes
  484.         if ($short === 'en') {
  485.             return sprintf('%s %s to %s %s, %s'$months[$m1].' '.$d1$y1$months[$m2].' '.$d2$months[$m2], $y2); // forma enxuta
  486.         } elseif ($short === 'es') {
  487.             return sprintf('%s de %s a %s de %s de %s'$d1$months[$m1], $d2$months[$m2], $y2);
  488.         }
  489.         // pt
  490.         return sprintf('%s de %s a %s de %s %s'$d1$months[$m1], $d2$months[$m2], $y2);
  491.     }
  492. }