1717use yii \helpers \Json ;
1818use yii \web \AssetBundle ;
1919
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+ */
2040class DbAsset extends AssetBundle
2141{
2242 const CACHE_ID = 'app\assets\SettingsAsset ' ;
@@ -26,6 +46,15 @@ class DbAsset extends AssetBundle
2646
2747 public $ sourcePath = '@runtime/settings-asset ' ;
2848 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 ' ;
2958
3059 public $ settingsKey = 'registerPrototypeAssetKey ' ;
3160
@@ -34,9 +63,18 @@ class DbAsset extends AssetBundle
3463 // if a full BootstrapAsset (CSS) is compiled, it's recommended to disable it in assetManager configuration
3564 'yii\bootstrap\BootstrapPluginAsset ' , // (JS)
3665 ];
66+ /**
67+ * internal cache property
68+ *
69+ * @var
70+ */
71+ protected $ cache ;
3772
3873 public function init ()
3974 {
75+ // init configured cache component
76+ $ this ->cache = Yii::$ app ->{$ this ->cacheComponent };
77+
4078 $ this ->css [] = Yii::$ app ->settings ->get ($ this ->settingsKey , self ::SETTINGS_SECTION ).'- ' .self ::MAIN_LESS_FILE ;
4179
4280 parent ::init ();
@@ -49,21 +87,56 @@ public function init()
4987
5088 $ models = Less::find ()->all ();
5189 $ 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+
53102 $ tmpPath = uniqid ($ sourcePath .'- ' );
54103 FileHelper::createDirectory ($ tmpPath );
55-
56104 foreach ($ models as $ model ) {
57105 file_put_contents ("$ tmpPath/ {$ model ->key }.less " , $ model ->value );
58106 }
59107
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+ }
63120
64121 // 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+ }
67140 }
68141 }
69142 }
0 commit comments