-
Notifications
You must be signed in to change notification settings - Fork 0
/
scss_cache.php
290 lines (253 loc) · 8.8 KB
/
scss_cache.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
<?php
class scss_cache{
protected $debug = false;
protected $queryParam;
protected $source;
protected $name;
/**
* instanciate new cache for the given SCSS file path
*
* @param string $path path to import via SCSS
* @param string|null $name optional name used to reference cache, defaults to name derived from given file name
* @return scss_cache
*/
public static function file($path,$name=null){
if($name === null){
$name = basename($path).'-'.substr(md5($path),-5);
}
return new self('@import "'.$path.'";',$name);
}
/**
* instanciate new cache for given SCSS input source
*
* @param string $source input SCSS source
* @param string|null $name name to use for caching this source
*/
public function __construct($source,$name=null){
$this->source = $source;
if($name === null){
$temp = debug_backtrace();
$name = (isset($temp[0]['file'])) ? $temp[0]['file'].$temp[0]['line'] : 'scss_cache';
}
$this->name = $name;
}
protected function httpDate($time){
return gmdate('D, d M Y H:i:s',$time) . ' GMT';
}
protected function httpPrepare($time){
header('Content-Type: text/css');
header('Last-Modified: '.$this->httpDate($time));
if($this->queryParam !== null && !$this->debug){ // virtually "never" expire this resource if a new query param is added every time the resource changes
header('Expires: '.$this->httpDate(strtotime('+1 year',$time))); // expire in 1 year ("never" request this resource again, always keep in cache)
}
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])){ // previous last-modified header received
$ref = @strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
if($ref == $time){ // same as cached time, so stop transfering actual content
if($this->debug){
echo '/* HTTP not modified ('.$time.') */';
}else{
header(' ',true,304); // not modified
exit();
}
}
}
// if nothing has been sent (or is to be sent due to parent output buffer)
if(!headers_sent() && !ob_get_level()){
// enable compression
ob_start('ob_gzhandler');
}
}
/**
* set option query param to generate unique URLs whenever the source changes
*
* once set, this allows to send HTTP headers to indicate this resorce
* never expires. Browsers will therefor not try to access this resource
* again. Once a change has been detected, the resource has to be requested
* with a different URI (i.e. a new query parameter is added every time the
* resource changes - aka cache busting)
*
* @param string|NULL $queryParam parameter name to use or NULL=disable
* @return scss_cache $this (chainable)
*/
public function setQueryParam($queryParam){
$this->queryParam = $queryParam;
return $this;
}
/**
* check whether the source is cache or needs to be recompiled
*
* @return boolean
*/
public function isCached(){
return ($this->getMetaChecked() !== null);
}
/**
* serve up-to-date resulting CSS via HTTP (recompile if neccessary)
*
* @return void
* @throws Exception on error
* @uses self::getMetaChecked()
* @uses self::update()
*/
public function serve(){
$meta = $this->getMetaChecked();
if($meta !== null){
if($this->queryParam !== null && (!isset($_GET[$this->queryParam]) || $_GET[$this->queryParam] != $meta['time'])){ // old or no timestamp supplied
header('Location: ?'.$this->queryParam.'='.$meta['time'],true,301); // permanently moved
return;
}
$this->httpPrepare($meta['time']);
if($this->debug) echo '/* read from cache ('.$meta['time'].') */';
readfile($meta['target']);
return;
}
// TODO: lock and retry...
// flock();
// check cache again
// otherwise continue:
$meta = array();
try{
$content = $this->update($meta);
}
catch(Exception $e){
header(' ',true,500); // server error
echo '/* error: '.$e->getMessage().' */';
return;
}
if($this->queryParam !== null){
header('Location: ?'.$this->queryParam.'='.$meta['time'],true,301); // redirect to new cached file (permanently moved)
}else{
if($this->debug) echo '/* new cache target created ('.$meta['time'].') */';
$this->httpPrepare($meta['time']);
echo $content;
}
}
/**
* purge (delete) cache files and cache meta data
*
* @return void
*/
public function purge(){
$meta = @xcache_get($this->name);
if($meta){
if(is_writeable($meta['target'])){
unlink($meta['target']);
}
xcache_unset($this->name);
}
}
/**
* refresh
*
* @param array $meta
* @throws Exception
* @return string
*/
protected function update(&$meta=null){
$this->purge();
$meta = array();
$meta['time'] = time();
$meta['target'] = $this->tempnam();
$meta['hash'] = md5($this->source);
$meta['files'] = array();
$content = $this->compile($meta);
if(file_put_contents($meta['target'],$content,LOCK_EX) === false){
throw new Exception('Unable to write compiled source to target cache file');
}
if(!xcache_set($this->name,$meta)){
throw new Exception('Unable to write cache meta data to temporary xcache');
}
return $content;
}
/**
* get up-to-date cache meta data (re-compile if neccessary)
*
* @return array
* @uses self::getMetaChecked()
* @uses self::update()
*/
public function getMeta(){
$meta = $this->getMetaChecked();
if(!$meta){
$this->update($meta);
}
return $meta;
}
/**
* get up-to-date css output (re-compile if neccessary)
*
* @return string
* @uses self::getMetaChecked()
* @uses self::update()
*/
public function getOutput(){
$meta = $this->getMetaChecked();
if($meta){
return file_get_contents($meta['target']);
}
return $this->update($meta);
}
protected function getMetaChecked(){
$meta = @xcache_get($this->name);
if(!$meta){
return null;
}
// check cache file
if(!is_file($meta['target'])){
if($this->debug) echo '/* target '.$meta['target'].' ('.$meta['time'].') missing */';
return null;
}
// check if scss input source has changed
$hash = md5($this->source);
if($meta['hash'] !== $hash){
if($this->debug) echo '/* input source hash ('.$meta['time'].') changed */';
return null;
}
// check all files from cache for changes ever since cache was created
foreach($meta['files'] as $file){
if(!is_file($file) || filemtime($file) > $meta['time']){
if($this->debug) echo '/* updated '.$file.' (file:'.filemtime($file).' cache:'.$meta['time'].') */';
return null;
}
}
return $meta;
}
/**
* create new temporary filename to write output cache to
*
* @return string
*/
protected function tempnam(){
//'/var/run/'.$this->name.'.out.css';
return tempnam(sys_get_temp_dir(),$this->name);
}
/**
* create new SCSS compiler instance
*
* can be overwritten in order to pass custom formatter options, custom
* import paths, etc.
*
* @return scssc
*/
protected function scssc(){
$formatter = new scss_formatter_compressed();
$scssc = new scssc();
$scssc->setFormatter($formatter);
return $scssc;
}
/**
* compile local SCSS source and return resulting CSS
*
* @param array $meta
* @return string
* @throws Exception on error
*/
protected function compile(&$meta){
$scssc = $this->scssc();
$content = $scssc->compile($this->source);
foreach($scssc->getParsedFiles() as $file){
$meta['files'][] = realpath($file);
}
return $content;
}
}