1: <?php namespace Laravel;
2:
3: class Asset {
4:
5: /**
6: * All of the instantiated asset containers.
7: *
8: * @var array
9: */
10: public static $containers = array();
11:
12: /**
13: * Get an asset container instance.
14: *
15: * <code>
16: * // Get the default asset container
17: * $container = Asset::container();
18: *
19: * // Get a named asset container
20: * $container = Asset::container('footer');
21: * </code>
22: *
23: * @param string $container
24: * @return Asset_Container
25: */
26: public static function container($container = 'default')
27: {
28: if ( ! isset(static::$containers[$container]))
29: {
30: static::$containers[$container] = new Asset_Container($container);
31: }
32:
33: return static::$containers[$container];
34: }
35:
36: /**
37: * Magic Method for calling methods on the default container.
38: *
39: * <code>
40: * // Call the "styles" method on the default container
41: * echo Asset::styles();
42: *
43: * // Call the "add" method on the default container
44: * Asset::add('jquery', 'js/jquery.js');
45: * </code>
46: */
47: public static function __callStatic($method, $parameters)
48: {
49: return call_user_func_array(array(static::container(), $method), $parameters);
50: }
51:
52: }
53:
54: class Asset_Container {
55:
56: /**
57: * The asset container name.
58: *
59: * @var string
60: */
61: public $name;
62:
63: /**
64: * The bundle that the assets belong to.
65: *
66: * @var string
67: */
68: public $bundle = DEFAULT_BUNDLE;
69:
70: /**
71: * All of the registered assets.
72: *
73: * @var array
74: */
75: public $assets = array();
76:
77: /**
78: * Create a new asset container instance.
79: *
80: * @param string $name
81: * @return void
82: */
83: public function __construct($name)
84: {
85: $this->name = $name;
86: }
87:
88: /**
89: * Add an asset to the container.
90: *
91: * The extension of the asset source will be used to determine the type of
92: * asset being registered (CSS or JavaScript). When using a non-standard
93: * extension, the style/script methods may be used to register assets.
94: *
95: * <code>
96: * // Add an asset to the container
97: * Asset::container()->add('jquery', 'js/jquery.js');
98: *
99: * // Add an asset that has dependencies on other assets
100: * Asset::add('jquery', 'js/jquery.js', 'jquery-ui');
101: *
102: * // Add an asset that should have attributes applied to its tags
103: * Asset::add('jquery', 'js/jquery.js', null, array('defer'));
104: * </code>
105: *
106: * @param string $name
107: * @param string $source
108: * @param array $dependencies
109: * @param array $attributes
110: * @return Asset_Container
111: */
112: public function add($name, $source, $dependencies = array(), $attributes = array())
113: {
114: $type = (pathinfo($source, PATHINFO_EXTENSION) == 'css') ? 'style' : 'script';
115:
116: return $this->$type($name, $source, $dependencies, $attributes);
117: }
118:
119: /**
120: * Add a CSS file to the registered assets.
121: *
122: * @param string $name
123: * @param string $source
124: * @param array $dependencies
125: * @param array $attributes
126: * @return Asset_Container
127: */
128: public function style($name, $source, $dependencies = array(), $attributes = array())
129: {
130: if ( ! array_key_exists('media', $attributes))
131: {
132: $attributes['media'] = 'all';
133: }
134:
135: $this->register('style', $name, $source, $dependencies, $attributes);
136:
137: return $this;
138: }
139:
140: /**
141: * Add a JavaScript file to the registered assets.
142: *
143: * @param string $name
144: * @param string $source
145: * @param array $dependencies
146: * @param array $attributes
147: * @return Asset_Container
148: */
149: public function script($name, $source, $dependencies = array(), $attributes = array())
150: {
151: $this->register('script', $name, $source, $dependencies, $attributes);
152:
153: return $this;
154: }
155:
156: /**
157: * Returns the full-path for an asset.
158: *
159: * @param string $source
160: * @return string
161: */
162: public function path($source)
163: {
164: return Bundle::assets($this->bundle).$source;
165: }
166:
167: /**
168: * Set the bundle that the container's assets belong to.
169: *
170: * @param string $bundle
171: * @return Asset_Container
172: */
173: public function bundle($bundle)
174: {
175: $this->bundle = $bundle;
176: return $this;
177: }
178:
179: /**
180: * Add an asset to the array of registered assets.
181: *
182: * @param string $type
183: * @param string $name
184: * @param string $source
185: * @param array $dependencies
186: * @param array $attributes
187: * @return void
188: */
189: protected function register($type, $name, $source, $dependencies, $attributes)
190: {
191: $dependencies = (array) $dependencies;
192:
193: $attributes = (array) $attributes;
194:
195: $this->assets[$type][$name] = compact('source', 'dependencies', 'attributes');
196: }
197:
198: /**
199: * Get the links to all of the registered CSS assets.
200: *
201: * @return string
202: */
203: public function styles()
204: {
205: return $this->group('style');
206: }
207:
208: /**
209: * Get the links to all of the registered JavaScript assets.
210: *
211: * @return string
212: */
213: public function scripts()
214: {
215: return $this->group('script');
216: }
217:
218: /**
219: * Get all of the registered assets for a given type / group.
220: *
221: * @param string $group
222: * @return string
223: */
224: protected function group($group)
225: {
226: if ( ! isset($this->assets[$group]) or count($this->assets[$group]) == 0) return '';
227:
228: $assets = '';
229:
230: foreach ($this->arrange($this->assets[$group]) as $name => $data)
231: {
232: $assets .= $this->asset($group, $name);
233: }
234:
235: return $assets;
236: }
237:
238: /**
239: * Get the HTML link to a registered asset.
240: *
241: * @param string $group
242: * @param string $name
243: * @return string
244: */
245: protected function asset($group, $name)
246: {
247: if ( ! isset($this->assets[$group][$name])) return '';
248:
249: $asset = $this->assets[$group][$name];
250:
251: // If the bundle source is not a complete URL, we will go ahead and prepend
252: // the bundle's asset path to the source provided with the asset. This will
253: // ensure that we attach the correct path to the asset.
254: if (filter_var($asset['source'], FILTER_VALIDATE_URL) === false)
255: {
256: $asset['source'] = $this->path($asset['source']);
257: }
258:
259: return HTML::$group($asset['source'], $asset['attributes']);
260: }
261:
262: /**
263: * Sort and retrieve assets based on their dependencies
264: *
265: * @param array $assets
266: * @return array
267: */
268: protected function arrange($assets)
269: {
270: list($original, $sorted) = array($assets, array());
271:
272: while (count($assets) > 0)
273: {
274: foreach ($assets as $asset => $value)
275: {
276: $this->evaluate_asset($asset, $value, $original, $sorted, $assets);
277: }
278: }
279:
280: return $sorted;
281: }
282:
283: /**
284: * Evaluate an asset and its dependencies.
285: *
286: * @param string $asset
287: * @param string $value
288: * @param array $original
289: * @param array $sorted
290: * @param array $assets
291: * @return void
292: */
293: protected function evaluate_asset($asset, $value, $original, &$sorted, &$assets)
294: {
295: // If the asset has no more dependencies, we can add it to the sorted list
296: // and remove it from the array of assets. Otherwise, we will not verify
297: // the asset's dependencies and determine if they've been sorted.
298: if (count($assets[$asset]['dependencies']) == 0)
299: {
300: $sorted[$asset] = $value;
301:
302: unset($assets[$asset]);
303: }
304: else
305: {
306: foreach ($assets[$asset]['dependencies'] as $key => $dependency)
307: {
308: if ( ! $this->dependency_is_valid($asset, $dependency, $original, $assets))
309: {
310: unset($assets[$asset]['dependencies'][$key]);
311:
312: continue;
313: }
314:
315: // If the dependency has not yet been added to the sorted list, we can not
316: // remove it from this asset's array of dependencies. We'll try again on
317: // the next trip through the loop.
318: if ( ! isset($sorted[$dependency])) continue;
319:
320: unset($assets[$asset]['dependencies'][$key]);
321: }
322: }
323: }
324:
325: /**
326: * Verify that an asset's dependency is valid.
327: *
328: * A dependency is considered valid if it exists, is not a circular reference, and is
329: * not a reference to the owning asset itself. If the dependency doesn't exist, no
330: * error or warning will be given. For the other cases, an exception is thrown.
331: *
332: * @param string $asset
333: * @param string $dependency
334: * @param array $original
335: * @param array $assets
336: * @return bool
337: */
338: protected function dependency_is_valid($asset, $dependency, $original, $assets)
339: {
340: if ( ! isset($original[$dependency]))
341: {
342: return false;
343: }
344: elseif ($dependency === $asset)
345: {
346: throw new \Exception("Asset [$asset] is dependent on itself.");
347: }
348: elseif (isset($assets[$dependency]) and in_array($asset, $assets[$dependency]['dependencies']))
349: {
350: throw new \Exception("Assets [$asset] and [$dependency] have a circular dependency.");
351: }
352:
353: return true;
354: }
355:
356: }
357: