Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like Requirements_Backend often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Requirements_Backend, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
397 | class Requirements_Backend { |
||
398 | |||
399 | /** |
||
400 | * Whether to add caching query params to the requests for file-based requirements. |
||
401 | * Eg: themes/myTheme/js/main.js?m=123456789. The parameter is a timestamp generated by |
||
402 | * filemtime. This has the benefit of allowing the browser to cache the URL infinitely, |
||
403 | * while automatically busting this cache every time the file is changed. |
||
404 | * |
||
405 | * @var bool |
||
406 | */ |
||
407 | protected $suffix_requirements = true; |
||
408 | |||
409 | /** |
||
410 | * Whether to combine CSS and JavaScript files |
||
411 | * |
||
412 | * @var bool |
||
413 | */ |
||
414 | protected $combined_files_enabled = true; |
||
415 | |||
416 | /** |
||
417 | * Paths to all required JavaScript files relative to docroot |
||
418 | * |
||
419 | * @var array $javascript |
||
420 | */ |
||
421 | protected $javascript = array(); |
||
422 | |||
423 | /** |
||
424 | * Paths to all required CSS files relative to the docroot. |
||
425 | * |
||
426 | * @var array $css |
||
427 | */ |
||
428 | protected $css = array(); |
||
429 | |||
430 | /** |
||
431 | * All custom javascript code that is inserted into the page's HTML |
||
432 | * |
||
433 | * @var array $customScript |
||
434 | */ |
||
435 | protected $customScript = array(); |
||
436 | |||
437 | /** |
||
438 | * All custom CSS rules which are inserted directly at the bottom of the HTML <head> tag |
||
439 | * |
||
440 | * @var array $customCSS |
||
441 | */ |
||
442 | protected $customCSS = array(); |
||
443 | |||
444 | /** |
||
445 | * All custom HTML markup which is added before the closing <head> tag, e.g. additional |
||
446 | * metatags. |
||
447 | */ |
||
448 | protected $customHeadTags = array(); |
||
449 | |||
450 | /** |
||
451 | * Remembers the file paths or uniquenessIDs of all Requirements cleared through |
||
452 | * {@link clear()}, so that they can be restored later. |
||
453 | * |
||
454 | * @var array $disabled |
||
455 | */ |
||
456 | protected $disabled = array(); |
||
457 | |||
458 | /** |
||
459 | * The file paths (relative to docroot) or uniquenessIDs of any included requirements which |
||
460 | * should be blocked when executing {@link inlcudeInHTML()}. This is useful, for example, |
||
461 | * to block scripts included by a superclass without having to override entire functions and |
||
462 | * duplicate a lot of code. |
||
463 | * |
||
464 | * Use {@link unblock()} or {@link unblock_all()} to revert changes. |
||
465 | * |
||
466 | * @var array $blocked |
||
467 | */ |
||
468 | protected $blocked = array(); |
||
469 | |||
470 | /** |
||
471 | * A list of combined files registered via {@link combine_files()}. Keys are the output file |
||
472 | * names, values are lists of input files. |
||
473 | * |
||
474 | * @var array $combine_files |
||
475 | */ |
||
476 | public $combine_files = array(); |
||
477 | |||
478 | /** |
||
479 | * Use the JSMin library to minify any javascript file passed to {@link combine_files()}. |
||
480 | * |
||
481 | * @var bool |
||
482 | */ |
||
483 | public $combine_js_with_jsmin = true; |
||
484 | |||
485 | /** |
||
486 | * Whether or not file headers should be written when combining files |
||
487 | * |
||
488 | * @var boolean |
||
489 | */ |
||
490 | public $write_header_comment = true; |
||
491 | |||
492 | /** |
||
493 | * Where to save combined files. By default they're placed in assets/_combinedfiles, however |
||
494 | * this may be an issue depending on your setup, especially for CSS files which often contain |
||
495 | * relative paths. |
||
496 | * |
||
497 | * @var string |
||
498 | */ |
||
499 | protected $combinedFilesFolder = null; |
||
500 | |||
501 | /** |
||
502 | * Put all JavaScript includes at the bottom of the template before the closing <body> tag, |
||
503 | * rather than the default behaviour of placing them at the end of the <head> tag. This means |
||
504 | * script downloads won't block other HTTP requests, which can be a performance improvement. |
||
505 | * |
||
506 | * @var bool |
||
507 | */ |
||
508 | public $write_js_to_body = true; |
||
509 | |||
510 | /** |
||
511 | * Force the JavaScript to the bottom of the page, even if there's a script tag in the body already |
||
512 | * |
||
513 | * @var boolean |
||
514 | */ |
||
515 | protected $force_js_to_bottom = false; |
||
516 | |||
517 | /** |
||
518 | * Enable or disable the combination of CSS and JavaScript files |
||
519 | * |
||
520 | * @param $enable |
||
521 | */ |
||
522 | public function set_combined_files_enabled($enable) { |
||
525 | |||
526 | /** |
||
527 | * Check whether file combination is enabled. |
||
528 | * |
||
529 | * @return bool |
||
530 | */ |
||
531 | public function get_combined_files_enabled() { |
||
534 | |||
535 | /** |
||
536 | * Set the folder to save combined files in. By default they're placed in assets/_combinedfiles, |
||
537 | * however this may be an issue depending on your setup, especially for CSS files which often |
||
538 | * contain relative paths. |
||
539 | * |
||
540 | * @param string $folder |
||
541 | */ |
||
542 | public function setCombinedFilesFolder($folder) { |
||
545 | |||
546 | /** |
||
547 | * @return string Folder relative to the webroot |
||
548 | */ |
||
549 | public function getCombinedFilesFolder() { |
||
552 | |||
553 | /** |
||
554 | * Set whether to add caching query params to the requests for file-based requirements. |
||
555 | * Eg: themes/myTheme/js/main.js?m=123456789. The parameter is a timestamp generated by |
||
556 | * filemtime. This has the benefit of allowing the browser to cache the URL infinitely, |
||
557 | * while automatically busting this cache every time the file is changed. |
||
558 | * |
||
559 | * @param bool |
||
560 | */ |
||
561 | public function set_suffix_requirements($var) { |
||
564 | |||
565 | /** |
||
566 | * Check whether we want to suffix requirements |
||
567 | * |
||
568 | * @return bool |
||
569 | */ |
||
570 | public function get_suffix_requirements() { |
||
573 | |||
574 | /** |
||
575 | * Set whether you want to write the JS to the body of the page rather than at the end of the |
||
576 | * head tag. |
||
577 | * |
||
578 | * @param bool |
||
579 | */ |
||
580 | public function set_write_js_to_body($var) { |
||
583 | |||
584 | /** |
||
585 | * Forces the JavaScript requirements to the end of the body, right before the closing tag |
||
586 | * |
||
587 | * @param bool |
||
588 | */ |
||
589 | public function set_force_js_to_bottom($var) { |
||
592 | |||
593 | /** |
||
594 | * Register the given JavaScript file as required. |
||
595 | * |
||
596 | * @param string $file Relative to docroot |
||
597 | */ |
||
598 | public function javascript($file) { |
||
601 | |||
602 | /** |
||
603 | * Returns an array of all required JavaScript |
||
604 | * |
||
605 | * @return array |
||
606 | */ |
||
607 | public function get_javascript() { |
||
610 | |||
611 | /** |
||
612 | * Register the given JavaScript code into the list of requirements |
||
613 | * |
||
614 | * @param string $script The script content as a string (without enclosing <script> tag) |
||
615 | * @param string|int $uniquenessID A unique ID that ensures a piece of code is only added once |
||
616 | */ |
||
617 | public function customScript($script, $uniquenessID = null) { |
||
618 | if($uniquenessID) $this->customScript[$uniquenessID] = $script; |
||
619 | else $this->customScript[] = $script; |
||
620 | |||
621 | $script .= "\n"; |
||
622 | } |
||
623 | |||
624 | /** |
||
625 | * Return all registered custom scripts |
||
626 | * |
||
627 | * @return array |
||
628 | */ |
||
629 | public function get_custom_scripts() { |
||
630 | $requirements = ""; |
||
631 | |||
632 | if($this->customScript) { |
||
|
|||
633 | foreach($this->customScript as $script) { |
||
634 | $requirements .= "$script\n"; |
||
635 | } |
||
636 | } |
||
637 | |||
638 | return $requirements; |
||
639 | } |
||
640 | |||
641 | /** |
||
642 | * Register the given CSS styles into the list of requirements |
||
643 | * |
||
644 | * @param string $script CSS selectors as a string (without enclosing <style> tag) |
||
645 | * @param string|int $uniquenessID A unique ID that ensures a piece of code is only added once |
||
646 | */ |
||
647 | public function customCSS($script, $uniquenessID = null) { |
||
651 | |||
652 | /** |
||
653 | * Add the following custom HTML code to the <head> section of the page |
||
654 | * |
||
655 | * @param string $html Custom HTML code |
||
656 | * @param string|int $uniquenessID A unique ID that ensures a piece of code is only added once |
||
657 | */ |
||
658 | public function insertHeadTags($html, $uniquenessID = null) { |
||
662 | |||
663 | /** |
||
664 | * Include the content of the given JavaScript file in the list of requirements. Dollar-sign |
||
665 | * variables will be interpolated with values from $vars similar to a .ss template. |
||
666 | * |
||
667 | * @param string $file The template file to load, relative to docroot |
||
668 | * @param string[]|int[] $vars The array of variables to interpolate. |
||
669 | * @param string|int $uniquenessID A unique ID that ensures a piece of code is only added once |
||
670 | */ |
||
671 | public function javascriptTemplate($file, $vars, $uniquenessID = null) { |
||
672 | $script = file_get_contents(Director::getAbsFile($file)); |
||
673 | $search = array(); |
||
674 | $replace = array(); |
||
675 | |||
676 | if($vars) foreach($vars as $k => $v) { |
||
677 | $search[] = '$' . $k; |
||
678 | $replace[] = str_replace("\\'","'", Convert::raw2js($v)); |
||
679 | } |
||
680 | |||
681 | $script = str_replace($search, $replace, $script); |
||
682 | $this->customScript($script, $uniquenessID); |
||
683 | } |
||
684 | |||
685 | /** |
||
686 | * Register the given stylesheet into the list of requirements. |
||
687 | * |
||
688 | * @param string $file The CSS file to load, relative to site root |
||
689 | * @param string $media Comma-separated list of media types to use in the link tag |
||
690 | * (e.g. 'screen,projector') |
||
691 | */ |
||
692 | public function css($file, $media = null) { |
||
693 | $this->css[$file] = array( |
||
694 | "media" => $media |
||
695 | ); |
||
696 | } |
||
697 | |||
698 | /** |
||
699 | * Get the list of registered CSS file requirements, excluding blocked files |
||
700 | * |
||
701 | * @return array |
||
702 | */ |
||
703 | public function get_css() { |
||
706 | |||
707 | /** |
||
708 | * Clear either a single or all requirements |
||
709 | * |
||
710 | * Caution: Clearing single rules added via customCSS and customScript only works if you |
||
711 | * originally specified a $uniquenessID. |
||
712 | * |
||
713 | * @param string|int $fileOrID |
||
714 | */ |
||
715 | public function clear($fileOrID = null) { |
||
716 | if($fileOrID) { |
||
717 | foreach(array('javascript','css', 'customScript', 'customCSS', 'customHeadTags') as $type) { |
||
718 | if(isset($this->{$type}[$fileOrID])) { |
||
719 | $this->disabled[$type][$fileOrID] = $this->{$type}[$fileOrID]; |
||
720 | unset($this->{$type}[$fileOrID]); |
||
721 | } |
||
722 | } |
||
723 | } else { |
||
724 | $this->disabled['javascript'] = $this->javascript; |
||
725 | $this->disabled['css'] = $this->css; |
||
726 | $this->disabled['customScript'] = $this->customScript; |
||
727 | $this->disabled['customCSS'] = $this->customCSS; |
||
728 | $this->disabled['customHeadTags'] = $this->customHeadTags; |
||
729 | |||
730 | $this->javascript = array(); |
||
731 | $this->css = array(); |
||
732 | $this->customScript = array(); |
||
733 | $this->customCSS = array(); |
||
734 | $this->customHeadTags = array(); |
||
735 | } |
||
736 | } |
||
737 | |||
738 | /** |
||
739 | * Restore requirements cleared by call to Requirements::clear |
||
740 | */ |
||
741 | public function restore() { |
||
742 | $this->javascript = $this->disabled['javascript']; |
||
743 | $this->css = $this->disabled['css']; |
||
744 | $this->customScript = $this->disabled['customScript']; |
||
745 | $this->customCSS = $this->disabled['customCSS']; |
||
746 | $this->customHeadTags = $this->disabled['customHeadTags']; |
||
747 | } |
||
748 | /** |
||
749 | * Block inclusion of a specific file |
||
750 | * |
||
751 | * The difference between this and {@link clear} is that the calling order does not matter; |
||
752 | * {@link clear} must be called after the initial registration, whereas {@link block} can be |
||
753 | * used in advance. This is useful, for example, to block scripts included by a superclass |
||
754 | * without having to override entire functions and duplicate a lot of code. |
||
755 | * |
||
756 | * Note that blocking should be used sparingly because it's hard to trace where an file is |
||
757 | * being blocked from. |
||
758 | * |
||
759 | * @param string|int $fileOrID |
||
760 | */ |
||
761 | public function block($fileOrID) { |
||
764 | |||
765 | /** |
||
766 | * Remove an item from the block list |
||
767 | * |
||
768 | * @param string|int $fileOrID |
||
769 | */ |
||
770 | public function unblock($fileOrID) { |
||
773 | |||
774 | /** |
||
775 | * Removes all items from the block list |
||
776 | */ |
||
777 | public function unblock_all() { |
||
780 | |||
781 | /** |
||
782 | * Update the given HTML content with the appropriate include tags for the registered |
||
783 | * requirements. Needs to receive a valid HTML/XHTML template in the $content parameter, |
||
784 | * including a head and body tag. |
||
785 | * |
||
786 | * @param string $templateFile No longer used, only retained for compatibility |
||
787 | * @param string $content HTML content that has already been parsed from the $templateFile |
||
788 | * through {@link SSViewer} |
||
789 | * @return string HTML content augmented with the requirements tags |
||
790 | */ |
||
791 | public function includeInHTML($templateFile, $content) { |
||
792 | if( |
||
793 | (strpos($content, '</head>') !== false || strpos($content, '</head ') !== false) |
||
794 | && ($this->css || $this->javascript || $this->customCSS || $this->customScript || $this->customHeadTags) |
||
795 | ) { |
||
796 | $requirements = ''; |
||
797 | $jsRequirements = ''; |
||
798 | |||
799 | // Combine files - updates $this->javascript and $this->css |
||
800 | $this->process_combined_files(); |
||
801 | |||
802 | View Code Duplication | foreach(array_diff_key($this->javascript,$this->blocked) as $file => $dummy) { |
|
803 | $path = Convert::raw2xml($this->path_for_file($file)); |
||
804 | if($path) { |
||
805 | $jsRequirements .= "<script type=\"text/javascript\" src=\"$path\"></script>\n"; |
||
806 | } |
||
807 | } |
||
808 | |||
809 | // Add all inline JavaScript *after* including external files they might rely on |
||
810 | if($this->customScript) { |
||
811 | foreach(array_diff_key($this->customScript,$this->blocked) as $script) { |
||
812 | $jsRequirements .= "<script type=\"text/javascript\">\n//<![CDATA[\n"; |
||
813 | $jsRequirements .= "$script\n"; |
||
814 | $jsRequirements .= "\n//]]>\n</script>\n"; |
||
815 | } |
||
816 | } |
||
817 | |||
818 | View Code Duplication | foreach(array_diff_key($this->css,$this->blocked) as $file => $params) { |
|
819 | $path = Convert::raw2xml($this->path_for_file($file)); |
||
820 | if($path) { |
||
821 | $media = (isset($params['media']) && !empty($params['media'])) |
||
822 | ? " media=\"{$params['media']}\"" : ""; |
||
823 | $requirements .= "<link rel=\"stylesheet\" type=\"text/css\"{$media} href=\"$path\" />\n"; |
||
824 | } |
||
825 | } |
||
826 | |||
827 | foreach(array_diff_key($this->customCSS, $this->blocked) as $css) { |
||
828 | $requirements .= "<style type=\"text/css\">\n$css\n</style>\n"; |
||
829 | } |
||
830 | |||
831 | foreach(array_diff_key($this->customHeadTags,$this->blocked) as $customHeadTag) { |
||
832 | $requirements .= "$customHeadTag\n"; |
||
833 | } |
||
834 | |||
835 | if ($this->force_js_to_bottom) { |
||
836 | // Remove all newlines from code to preserve layout |
||
837 | $jsRequirements = preg_replace('/>\n*/', '>', $jsRequirements); |
||
838 | |||
839 | // Forcefully put the scripts at the bottom of the body instead of before the first |
||
840 | // script tag. |
||
841 | $content = preg_replace("/(<\/body[^>]*>)/i", $jsRequirements . "\\1", $content); |
||
842 | |||
843 | // Put CSS at the bottom of the head |
||
844 | $content = preg_replace("/(<\/head>)/i", $requirements . "\\1", $content); |
||
845 | } elseif($this->write_js_to_body) { |
||
846 | // Remove all newlines from code to preserve layout |
||
847 | $jsRequirements = preg_replace('/>\n*/', '>', $jsRequirements); |
||
848 | |||
849 | // If your template already has script tags in the body, then we try to put our script |
||
850 | // tags just before those. Otherwise, we put it at the bottom. |
||
851 | $p2 = stripos($content, '<body'); |
||
852 | $p1 = stripos($content, '<script', $p2); |
||
853 | |||
854 | $commentTags = array(); |
||
855 | $canWriteToBody = ($p1 !== false) |
||
856 | && |
||
857 | // Check that the script tag is not inside a html comment tag |
||
858 | !( |
||
859 | preg_match('/.*(?|(<!--)|(-->))/U', $content, $commentTags, 0, $p1) |
||
860 | && |
||
861 | $commentTags[1] == '-->' |
||
862 | ); |
||
863 | |||
864 | if($canWriteToBody) { |
||
865 | $content = substr($content,0,$p1) . $jsRequirements . substr($content,$p1); |
||
866 | } else { |
||
867 | $content = preg_replace("/(<\/body[^>]*>)/i", $jsRequirements . "\\1", $content); |
||
868 | } |
||
869 | |||
870 | // Put CSS at the bottom of the head |
||
871 | $content = preg_replace("/(<\/head>)/i", $requirements . "\\1", $content); |
||
872 | } else { |
||
873 | $content = preg_replace("/(<\/head>)/i", $requirements . "\\1", $content); |
||
874 | $content = preg_replace("/(<\/head>)/i", $jsRequirements . "\\1", $content); |
||
875 | } |
||
876 | } |
||
877 | |||
878 | return $content; |
||
879 | } |
||
880 | |||
881 | /** |
||
882 | * Attach requirements inclusion to X-Include-JS and X-Include-CSS headers on the given |
||
883 | * HTTP Response |
||
884 | * |
||
885 | * @param SS_HTTPResponse $response |
||
886 | */ |
||
887 | public function include_in_response(SS_HTTPResponse $response) { |
||
888 | $this->process_combined_files(); |
||
889 | $jsRequirements = array(); |
||
890 | $cssRequirements = array(); |
||
891 | |||
892 | View Code Duplication | foreach(array_diff_key($this->javascript, $this->blocked) as $file => $dummy) { |
|
893 | $path = $this->path_for_file($file); |
||
894 | if($path) { |
||
895 | $jsRequirements[] = str_replace(',', '%2C', $path); |
||
896 | } |
||
897 | } |
||
898 | |||
899 | $response->addHeader('X-Include-JS', implode(',', $jsRequirements)); |
||
900 | |||
901 | View Code Duplication | foreach(array_diff_key($this->css,$this->blocked) as $file => $params) { |
|
902 | $path = $this->path_for_file($file); |
||
903 | if($path) { |
||
904 | $path = str_replace(',', '%2C', $path); |
||
905 | $cssRequirements[] = isset($params['media']) ? "$path:##:$params[media]" : $path; |
||
906 | } |
||
907 | } |
||
908 | |||
909 | $response->addHeader('X-Include-CSS', implode(',', $cssRequirements)); |
||
910 | } |
||
911 | |||
912 | /** |
||
913 | * Add i18n files from the given javascript directory. SilverStripe expects that the given |
||
914 | * directory will contain a number of JavaScript files named by language: en_US.js, de_DE.js, |
||
915 | * etc. |
||
916 | * |
||
917 | * @param string $langDir The JavaScript lang directory, relative to the site root, e.g., |
||
918 | * 'framework/javascript/lang' |
||
919 | * @param bool $return Return all relative file paths rather than including them in |
||
920 | * requirements |
||
921 | * @param bool $langOnly Only include language files, not the base libraries |
||
922 | * |
||
923 | * @return array |
||
924 | */ |
||
925 | public function add_i18n_javascript($langDir, $return = false, $langOnly = false) { |
||
926 | $files = array(); |
||
927 | $base = Director::baseFolder() . '/'; |
||
928 | if(i18n::config()->js_i18n) { |
||
929 | // Include i18n.js even if no languages are found. The fact that |
||
930 | // add_i18n_javascript() was called indicates that the methods in |
||
931 | // here are needed. |
||
932 | if(!$langOnly) $files[] = FRAMEWORK_DIR . '/javascript/i18n.js'; |
||
933 | |||
934 | if(substr($langDir,-1) != '/') $langDir .= '/'; |
||
935 | |||
936 | $candidates = array( |
||
937 | 'en.js', |
||
938 | 'en_US.js', |
||
939 | i18n::get_lang_from_locale(i18n::default_locale()) . '.js', |
||
940 | i18n::default_locale() . '.js', |
||
941 | i18n::get_lang_from_locale(i18n::get_locale()) . '.js', |
||
942 | i18n::get_locale() . '.js', |
||
943 | ); |
||
944 | foreach($candidates as $candidate) { |
||
945 | if(file_exists($base . DIRECTORY_SEPARATOR . $langDir . $candidate)) { |
||
946 | $files[] = $langDir . $candidate; |
||
947 | } |
||
948 | } |
||
949 | } else { |
||
950 | // Stub i18n implementation for when i18n is disabled. |
||
951 | if(!$langOnly) $files[] = FRAMEWORK_DIR . '/javascript/i18nx.js'; |
||
952 | } |
||
953 | |||
954 | if($return) { |
||
955 | return $files; |
||
956 | } else { |
||
957 | foreach($files as $file) $this->javascript($file); |
||
958 | } |
||
959 | } |
||
960 | |||
961 | /** |
||
962 | * Finds the path for specified file |
||
963 | * |
||
964 | * @param string $fileOrUrl |
||
965 | * @return string|bool |
||
966 | */ |
||
967 | protected function path_for_file($fileOrUrl) { |
||
968 | if(preg_match('{^//|http[s]?}', $fileOrUrl)) { |
||
969 | return $fileOrUrl; |
||
970 | } elseif(Director::fileExists($fileOrUrl)) { |
||
971 | $filePath = preg_replace('/\?.*/', '', Director::baseFolder() . '/' . $fileOrUrl); |
||
972 | $prefix = Director::baseURL(); |
||
973 | $mtimesuffix = ""; |
||
974 | $suffix = ''; |
||
975 | if($this->suffix_requirements) { |
||
976 | $mtimesuffix = "?m=" . filemtime($filePath); |
||
977 | $suffix = '&'; |
||
978 | } |
||
979 | if(strpos($fileOrUrl, '?') !== false) { |
||
980 | if (strlen($suffix) == 0) { |
||
981 | $suffix = '?'; |
||
982 | } |
||
983 | $suffix .= substr($fileOrUrl, strpos($fileOrUrl, '?')+1); |
||
984 | $fileOrUrl = substr($fileOrUrl, 0, strpos($fileOrUrl, '?')); |
||
985 | } else { |
||
986 | $suffix = ''; |
||
987 | } |
||
988 | return "{$prefix}{$fileOrUrl}{$mtimesuffix}{$suffix}"; |
||
989 | } else { |
||
990 | return false; |
||
991 | } |
||
992 | } |
||
993 | |||
994 | /** |
||
995 | * Concatenate several css or javascript files into a single dynamically generated file. This |
||
996 | * increases performance by fewer HTTP requests. |
||
997 | * |
||
998 | * The combined file is regenerated based on every file modification time. Optionally a |
||
999 | * rebuild can be triggered by appending ?flush=1 to the URL. If all files to be combined are |
||
1000 | * JavaScript, we use the external JSMin library to minify the JavaScript. This can be |
||
1001 | * controlled using {@link $combine_js_with_jsmin}. |
||
1002 | * |
||
1003 | * All combined files will have a comment on the start of each concatenated file denoting their |
||
1004 | * original position. For easier debugging, we only minify JavaScript if not in development |
||
1005 | * mode ({@link Director::isDev()}). |
||
1006 | * |
||
1007 | * CAUTION: You're responsible for ensuring that the load order for combined files is |
||
1008 | * retained - otherwise combining JavaScript files can lead to functional errors in the |
||
1009 | * JavaScript logic, and combining CSS can lead to incorrect inheritance. You can also |
||
1010 | * only include each file once across all includes and combinations in a single page load. |
||
1011 | * |
||
1012 | * CAUTION: Combining CSS Files discards any "media" information. |
||
1013 | * |
||
1014 | * Example for combined JavaScript: |
||
1015 | * <code> |
||
1016 | * Requirements::combine_files( |
||
1017 | * 'foobar.js', |
||
1018 | * array( |
||
1019 | * 'mysite/javascript/foo.js', |
||
1020 | * 'mysite/javascript/bar.js', |
||
1021 | * ) |
||
1022 | * ); |
||
1023 | * </code> |
||
1024 | * |
||
1025 | * Example for combined CSS: |
||
1026 | * <code> |
||
1027 | * Requirements::combine_files( |
||
1028 | * 'foobar.css', |
||
1029 | * array( |
||
1030 | * 'mysite/javascript/foo.css', |
||
1031 | * 'mysite/javascript/bar.css', |
||
1032 | * ) |
||
1033 | * ); |
||
1034 | * </code> |
||
1035 | * |
||
1036 | * @param string $combinedFileName Filename of the combined file relative to docroot |
||
1037 | * @param array $files Array of filenames relative to docroot |
||
1038 | * @param string $media |
||
1039 | * |
||
1040 | * @return bool|void |
||
1041 | */ |
||
1042 | public function combine_files($combinedFileName, $files, $media = null) { |
||
1043 | // duplicate check |
||
1044 | foreach($this->combine_files as $_combinedFileName => $_files) { |
||
1045 | $duplicates = array_intersect($_files, $files); |
||
1046 | if($duplicates && $combinedFileName != $_combinedFileName) { |
||
1047 | user_error("Requirements_Backend::combine_files(): Already included files " . implode(',', $duplicates) |
||
1048 | . " in combined file '{$_combinedFileName}'", E_USER_NOTICE); |
||
1049 | return false; |
||
1050 | } |
||
1051 | } |
||
1052 | foreach($files as $index=>$file) { |
||
1053 | if(is_array($file)) { |
||
1054 | // Either associative array path=>path type=>type or numeric 0=>path 1=>type |
||
1055 | // Otherwise, assume path is the first item |
||
1056 | if (isset($file['type']) && in_array($file['type'], array('css', 'javascript', 'js'))) { |
||
1057 | View Code Duplication | switch ($file['type']) { |
|
1058 | case 'css': |
||
1059 | $this->css($file['path'], $media); |
||
1060 | break; |
||
1061 | default: |
||
1062 | $this->javascript($file['path']); |
||
1063 | break; |
||
1064 | } |
||
1065 | $files[$index] = $file['path']; |
||
1066 | } elseif (isset($file[1]) && in_array($file[1], array('css', 'javascript', 'js'))) { |
||
1067 | View Code Duplication | switch ($file[1]) { |
|
1068 | case 'css': |
||
1069 | $this->css($file[0], $media); |
||
1070 | break; |
||
1071 | default: |
||
1072 | $this->javascript($file[0]); |
||
1073 | break; |
||
1074 | } |
||
1075 | $files[$index] = $file[0]; |
||
1076 | } else { |
||
1077 | $file = array_shift($file); |
||
1078 | } |
||
1079 | } |
||
1080 | if (!is_array($file)) { |
||
1081 | if(substr($file, -2) == 'js') { |
||
1082 | $this->javascript($file); |
||
1083 | } elseif(substr($file, -3) == 'css') { |
||
1084 | $this->css($file, $media); |
||
1085 | } else { |
||
1086 | user_error("Requirements_Backend::combine_files(): Couldn't guess file type for file '$file', " |
||
1087 | . "please specify by passing using an array instead.", E_USER_NOTICE); |
||
1088 | } |
||
1089 | } |
||
1090 | } |
||
1091 | $this->combine_files[$combinedFileName] = $files; |
||
1092 | } |
||
1093 | |||
1094 | /** |
||
1095 | * Return all combined files; keys are the combined file names, values are lists of |
||
1096 | * files being combined. |
||
1097 | * |
||
1098 | * @return array |
||
1099 | */ |
||
1100 | public function get_combine_files() { |
||
1103 | |||
1104 | /** |
||
1105 | * Delete all dynamically generated combined files from the filesystem |
||
1106 | * |
||
1107 | * @param string $combinedFileName If left blank, all combined files are deleted. |
||
1108 | */ |
||
1109 | public function delete_combined_files($combinedFileName = null) { |
||
1110 | $combinedFiles = ($combinedFileName) ? array($combinedFileName => null) : $this->combine_files; |
||
1111 | $combinedFolder = ($this->getCombinedFilesFolder()) ? |
||
1112 | (Director::baseFolder() . '/' . $this->combinedFilesFolder) : Director::baseFolder(); |
||
1113 | foreach($combinedFiles as $combinedFile => $sourceItems) { |
||
1114 | $filePath = $combinedFolder . '/' . $combinedFile; |
||
1115 | if(file_exists($filePath)) { |
||
1116 | unlink($filePath); |
||
1117 | } |
||
1118 | } |
||
1119 | } |
||
1120 | |||
1121 | /** |
||
1122 | * Deletes all generated combined files in the configured combined files directory, |
||
1123 | * but doesn't delete the directory itself. |
||
1124 | */ |
||
1125 | public function delete_all_combined_files() { |
||
1126 | $combinedFolder = $this->getCombinedFilesFolder(); |
||
1127 | if(!$combinedFolder) return false; |
||
1128 | |||
1129 | $path = Director::baseFolder() . '/' . $combinedFolder; |
||
1130 | if(file_exists($path)) { |
||
1131 | Filesystem::removeFolder($path, true); |
||
1132 | } |
||
1133 | } |
||
1134 | |||
1135 | /** |
||
1136 | * Clear all registered CSS and JavaScript file combinations |
||
1137 | */ |
||
1138 | public function clear_combined_files() { |
||
1141 | |||
1142 | /** |
||
1143 | * Do the heavy lifting involved in combining (and, in the case of JavaScript minifying) the |
||
1144 | * combined files. |
||
1145 | */ |
||
1146 | public function process_combined_files() { |
||
1281 | |||
1282 | /** |
||
1283 | * Minify the given $content according to the file type indicated in $filename |
||
1284 | * |
||
1285 | * @param string $filename |
||
1286 | * @param string $content |
||
1287 | * @return string |
||
1288 | */ |
||
1289 | protected function minifyFile($filename, $content) { |
||
1301 | |||
1302 | /** |
||
1303 | * Registers the given themeable stylesheet as required. |
||
1304 | * |
||
1305 | * A CSS file in the current theme path name 'themename/css/$name.css' is first searched for, |
||
1306 | * and it that doesn't exist and the module parameter is set then a CSS file with that name in |
||
1307 | * the module is used. |
||
1308 | * |
||
1309 | * @param string $name The name of the file - eg '/css/File.css' would have the name 'File' |
||
1310 | * @param string $module The module to fall back to if the css file does not exist in the |
||
1311 | * current theme. |
||
1312 | * @param string $media Comma-separated list of media types to use in the link tag |
||
1313 | * (e.g. 'screen,projector') |
||
1314 | */ |
||
1315 | public function themedCSS($name, $module = null, $media = null) { |
||
1333 | |||
1334 | /** |
||
1335 | * Output debugging information. |
||
1336 | */ |
||
1337 | public function debug() { |
||
1345 | |||
1346 | } |
||
1347 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.