1<?php 2 3/* 4 * This file is part of the Assetic package, an OpenSky project. 5 * 6 * (c) 2010-2014 OpenSky Project Inc 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Assetic\Factory; 13 14use Assetic\Asset\AssetCollection; 15use Assetic\Asset\AssetCollectionInterface; 16use Assetic\Asset\AssetInterface; 17use Assetic\Asset\AssetReference; 18use Assetic\Asset\FileAsset; 19use Assetic\Asset\GlobAsset; 20use Assetic\Asset\HttpAsset; 21use Assetic\AssetManager; 22use Assetic\Factory\Worker\WorkerInterface; 23use Assetic\Filter\DependencyExtractorInterface; 24use Assetic\FilterManager; 25 26/** 27 * The asset factory creates asset objects. 28 * 29 * @author Kris Wallsmith <kris.wallsmith@gmail.com> 30 */ 31class AssetFactory 32{ 33 private $root; 34 private $debug; 35 private $output; 36 private $workers; 37 private $am; 38 private $fm; 39 40 /** 41 * Constructor. 42 * 43 * @param string $root The default root directory 44 * @param Boolean $debug Filters prefixed with a "?" will be omitted in debug mode 45 */ 46 public function __construct($root, $debug = false) 47 { 48 $this->root = rtrim($root, '/'); 49 $this->debug = $debug; 50 $this->output = 'assetic/*'; 51 $this->workers = array(); 52 } 53 54 /** 55 * Sets debug mode for the current factory. 56 * 57 * @param Boolean $debug Debug mode 58 */ 59 public function setDebug($debug) 60 { 61 $this->debug = $debug; 62 } 63 64 /** 65 * Checks if the factory is in debug mode. 66 * 67 * @return Boolean Debug mode 68 */ 69 public function isDebug() 70 { 71 return $this->debug; 72 } 73 74 /** 75 * Sets the default output string. 76 * 77 * @param string $output The default output string 78 */ 79 public function setDefaultOutput($output) 80 { 81 $this->output = $output; 82 } 83 84 /** 85 * Adds a factory worker. 86 * 87 * @param WorkerInterface $worker A worker 88 */ 89 public function addWorker(WorkerInterface $worker) 90 { 91 $this->workers[] = $worker; 92 } 93 94 /** 95 * Returns the current asset manager. 96 * 97 * @return AssetManager|null The asset manager 98 */ 99 public function getAssetManager() 100 { 101 return $this->am; 102 } 103 104 /** 105 * Sets the asset manager to use when creating asset references. 106 * 107 * @param AssetManager $am The asset manager 108 */ 109 public function setAssetManager(AssetManager $am) 110 { 111 $this->am = $am; 112 } 113 114 /** 115 * Returns the current filter manager. 116 * 117 * @return FilterManager|null The filter manager 118 */ 119 public function getFilterManager() 120 { 121 return $this->fm; 122 } 123 124 /** 125 * Sets the filter manager to use when adding filters. 126 * 127 * @param FilterManager $fm The filter manager 128 */ 129 public function setFilterManager(FilterManager $fm) 130 { 131 $this->fm = $fm; 132 } 133 134 /** 135 * Creates a new asset. 136 * 137 * Prefixing a filter name with a question mark will cause it to be 138 * omitted when the factory is in debug mode. 139 * 140 * Available options: 141 * 142 * * output: An output string 143 * * name: An asset name for interpolation in output patterns 144 * * debug: Forces debug mode on or off for this asset 145 * * root: An array or string of more root directories 146 * 147 * @param array|string $inputs An array of input strings 148 * @param array|string $filters An array of filter names 149 * @param array $options An array of options 150 * 151 * @return AssetCollection An asset collection 152 */ 153 public function createAsset($inputs = array(), $filters = array(), array $options = array()) 154 { 155 if (!is_array($inputs)) { 156 $inputs = array($inputs); 157 } 158 159 if (!is_array($filters)) { 160 $filters = array($filters); 161 } 162 163 if (!isset($options['output'])) { 164 $options['output'] = $this->output; 165 } 166 167 if (!isset($options['vars'])) { 168 $options['vars'] = array(); 169 } 170 171 if (!isset($options['debug'])) { 172 $options['debug'] = $this->debug; 173 } 174 175 if (!isset($options['root'])) { 176 $options['root'] = array($this->root); 177 } else { 178 if (!is_array($options['root'])) { 179 $options['root'] = array($options['root']); 180 } 181 182 $options['root'][] = $this->root; 183 } 184 185 if (!isset($options['name'])) { 186 $options['name'] = $this->generateAssetName($inputs, $filters, $options); 187 } 188 189 $asset = $this->createAssetCollection(array(), $options); 190 $extensions = array(); 191 192 // inner assets 193 foreach ($inputs as $input) { 194 if (is_array($input)) { 195 // nested formula 196 $asset->add(call_user_func_array(array($this, 'createAsset'), $input)); 197 } else { 198 $asset->add($this->parseInput($input, $options)); 199 $extensions[pathinfo($input, PATHINFO_EXTENSION)] = true; 200 } 201 } 202 203 // filters 204 foreach ($filters as $filter) { 205 if ('?' != $filter[0]) { 206 $asset->ensureFilter($this->getFilter($filter)); 207 } elseif (!$options['debug']) { 208 $asset->ensureFilter($this->getFilter(substr($filter, 1))); 209 } 210 } 211 212 // append variables 213 if (!empty($options['vars'])) { 214 $toAdd = array(); 215 foreach ($options['vars'] as $var) { 216 if (false !== strpos($options['output'], '{'.$var.'}')) { 217 continue; 218 } 219 220 $toAdd[] = '{'.$var.'}'; 221 } 222 223 if ($toAdd) { 224 $options['output'] = str_replace('*', '*.'.implode('.', $toAdd), $options['output']); 225 } 226 } 227 228 // append consensus extension if missing 229 if (1 == count($extensions) && !pathinfo($options['output'], PATHINFO_EXTENSION) && $extension = key($extensions)) { 230 $options['output'] .= '.'.$extension; 231 } 232 233 // output --> target url 234 $asset->setTargetPath(str_replace('*', $options['name'], $options['output'])); 235 236 // apply workers and return 237 return $this->applyWorkers($asset); 238 } 239 240 public function generateAssetName($inputs, $filters, $options = array()) 241 { 242 foreach (array_diff(array_keys($options), array('output', 'debug', 'root')) as $key) { 243 unset($options[$key]); 244 } 245 246 ksort($options); 247 248 return substr(sha1(serialize($inputs).serialize($filters).serialize($options)), 0, 7); 249 } 250 251 public function getLastModified(AssetInterface $asset) 252 { 253 $mtime = 0; 254 foreach ($asset instanceof AssetCollectionInterface ? $asset : array($asset) as $leaf) { 255 $mtime = max($mtime, $leaf->getLastModified()); 256 257 if (!$filters = $leaf->getFilters()) { 258 continue; 259 } 260 261 $prevFilters = array(); 262 foreach ($filters as $filter) { 263 $prevFilters[] = $filter; 264 265 if (!$filter instanceof DependencyExtractorInterface) { 266 continue; 267 } 268 269 // extract children from leaf after running all preceeding filters 270 $clone = clone $leaf; 271 $clone->clearFilters(); 272 foreach (array_slice($prevFilters, 0, -1) as $prevFilter) { 273 $clone->ensureFilter($prevFilter); 274 } 275 $clone->load(); 276 277 foreach ($filter->getChildren($this, $clone->getContent(), $clone->getSourceDirectory()) as $child) { 278 $mtime = max($mtime, $this->getLastModified($child)); 279 } 280 } 281 } 282 283 return $mtime; 284 } 285 286 /** 287 * Parses an input string string into an asset. 288 * 289 * The input string can be one of the following: 290 * 291 * * A reference: If the string starts with an "at" sign it will be interpreted as a reference to an asset in the asset manager 292 * * An absolute URL: If the string contains "://" or starts with "//" it will be interpreted as an HTTP asset 293 * * A glob: If the string contains a "*" it will be interpreted as a glob 294 * * A path: Otherwise the string is interpreted as a filesystem path 295 * 296 * Both globs and paths will be absolutized using the current root directory. 297 * 298 * @param string $input An input string 299 * @param array $options An array of options 300 * 301 * @return AssetInterface An asset 302 */ 303 protected function parseInput($input, array $options = array()) 304 { 305 if ('@' == $input[0]) { 306 return $this->createAssetReference(substr($input, 1)); 307 } 308 309 if (false !== strpos($input, '://') || 0 === strpos($input, '//')) { 310 return $this->createHttpAsset($input, $options['vars']); 311 } 312 313 if (self::isAbsolutePath($input)) { 314 if ($root = self::findRootDir($input, $options['root'])) { 315 $path = ltrim(substr($input, strlen($root)), '/'); 316 } else { 317 $path = null; 318 } 319 } else { 320 $root = $this->root; 321 $path = $input; 322 $input = $this->root.'/'.$path; 323 } 324 325 if (false !== strpos($input, '*')) { 326 return $this->createGlobAsset($input, $root, $options['vars']); 327 } 328 329 return $this->createFileAsset($input, $root, $path, $options['vars']); 330 } 331 332 protected function createAssetCollection(array $assets = array(), array $options = array()) 333 { 334 return new AssetCollection($assets, array(), null, isset($options['vars']) ? $options['vars'] : array()); 335 } 336 337 protected function createAssetReference($name) 338 { 339 if (!$this->am) { 340 throw new \LogicException('There is no asset manager.'); 341 } 342 343 return new AssetReference($this->am, $name); 344 } 345 346 protected function createHttpAsset($sourceUrl, $vars) 347 { 348 return new HttpAsset($sourceUrl, array(), false, $vars); 349 } 350 351 protected function createGlobAsset($glob, $root = null, $vars) 352 { 353 return new GlobAsset($glob, array(), $root, $vars); 354 } 355 356 protected function createFileAsset($source, $root = null, $path = null, $vars) 357 { 358 return new FileAsset($source, array(), $root, $path, $vars); 359 } 360 361 protected function getFilter($name) 362 { 363 if (!$this->fm) { 364 throw new \LogicException('There is no filter manager.'); 365 } 366 367 return $this->fm->get($name); 368 } 369 370 /** 371 * Filters an asset collection through the factory workers. 372 * 373 * Each leaf asset will be processed first, followed by the asset 374 * collection itself. 375 * 376 * @param AssetCollectionInterface $asset An asset collection 377 * 378 * @return AssetCollectionInterface 379 */ 380 private function applyWorkers(AssetCollectionInterface $asset) 381 { 382 foreach ($asset as $leaf) { 383 foreach ($this->workers as $worker) { 384 $retval = $worker->process($leaf, $this); 385 386 if ($retval instanceof AssetInterface && $leaf !== $retval) { 387 $asset->replaceLeaf($leaf, $retval); 388 } 389 } 390 } 391 392 foreach ($this->workers as $worker) { 393 $retval = $worker->process($asset, $this); 394 395 if ($retval instanceof AssetInterface) { 396 $asset = $retval; 397 } 398 } 399 400 return $asset instanceof AssetCollectionInterface ? $asset : $this->createAssetCollection(array($asset)); 401 } 402 403 private static function isAbsolutePath($path) 404 { 405 return '/' == $path[0] || '\\' == $path[0] || (3 < strlen($path) && ctype_alpha($path[0]) && $path[1] == ':' && ('\\' == $path[2] || '/' == $path[2])); 406 } 407 408 /** 409 * Loops through the root directories and returns the first match. 410 * 411 * @param string $path An absolute path 412 * @param array $roots An array of root directories 413 * 414 * @return string|null The matching root directory, if found 415 */ 416 private static function findRootDir($path, array $roots) 417 { 418 foreach ($roots as $root) { 419 if (0 === strpos($path, $root)) { 420 return $root; 421 } 422 } 423 } 424} 425