17
17
use yii \helpers \Json ;
18
18
use yii \web \AssetBundle ;
19
19
20
+ /**
21
+ * Class DbAsset
22
+ * @package extensions\dmstr\prototype\assets
23
+ *
24
+ * This class implements an AssetBundle for Less "files" stored in DB as prototype\models\Less models
25
+ * In init() we get the less models from DB, build an overall checksum that will be compared with a cached value
26
+ * previous run.
27
+ *
28
+ * If nothing has changed (checksum match, and db less was yet exported to disc) nothing will be done here
29
+ * If "something" changed (or in the first run):
30
+ * - if not exists, create empty sourcePath dir to prevent race condition with next req which will try to init
31
+ * this bundle again...
32
+ * - write contents of less models as *.less files in a tmp dir
33
+ * - To prevent multiple converter runs while asset publishing the main less will be converted in this tmp dir
34
+ * - To prevent multiple publishing runs we use another tmp file and simple renames while changing the sourcePath contents
35
+ * - To prevent unnecessary publishing you should configure a persistent cacheComponent to store the checksum
36
+ * which survive a restart (eg. do not use a memory cache which is part of a docker-compose stack)
37
+ * - if something went wrong the previous state (prev. checksum in cache) is restored
38
+ *
39
+ */
20
40
class DbAsset extends AssetBundle
21
41
{
22
42
const CACHE_ID = 'app\assets\SettingsAsset ' ;
@@ -26,6 +46,15 @@ class DbAsset extends AssetBundle
26
46
27
47
public $ sourcePath = '@runtime/settings-asset ' ;
28
48
public $ tmpPath = '@runtime/settings-asset-tmp ' ;
49
+ /**
50
+ *
51
+ * name of the cache component that should be used for the less checksum cache
52
+ * for high volume sites this should be set to a persistent cache which survive a
53
+ * restart
54
+ *
55
+ * @var string
56
+ */
57
+ public $ cacheComponent = 'cache ' ;
29
58
30
59
public $ settingsKey = 'registerPrototypeAssetKey ' ;
31
60
@@ -34,9 +63,18 @@ class DbAsset extends AssetBundle
34
63
// if a full BootstrapAsset (CSS) is compiled, it's recommended to disable it in assetManager configuration
35
64
'yii\bootstrap\BootstrapPluginAsset ' , // (JS)
36
65
];
66
+ /**
67
+ * internal cache property
68
+ *
69
+ * @var
70
+ */
71
+ protected $ cache ;
37
72
38
73
public function init ()
39
74
{
75
+ // init configured cache component
76
+ $ this ->cache = Yii::$ app ->{$ this ->cacheComponent };
77
+
40
78
$ this ->css [] = Yii::$ app ->settings ->get ($ this ->settingsKey , self ::SETTINGS_SECTION ).'- ' .self ::MAIN_LESS_FILE ;
41
79
42
80
parent ::init ();
@@ -49,21 +87,56 @@ public function init()
49
87
50
88
$ models = Less::find ()->all ();
51
89
$ hash = sha1 (Json::encode ($ models ));
52
- if (!is_dir ($ sourcePath ) || ($ hash !== Yii::$ app ->cache ->get (self ::CACHE_ID ))) {
90
+ $ prevHash = $ this ->cache ->get (self ::CACHE_ID );
91
+ $ sourcePathExists = is_dir ($ sourcePath );
92
+ if (($ hash !== $ prevHash ) || ! $ sourcePathExists ) {
93
+
94
+ // create empty sourcePath dir to prevent race condition with next req which will init again...
95
+ if ( ! $ sourcePathExists ) {
96
+ FileHelper::createDirectory ($ sourcePath );
97
+ }
98
+ $ dependency = new FileDependency ();
99
+ $ dependency ->fileName = __FILE__ ;
100
+ $ this ->cache ->set (self ::CACHE_ID , $ hash , 0 , $ dependency );
101
+
53
102
$ tmpPath = uniqid ($ sourcePath .'- ' );
54
103
FileHelper::createDirectory ($ tmpPath );
55
-
56
104
foreach ($ models as $ model ) {
57
105
file_put_contents ("$ tmpPath/ {$ model ->key }.less " , $ model ->value );
58
106
}
59
107
60
- $ dependency = new FileDependency ();
61
- $ dependency ->fileName = __FILE__ ;
62
- Yii::$ app ->cache ->set (self ::CACHE_ID , $ hash , 0 , $ dependency );
108
+ // convert less with new files in tmp folder before replacing bundle sourcePath
109
+ // to prevent multiple conversions while republishing on high-traffic sites
110
+ $ converter = Yii::$ app ->assetManager ->getConverter ();
111
+ try {
112
+ foreach ($ this ->css as $ cssFile ) {
113
+ $ result = $ converter ->convert ($ cssFile , $ tmpPath );
114
+ }
115
+ } catch (\Exception $ exception ) {
116
+ $ this ->cache ->set (self ::CACHE_ID , $ prevHash , 0 , $ dependency );
117
+ Yii::error ($ exception ->getMessage (), __METHOD__ );
118
+ return false ;
119
+ }
63
120
64
121
// force republishing of asset files by Yii Framework
65
- FileHelper::removeDirectory ($ sourcePath );
66
- rename ($ tmpPath , $ sourcePath );
122
+ // to prevent race conditions, use 2 rename cmds to switch dir and remove prev. dir afterwards
123
+ $ sourcePathToDelete = uniqid ($ sourcePath .'-to-delete- ' );
124
+ $ sourcePathRenamed = false ;
125
+ if ($ sourcePathExists ) {
126
+ if (rename ($ sourcePath , $ sourcePathToDelete )) {
127
+ $ sourcePathRenamed = true ;
128
+ } else {
129
+ $ this ->cache ->set (self ::CACHE_ID , $ prevHash , 0 , $ dependency );
130
+ return false ;
131
+ }
132
+ }
133
+ if ( ! rename ($ tmpPath , $ sourcePath )) {
134
+ $ this ->cache ->set (self ::CACHE_ID , $ prevHash , 0 , $ dependency );
135
+ return false ;
136
+ }
137
+ if ($ sourcePathRenamed ) {
138
+ FileHelper::removeDirectory ($ sourcePathToDelete );
139
+ }
67
140
}
68
141
}
69
142
}
0 commit comments