Overview

Namespaces

  • GAubry
    • ErrorHandler
    • Helpers
    • Logger
    • Shell
  • Himedia
    • Padocc
      • DB
      • Minifier
      • Numbering
      • Properties
      • Task
        • Base
        • Extended
  • None
  • Psr
    • Log

Classes

  • Factory
  • JSMinAdapter

Interfaces

  • MinifierInterface
  • Overview
  • Namespace
  • Class
  • Tree
  • Deprecated
  • Todo
  1: <?php
  2: 
  3: namespace Himedia\Padocc\Minifier;
  4: 
  5: use GAubry\Shell\ShellAdapter;
  6: 
  7: /**
  8:  * Compresser les fichiers JS et CSS.
  9:  *
 10:  * @author Geoffroy AUBRY <gaubry@hi-media.com>
 11:  */
 12: class JSMinAdapter implements MinifierInterface
 13: {
 14: 
 15:     /**
 16:      * Shell adapter.
 17:      *
 18:      * @var ShellAdapter
 19:      * @see minifyJS()
 20:      */
 21:     private $oShell;
 22: 
 23:     /**
 24:      * Chemin du binaire JSMin
 25:      *
 26:      * @var string
 27:      * @see minifyJS()
 28:      */
 29:     private $sBinPath;
 30: 
 31:     /**
 32:      * Constructeur.
 33:      *
 34:      * @param string $sJSMinBinPath chemin du binaire JSMin
 35:      * @param ShellAdapter $oShell instance utilisée pour exécuter le binaire jsmin
 36:      */
 37:     public function __construct ($sJSMinBinPath, ShellAdapter $oShell)
 38:     {
 39:         $this->sBinPath = $sJSMinBinPath;
 40:         $this->oShell = $oShell;
 41:     }
 42: 
 43:     /**
 44:      * Minifie la liste de fichiers JS ou CSS spécifiée et enregistre le résultat dans $sDestPath.
 45:      *
 46:      * @param array $aSrcPaths liste de fichiers se finissant tous par '.js', ou tous par '.css'
 47:      * @param string $sDestPath chemin/fichier dans lequel enregistrer le résultat du minify
 48:      * @return MinifierInterface $this
 49:      * @throws \BadMethodCallException si $aSrcPaths vide
 50:      * @throws \UnexpectedValueException si les sources n'ont pas toutes la même extension de fichier
 51:      * @throws \UnexpectedValueException si la destination est un CSS quand les sources sont des JS ou inversement
 52:      * @throws \DomainException si des fichiers ne se terminent ni par '.js', ni par '.css'
 53:      */
 54:     public function minify (array $aSrcPaths, $sDestPath)
 55:     {
 56:         if (count($aSrcPaths) === 0) {
 57:             throw new \BadMethodCallException('Source files missing!');
 58:         }
 59: 
 60:         // Est-ce que les fichiers en entrée sont tous des JS ou tous des CSS ?
 61:         $sFirstExtension = strrchr(reset($aSrcPaths), '.');
 62:         foreach ($aSrcPaths as $sSrcPath) {
 63:             $sExtension = strrchr($sSrcPath, '.');
 64:             if ($sExtension !== $sFirstExtension) {
 65:                 throw new \UnexpectedValueException('All files must be either JS or CSS: ' . print_r($aSrcPaths, true));
 66:             }
 67:         }
 68: 
 69:         // La destination est-elle en accord avec les entrées ?
 70:         if (strrchr($sDestPath, '.') !== $sFirstExtension) {
 71:             $sMsg = "Destination file must be same type of input files: '$sDestPath' : Src :"
 72:                   . print_r($aSrcPaths, true);
 73:             throw new \UnexpectedValueException($sMsg);
 74:         }
 75: 
 76:         // On redirige vers le service idoine :
 77:         switch ($sFirstExtension) {
 78:             case '.js':
 79:                 $this->minifyJS($aSrcPaths, $sDestPath);
 80:                 break;
 81: 
 82:             case '.css':
 83:                 $this->minifyCSS($aSrcPaths, $sDestPath);
 84:                 break;
 85: 
 86:             default:
 87:                 $sMsg = "All specified paths must finish either by '.js' or '.css': '$sFirstExtension'!";
 88:                 throw new \DomainException($sMsg);
 89:                 break;
 90:         }
 91: 
 92:         return $this;
 93:     }
 94: 
 95:     /**
 96:      * Minifie la liste des fichiers JS spécifiée et enregistre le résultat dans $sDestPath.
 97:      *
 98:      * @param array $aSrcPaths liste de fichiers se finissant tous par '.js'
 99:      * @param string $sDestPath chemin/fichier dans lequel enregistrer le résultat du minify
100:      * @throws \RuntimeException en cas d'erreur shell
101:      */
102:     protected function minifyJS (array $aSrcPaths, $sDestPath)
103:     {
104:         $sHeader = $this->getHeader($aSrcPaths);
105:         $sCmd = 'cat';
106:         foreach ($aSrcPaths as $sSrcPath) {
107:             $sCmd .= ' ' . $this->oShell->escapePath($sSrcPath);
108:         }
109:         $sCmd .= " | $this->sBinPath >'$sDestPath' && sed --in-place '1i$sHeader' '$sDestPath'";
110:         $this->oShell->exec($sCmd);
111:     }
112: 
113:     /**
114:      * Minifie la liste des fichiers CSS spécifiée et enregistre le résultat dans $sDestPath.
115:      *
116:      * @param array $aSrcPaths liste de fichiers se finissant tous par '.css'
117:      * @param string $sDestPath chemin/fichier dans lequel enregistrer le résultat du minify
118:      * @throws \RuntimeException si l'un des fichiers est introuvable
119:      */
120:     protected function minifyCSS (array $aSrcPaths, $sDestPath)
121:     {
122:         $sContent = $this->getContent($aSrcPaths);
123: 
124:         // remove comments:
125:         $sContent = preg_replace('#/\*[^*]*\*+([^/][^*]*\*+)*/#', '', $sContent);
126: 
127:         // remove tabs, spaces, newlines, etc.
128:         $sContent = str_replace(array("\r" , "\n" , "\t"), '', $sContent);
129:         $sContent = str_replace(array('    ' , '   ' , '  '), ' ', $sContent);
130: 
131:         $sContent = $this->getHeader($aSrcPaths) . $sContent;
132:         file_put_contents($sDestPath, $sContent);
133:     }
134: 
135:     /**
136:      * Retourne une ligne de commentaire, à insérer en 1re ligne d'un fichier CSS ou JS minifié,
137:      * énumérant tous les fichiers sources le constituant.
138:      *
139:      * Par exemple :
140:      * "/* Contains: /home/resources/a.css *[slash]\n"
141:      * "/* Contains (basedir='/path/to/resources/'): a.txt, b.txt *[slash]\n"
142:      *
143:      * @param array $aSrcPaths liste de fichiers sources
144:      * @return string une ligne de commentaire, à insérer en 1re ligne d'un fichier CSS ou JS minifié,
145:      * énumérant tous les fichiers sources le constituant.
146:      */
147:     private function getHeader (array $aSrcPaths)
148:     {
149:         if (count($aSrcPaths) === 1) {
150:             $sHeader = "/* Contains: " . reset($aSrcPaths) . ' */' . "\n";
151:         } else {
152:             $sCommonPrefix = $this->getLargestCommonPrefix($aSrcPaths);
153:             $iPrefixLength = strlen($sCommonPrefix);
154:             $aShortPaths = array();
155:             foreach ($aSrcPaths as $sSrcPath) {
156:                 $aShortPaths[] = substr($sSrcPath, $iPrefixLength);
157:             }
158:             $sHeader = "/* Contains (basedir='$sCommonPrefix'): " . implode(', ', $aShortPaths) . ' */' . "\n";
159:         }
160:         return $sHeader;
161:     }
162: 
163:     /**
164:      * Retourne le plus long préfixe commun aux chaînes fournies.
165:      *
166:      * @param array $aStrings liste de chaînes à comparer
167:      * @return string le plus long préfixe commun aux chaînes fournies.
168:      * @see http://stackoverflow.com/questions/1336207/finding-common-prefix-of-array-of-strings/1336357#1336357
169:      */
170:     private function getLargestCommonPrefix (array $aStrings)
171:     {
172:         // take the first item as initial prefix:
173:         $sPrefix = array_shift($aStrings);
174:         $iLength = strlen($sPrefix);
175: 
176:         // compare the current prefix with the prefix of the same length of the other items
177:         foreach ($aStrings as $sItem) {
178: 
179:             // check if there is a match; if not, decrease the prefix by one character at a time
180:             while ($iLength > 0 && substr($sItem, 0, $iLength) !== $sPrefix) {
181:                 $iLength--;
182:                 $sPrefix = substr($sPrefix, 0, -1);
183:             }
184: 
185:             if ($iLength === 0) {
186:                 break;
187:             }
188:         }
189: 
190:         return $sPrefix;
191:     }
192: 
193:     /**
194:      * Retourne la concaténation du contenu des fichiers spécifiés.
195:      *
196:      * @param array $aSrcPaths liste de chemins dont on veut concaténer le contenu
197:      * @return string la concaténation du contenu des fichiers spécifiés.
198:      * @throws \RuntimeException si l'un des fichiers est introuvable
199:      * @see minifyCSS()
200:      */
201:     private function getContent (array $aSrcPaths)
202:     {
203:         $aExpandedPaths = array();
204:         foreach ($aSrcPaths as $sPath) {
205:             if (strpos($sPath, '*') !== false || strpos($sPath, '?') !== false) {
206:                 $aExpandedPaths = array_merge($aExpandedPaths, glob($sPath));
207:             } else {
208:                 $aExpandedPaths[] = $sPath;
209:             }
210:         }
211: 
212:         $sContent = '';
213:         foreach ($aExpandedPaths as $sPath) {
214:             try {
215:                 $sContent .= file_get_contents($sPath);
216:             } catch (\Exception $oException) {
217:                 throw new \RuntimeException("File not found: '$sPath'!", 1, $oException);
218:             }
219:         }
220:         return $sContent;
221:     }
222: }
223: 
Platform for Automatized Deployments with pOwerful Concise Configuration API documentation generated by ApiGen 2.8.0