@@ -28,9 +28,26 @@ final class ReflectionCache
2828 */
2929 private $ changes = [];
3030
31+ /**
32+ * @var resource
33+ */
34+ private static $ lockHandle ;
35+
3136 private function __construct (string $ cacheFile )
3237 {
3338 $ this ->cacheFile = $ cacheFile ;
39+
40+ if (null === self ::$ lockHandle ) {
41+ // prevent parallel phpstan-worker-process from writing into the cache file at the same time
42+ // XXX we use a single system-wide lock file, which might get problematic if multiple users run phpstan on the same machine at the same time
43+ $ lockFile = sys_get_temp_dir ().'/staabm-phpstan-dba-cache.lock ' ;
44+ $ lockHandle = fopen ($ lockFile , 'w+ ' );
45+ if (false === $ lockHandle ) {
46+ throw new DbaException (sprintf ('Could not open lock file "%s". ' , $ this ->cacheFile ));
47+ }
48+
49+ self ::$ lockHandle = $ lockHandle ;
50+ }
3451 }
3552
3653 public static function create (string $ cacheFile ): self
@@ -41,7 +58,7 @@ public static function create(string $cacheFile): self
4158 public static function load (string $ cacheFile ): self
4259 {
4360 $ reflectionCache = new self ($ cacheFile );
44- $ cachedRecords = $ reflectionCache ->readCache ();
61+ $ cachedRecords = $ reflectionCache ->readCache (true );
4562 if (null !== $ cachedRecords ) {
4663 $ reflectionCache ->records = $ cachedRecords ;
4764 }
@@ -52,15 +69,24 @@ public static function load(string $cacheFile): self
5269 /**
5370 * @return array<string, array{error?: ?Error, result?: array<QueryReflector::FETCH_TYPE*, ?Type>}>|null
5471 */
55- private function readCache (): ?array
72+ private function readCache (bool $ useReadLock ): ?array
5673 {
5774 if (!is_file ($ this ->cacheFile )) {
5875 if (false === file_put_contents ($ this ->cacheFile , '' )) {
5976 throw new DbaException (sprintf ('Cache file "%s" is not readable and creating a new one did not succeed. ' , $ this ->cacheFile ));
6077 }
6178 }
62- clearstatcache (true , $ this ->cacheFile );
63- $ cache = require $ this ->cacheFile ;
79+
80+ try {
81+ if ($ useReadLock ) {
82+ flock (self ::$ lockHandle , \LOCK_SH );
83+ }
84+ $ cache = require $ this ->cacheFile ;
85+ } finally {
86+ if ($ useReadLock ) {
87+ flock (self ::$ lockHandle , \LOCK_UN );
88+ }
89+ }
6490
6591 if (\is_array ($ cache ) && \array_key_exists ('schemaVersion ' , $ cache ) && self ::SCHEMA_VERSION === $ cache ['schemaVersion ' ]) {
6692 return $ cache ['records ' ];
@@ -75,47 +101,40 @@ public function persist(): void
75101 return ;
76102 }
77103
78- // prevent parallel phpstan-worker-process from writing into the cache file at the same time
79- // XXX we use a single system-wide lock file, which might get problematic if multiple users run phpstan on the same machine at the same time
80- $ lockFile = sys_get_temp_dir ().'/staabm-phpstan-dba-cache.lock ' ;
81- $ lockHandle = fopen ($ lockFile , 'w+ ' );
82- if (false === $ lockHandle ) {
83- throw new DbaException (sprintf ('Could not open cache file "%s" for writing ' , $ this ->cacheFile ));
84- }
85- flock ($ lockHandle , LOCK_EX );
104+ try {
105+ flock (self ::$ lockHandle , LOCK_EX );
86106
87- // freshly read the cache as it might have changed in the meantime
88- $ cachedRecords = $ this ->readCache ();
107+ // freshly read the cache as it might have changed in the meantime
108+ $ cachedRecords = $ this ->readCache (false );
89109
90- $ handle = fopen ($ this ->cacheFile , 'w+ ' );
91- if (false === $ handle ) {
92- throw new DbaException (sprintf ('Could not open cache file "%s" for writing ' , $ this ->cacheFile ));
93- }
110+ // re-apply all changes to the current cache-state
111+ if (null === $ cachedRecords ) {
112+ $ newRecords = $ this ->changes ;
113+ } else {
114+ $ newRecords = array_merge ($ cachedRecords , $ this ->changes );
115+ }
94116
95- // re-apply all changes to the current cache-state
96- if (null === $ cachedRecords ) {
97- $ newRecords = $ this ->changes ;
98- } else {
99- $ newRecords = array_merge ($ cachedRecords , $ this ->changes );
100- }
117+ // sort records to prevent unnecessary cache invalidation caused by different order of queries
118+ uksort ($ newRecords , function ($ queryA , $ queryB ) {
119+ return $ queryA <=> $ queryB ;
120+ });
101121
102- // sort records to prevent unnecessary cache invalidation caused by different order of queries
103- uksort ( $ newRecords , function ( $ queryA , $ queryB ) {
104- return $ queryA < => $ queryB ;
105- }) ;
122+ $ cacheContent = ' <?php return ' . var_export ([
123+ ' schemaVersion ' => self :: SCHEMA_VERSION ,
124+ ' records ' => $ newRecords ,
125+ ], true ). ' ; ' ;
106126
107- $ cacheContent = '<?php return ' .var_export ([
108- 'schemaVersion ' => self ::SCHEMA_VERSION ,
109- 'records ' => $ newRecords ,
110- ], true ).'; ' ;
127+ if (false === file_put_contents ($ this ->cacheFile , $ cacheContent )) {
128+ throw new DbaException (sprintf ('Unable to write cache file "%s" ' , $ this ->cacheFile ));
129+ }
111130
112- if (false === fwrite ($ handle , $ cacheContent )) {
113- throw new DbaException (sprintf ('Unable to write cache file "%s" ' , $ this ->cacheFile ));
131+ clearstatcache (true , $ this ->cacheFile );
132+ if (\function_exists ('opcache_invalidate ' )) {
133+ opcache_invalidate ($ this ->cacheFile , true );
134+ }
135+ } finally {
136+ flock (self ::$ lockHandle , \LOCK_UN );
114137 }
115-
116- fclose ($ handle );
117- // will free the lock implictly
118- fclose ($ lockHandle );
119138 }
120139
121140 public function hasValidationError (string $ queryString ): bool
0 commit comments