Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
57.09% covered (warning)
57.09%
165 / 289
41.18% covered (danger)
41.18%
14 / 34
CRAP
0.00% covered (danger)
0.00%
0 / 1
Workspace
57.09% covered (warning)
57.09%
165 / 289
41.18% covered (danger)
41.18%
14 / 34
1084.23
0.00% covered (danger)
0.00%
0 / 1
 getAll
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 hasFilesChanged
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getHashOfAllFiles
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getOrCreateWorkspacePath
57.14% covered (warning)
57.14%
4 / 7
0.00% covered (danger)
0.00%
0 / 1
6.97
 getOrCreateSubFolderPath
55.56% covered (warning)
55.56%
5 / 9
0.00% covered (danger)
0.00%
0 / 1
9.16
 getId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWorkspacePath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 deleteFiles
80.00% covered (warning)
80.00%
16 / 20
0.00% covered (danger)
0.00%
0 / 1
6.29
 deleteFileFromFs
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 deleteFileFromDb
55.56% covered (warning)
55.56%
5 / 9
0.00% covered (danger)
0.00%
0 / 1
7.19
 isPathLegal
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 importUncategorizedFiles
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 sortAndValidateToplevelFiles
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
9
 validateFilesForCategory
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 categorizeFile
32.50% covered (danger)
32.50%
13 / 40
0.00% covered (danger)
0.00%
0 / 1
33.91
 unpackRootlevelZipArchive
70.00% covered (warning)
70.00%
7 / 10
0.00% covered (danger)
0.00%
0 / 1
4.43
 getExtractionDirName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 deleteRootlevelFiles
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getFileById
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getFileByName
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 countFilesOfAllSubFolders
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 countFiles
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 delete
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 storeAllFiles
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
20
 removeVanishedFilesFromDB
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 storeFileMeta
88.89% covered (warning)
88.89%
24 / 27
0.00% covered (danger)
0.00%
0 / 1
7.07
 getRequestedAttachments
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 getFileRelations
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 updateDependentFiles
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
4.94
 getBookletResourcePaths
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getWorkspaceHash
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setWorkspaceHash
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 loadFilesIntoCache
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3/** @noinspection PhpUnhandledExceptionInspection */
4declare(strict_types=1);
5
6class Workspace {
7  protected int $workspaceId = 0;
8  protected string $workspacePath = '';
9  public WorkspaceDAO $workspaceDAO;
10
11  // dont' change order, it's the order of possible dependencies
12  const subFolders = ['Resource', 'Unit', 'Booklet', 'Testtakers', 'SysCheck'];
13
14  static function getAll(): array {
15    $workspaces = [];
16    $class = get_called_class();
17
18    foreach (Folder::glob(DATA_DIR, 'ws_*') as $workspaceDir) {
19      $workspaceFolderNameParts = explode('_', $workspaceDir);
20      $workspaceId = (int) array_pop($workspaceFolderNameParts);
21      if (!$workspaceId) {
22        continue;
23      }
24      $workspaces[$workspaceId] = new $class($workspaceId);
25    }
26
27    return $workspaces;
28  }
29
30  public function hasFilesChanged(string $currentHash): bool {
31    $originalHash = $this->workspaceDAO->getWorkspaceHash();
32    return $currentHash !== $originalHash;
33  }
34
35  private static function getHashOfAllFiles(string $dir): array {
36    $result = [];
37
38    $files = scandir($dir);
39
40    foreach ($files as $file) {
41      if ($file != '.' && $file != '..') {
42        $path = $dir . '/' . $file;
43        if (is_dir($path)) {
44          $result = array_merge($result, self::getHashOfAllFiles($path));
45        } else {
46          $result[] = [
47            'filename' => $file,
48            'filemtime' => filemtime($path),
49            'filesize' => filesize($path)
50          ];
51        }
52      }
53    }
54
55    return $result;
56  }
57
58  function __construct(int $workspaceId) {
59    $this->workspaceId = $workspaceId;
60    $this->workspacePath = $this->getOrCreateWorkspacePath();
61    $this->workspaceDAO = new WorkspaceDAO($this->workspaceId, $this->workspacePath);
62  }
63
64  protected function getOrCreateWorkspacePath(): string {
65    $workspacePath = DATA_DIR . '/ws_' . $this->workspaceId;
66    if (file_exists($workspacePath) and !is_dir($workspacePath)) {
67      throw new Exception("Workspace dir $this->workspaceId seems not to be a proper directory!");
68    }
69    if (!file_exists($workspacePath)) {
70      if (!mkdir($workspacePath)) {
71        throw new Exception("Could not create workspace dir $this->workspaceId");
72      }
73    }
74    return $workspacePath;
75  }
76
77  public function getOrCreateSubFolderPath(string $type): string {
78    $subFolderPath = $this->workspacePath . '/' . $type;
79    if (!in_array($type, $this::subFolders)) {
80      throw new Exception("Invalid type `$type`!");
81    }
82    if (file_exists($subFolderPath) and !is_dir($subFolderPath)) {
83      throw new Exception("Workspace dir `$subFolderPath` seems not to be a proper directory!");
84    }
85    if (!file_exists($subFolderPath)) {
86      if (!mkdir($subFolderPath)) {
87        throw new Exception("Could not create workspace dir `$subFolderPath`");
88      }
89    }
90    return $subFolderPath;
91  }
92
93  public function getId(): int {
94    return $this->workspaceId;
95  }
96
97  public function getWorkspacePath(): string {
98    return $this->workspacePath;
99  }
100
101  public function deleteFiles(array $filesToDelete): FileDeletionReport {
102    $deletionReport = new FileDeletionReport();
103
104    $cachedFilesToDelete = $this->workspaceDAO->getFiles($filesToDelete, true);
105    $blockedFiles = $this->workspaceDAO->getBlockedFiles(array_merge(...array_values($cachedFilesToDelete)));
106
107    foreach ($filesToDelete as $localFilePath) {
108      $pathParts = explode('/', $localFilePath, 2);
109
110      if (count($pathParts) < 2) {
111        $deletionReport->incorrect_path[] = $localFilePath;
112        continue;
113      }
114
115      list($type, $name) = $pathParts;
116
117      $cachedFile = $cachedFilesToDelete[$type][$name] ?? null;
118
119      // file does not exist in db means, it must be something not validatable like sysCheck-Reports
120      if ($cachedFile) {
121        if (isset($blockedFiles[$localFilePath])) {
122          $deletionReport->was_used[] = $localFilePath;
123          continue;
124        }
125
126        if (!$this->deleteFileFromDb($cachedFile)) {
127          $deletionReport->error[] = $localFilePath;
128          continue;
129        }
130      }
131      $fieldName = $this->deleteFileFromFs($this->workspacePath . '/' . $localFilePath);
132      $deletionReport->$fieldName[] = $localFilePath;
133    }
134
135    return $deletionReport;
136  }
137
138  protected function deleteFileFromFs(string $fullPath): string {
139    if (!file_exists($fullPath)) {
140      return 'did_not_exist';
141    }
142
143    if ($this->isPathLegal($fullPath) and unlink($fullPath)) {
144      return 'deleted';
145    }
146
147    return 'not_allowed';
148  }
149
150  private function deleteFileFromDb(?File $file): bool {
151    try {
152      if (is_a($file, XMLFileTesttakers::class)) {
153        $this->workspaceDAO->deleteLoginSource($file->getName());
154      }
155
156      if (is_a($file, ResourceFile::class) and $file->isPackage()) {
157        $file->uninstallPackage();
158      }
159
160      $this->workspaceDAO->deleteFile($file);
161
162    } catch (Exception $e) {
163      echo $e->getMessage();
164      return false;
165    }
166    return true;
167  }
168
169  protected function isPathLegal(string $path): bool {
170    return substr_count($path, '..') == 0;
171  }
172
173  /** takes files from the workspace-dir toplevel and puts it to the correct subdir if valid
174   * @return array{
175   *   type: string,
176   *   warning: array,
177   *   error: array,
178   *   info: array}
179   */
180  public function importUncategorizedFiles(array $fileNames): array {
181    $toDeleteFilePaths = [];
182    $toSortInFilePaths = [];
183    foreach ($fileNames as $fileName) {
184      if (FileExt::has($fileName, 'ZIP') and !FileExt::has($fileName, 'ITCR.ZIP')) {
185        array_push($toSortInFilePaths, ...$this->unpackRootlevelZipArchive($fileName));
186        array_push($toDeleteFilePaths, $fileName, $this->getExtractionDirName($fileName));
187      } else {
188        $toSortInFilePaths[] = $fileName;
189        $toDeleteFilePaths[] = $fileName;
190      }
191    }
192
193    $importedFiles = $this->sortAndValidateToplevelFiles($toSortInFilePaths);
194    $this->deleteRootlevelFiles($toDeleteFilePaths);
195
196    return $importedFiles;
197  }
198
199  private function sortAndValidateToplevelFiles(array $relativeFilePaths): array {
200    $filesAfterSorting = [];
201    $pathsPerType = array_fill_keys(Workspace::subFolders, []);
202
203    foreach ($relativeFilePaths as $relativeFilePath) {
204      $pathsPerType[File::determineType($this->workspacePath . '/' . $relativeFilePath)][] = $relativeFilePath;
205    }
206
207    $pathsPerType = [
208      // Resource and Unit are merged in order to save one call to getFilesWhere(['type' => 'Resource']), as this is the
209      // most expensive call in the following loop (Unit depends on Resource anyway)
210      !empty($pathsPerType['Unit']) ? 'Unit' : 'Resource' => array_merge(
211        $pathsPerType['Resource'],
212        $pathsPerType['Unit']
213      ),
214      'Booklet' => $pathsPerType['Booklet'],
215      'Testtakers' => $pathsPerType['Testtakers'],
216      'SysCheck' => $pathsPerType['SysCheck'],
217      'xml' => $pathsPerType['xml']
218    ];
219
220    foreach ($pathsPerType as $type => $filePaths) {
221      if (!empty($filePaths)) {
222        // these files are all not valid from the start
223        if ($type === 'xml') {
224          foreach ($filePaths as $path) {
225            $file = File::get($this->workspacePath . '/' . $path);
226            $filesAfterSorting[$path] = ['type' => $file->getType(), ...$file->getValidationReport()];
227          }
228          continue;
229        }
230
231        $files = $this->validateFilesForCategory($filePaths, FileType::tryFrom($type));
232        foreach ($files as $filePath => $file) {
233          if ($file->isValid()) {
234            $this->categorizeFile($filePath, $file);
235            $this->workspaceDAO->storeFile($file);
236            $this->storeFileMeta($file);
237            $this->updateDependentFiles($file);
238          }
239
240          $filesAfterSorting[$filePath] = ['type' => $file->getType(), ...$file->getValidationReport()];
241        }
242      }
243    }
244
245    return $filesAfterSorting;
246  }
247
248  protected function validateFilesForCategory(array $localFilePaths, FileType $highestTypeAmongFiles): array {
249    $filesPerType = [];
250    $workspaceCache = new WorkspaceCache($this);
251    $this->loadFilesIntoCache($workspaceCache, $highestTypeAmongFiles);
252
253    foreach ($localFilePaths as $localFilePath) {
254      $file = File::get($this->workspacePath . '/' . $localFilePath);
255      $workspaceCache->addFile($file->getType(), $file, true);
256      $filesPerType[$localFilePath] = $file;
257    }
258
259    $workspaceCache->validate($highestTypeAmongFiles->value);
260
261    return $filesPerType;
262  }
263
264  protected function categorizeFile(string $localFilePath, File $file): bool {
265    $targetFolder = $this->workspacePath . '/' . $file->getType();
266
267    if (!file_exists($targetFolder)) {
268      if (!mkdir($targetFolder)) {
269        $file->report('error', "Could not create folder: `$targetFolder`.");
270        return false;
271      }
272    }
273
274    $targetFilePath = $targetFolder . '/' . basename($localFilePath);
275
276    if (file_exists($targetFilePath)) {
277      $oldFile = File::get($targetFilePath, $file->getType());
278
279      if ($oldFile->getId() !== $file->getId()) {
280        $file->report(
281          'error',
282          "File of name `{$oldFile->getName()}` did already exist. "
283          . "Overwriting was rejected since new file's ID (`{$file->getId()}`) differs from old one (`{$oldFile->getId()}`)."
284        );
285        return false;
286      }
287
288      if ($oldFile->getVeronaModuleId() !== $file->getVeronaModuleId()) {
289        $file->report(
290          'error',
291          "File of name `{$oldFile->getName()}` did already exist. "
292          . "Overwriting was rejected since new file's Verona-Module-ID (`{$file->getVeronaModuleId()}`) differs from old one (`{$oldFile->getVeronaModuleId()}`)."
293          . "Filenames not according to the Verona-standard are a bad idea anyway and and will be forbidden in the future."
294        );
295        return false;
296      }
297
298      if (!Version::isCompatible($oldFile->getVersion(), $file->getVersion())) {
299        $file->report(
300          'error',
301          "File of name `{$oldFile->getName()}` did already exist. "
302          . "Overwriting was rejected since version conflict between old ({$oldFile->getVersion()}) and new ({$file->getVersion()}) file."
303          . "Filenames not according to the Verona-standard are a bad idea anyway and and will be forbidden in the future."
304        );
305        return false;
306      }
307
308      if (!unlink($targetFilePath)) {
309        $file->report('error', "Could not delete file: `$targetFolder/$localFilePath`");
310        return false;
311      }
312
313      $file->report('warning', "File of name `{$oldFile->getName()}` did already exist and was overwritten.");
314    }
315
316    if (!rename($this->workspacePath . '/' . $localFilePath, $targetFilePath)) {
317      $file->report('error', "Could not move file to `$targetFolder/$localFilePath`");
318      return false;
319    }
320
321    $file->readFileMeta($targetFilePath);
322
323    return true;
324  }
325
326  protected function unpackRootlevelZipArchive(string $fileName): array {
327    $filePath = "$this->workspacePath/$fileName";
328    $extractionFolder = $this->getExtractionDirName($fileName);
329    $extractionPath = "$this->workspacePath/$extractionFolder";
330
331    // sometimes in error cases there are remains from previous attempts
332    if (file_exists($extractionPath) and is_dir($extractionPath)) {
333      Folder::deleteContentsRecursive($extractionPath);
334      rmdir($extractionPath);
335    }
336
337    if (!mkdir($extractionPath)) {
338      throw new Exception("Could not create directory for extracted files: `$extractionPath`");
339    }
340
341    ZIP::extract($filePath, $extractionPath);
342
343    return Folder::getContentsFlat($extractionPath, $extractionFolder);
344  }
345
346  protected function getExtractionDirName(string $fileName): string {
347    return "{$fileName}_Extract";
348  }
349
350  protected function deleteRootlevelFiles(array $relativePaths): void {
351    foreach ($relativePaths as $relativePath) {
352      $filePath = "$this->workspacePath/$relativePath";
353      if (is_dir($filePath)) {
354        Folder::deleteContentsRecursive($filePath);
355        rmdir($filePath);
356      }
357      if (is_file($filePath)) {
358        unlink($filePath);
359      }
360    }
361  }
362
363  public function getFileById(string $type, string $fileId): File {
364    if ($file = $this->workspaceDAO->getFileById($fileId, $type)) {
365      if ($file->isValid()) {
366        return $file;
367      }
368    }
369
370    throw new HttpError("No $type with id `$fileId` found on workspace `$this->workspaceId`!", 404);
371  }
372
373  public function getFileByName(string $type, string $fileName): File {
374    $file = File::get("$this->workspacePath/$type/$fileName", $type);
375
376    if ($file->isValid()) {
377      return $file;
378    }
379
380    throw new HttpError("No $type with name `$fileName` found on workspace `$this->workspaceId`!", 404);
381  }
382
383  public function countFilesOfAllSubFolders(): array {
384    $result = [];
385
386    foreach ($this::subFolders as $type) {
387      $result[$type] = $this->countFiles($type);
388    }
389
390    return $result;
391  }
392
393  public function countFiles(string $type): int {
394    $pattern = ($type == 'Resource') ? "*.*" : "*.[xX][mM][lL]";
395    return count(Folder::glob($this->getOrCreateSubFolderPath($type), $pattern));
396  }
397
398  public function delete(): void {
399    Folder::deleteContentsRecursive($this->workspacePath);
400    rmdir($this->workspacePath);
401  }
402
403  // TODO unit-test
404  public function storeAllFiles(): array {
405    $workspaceCache = new WorkspaceCache($this);
406    $workspaceCache->loadFiles();
407
408    $typeStats = array_fill_keys(Workspace::subFolders, 0);
409    $loginStats = [
410      'added' => 0
411    ];
412    $invalidCount = 0;
413
414    $loginStats['deleted'] = $this->removeVanishedFilesFromDB($workspaceCache);
415
416    $workspaceCache->validate();
417
418    foreach ($workspaceCache->getFiles(true) as $file) {
419      /* @var File $file */
420
421      if (!$file->isValid()) {
422        $invalidCount++;
423      }
424
425      $this->workspaceDAO->storeFile($file);
426      $typeStats[$file->getType()] += 1;
427    }
428
429    foreach ($workspaceCache->getFiles(true) as $file) {
430      $stats = $this->storeFileMeta($file);
431
432      $loginStats['deleted'] += $stats['logins_deleted'];
433      $loginStats['added'] += $stats['logins_added'];
434    }
435
436    return [
437      'valid' => $typeStats,
438      'invalid' => $invalidCount,
439      'logins' => $loginStats
440    ];
441  }
442
443  private function removeVanishedFilesFromDB(WorkspaceCache $workspaceCache): int {
444    $filesInDb = $this->workspaceDAO->getAllFiles();
445    $filesInFolder = $workspaceCache->getFiles(true);
446    $deletedLogins = 0;
447
448    foreach ($filesInDb as $fileSet) {
449      foreach ($fileSet as $file) {
450        /* @var File $file */
451
452        if (!isset($filesInFolder[$file->getPath()])) {
453          $this->workspaceDAO->deleteFile($file);
454          $deletedLogins += $this->workspaceDAO->deleteLoginSource($file->getName());
455        }
456      }
457    }
458
459    return $deletedLogins;
460  }
461
462  // TODO unit-test
463  private function storeFileMeta(File $file): ?array {
464    $stats = [
465      'logins_deleted' => 0,
466      'logins_added' => 0,
467      'resource_packages_installed' => 0,
468      'attachments_noted' => 0,
469      'resolved_relations' => 0,
470      'relations_resolved' => 0,
471      'relations_unresolved' => 0
472    ];
473
474    if (!$file->isValid()) {
475      return $stats;
476    }
477
478    if ($file::canBeRelationSubject) {
479      list($relationsUnresolved) = $this->workspaceDAO->storeRelations($file);
480      $stats['relations_resolved'] = count($file->getRelations()) - count($relationsUnresolved);
481      $stats['relations_unresolved'] = count($relationsUnresolved);
482    }
483
484    if (is_a($file, XMLFileTesttakers::class)) {
485      list($deleted, $added) = $this->workspaceDAO->updateLoginSource($file->getName(), $file->getAllLogins());
486      $stats['logins_deleted'] = $deleted;
487      $stats['logins_added'] = $added;
488    }
489
490    if (is_a($file, ResourceFile::class) and $file->isPackage()) {
491      $file->installPackage();
492      $stats['resource_packages_installed'] = 1;
493    }
494
495    if (is_a($file, XMLFileBooklet::class)) {
496      $requestedAttachments = $this->getRequestedAttachments($file);
497      $this->workspaceDAO->updateUnitDefsAttachments($file->getId(), $requestedAttachments);
498      $stats['attachments_noted'] = count($requestedAttachments);
499    }
500
501    return $stats;
502  }
503
504  public function getRequestedAttachments(XMLFileBooklet $booklet): array {
505    if (!$booklet->isValid()) {
506      return [];
507    }
508
509    $requestedAttachments = [];
510    foreach ($booklet->getUnitIds() as $uniId) {
511      $unit = $this->getFileById('Unit', $uniId);
512      /* @var $unit XMLFileUnit */
513      $requestedAttachments = array_merge($requestedAttachments, $unit->getRequestedAttachments());
514    }
515    return $requestedAttachments;
516  }
517
518  public function getFileRelations(File $file): array {
519    return $this->workspaceDAO->getFileRelations($file->getName(), $file->getType());
520  }
521
522  /** checks files that are depending on the current file, upstream */
523  private function updateDependentFiles(File $file): void {
524    $relatingFiles = $this->workspaceDAO->getDependentFilesByTypes($file, ['Booklet']);
525
526    foreach ($relatingFiles as $fileset) {
527      foreach ($fileset as $file) {
528        $requestedAttachments = $this->getRequestedAttachments($file);
529        $this->workspaceDAO->updateUnitDefsAttachments($file->getId(), $requestedAttachments);
530      }
531    }
532
533  }
534
535  public function getBookletResourcePaths(string $bookletId): array {
536    $resourceList = $this->workspaceDAO->getBookletResourcePaths($bookletId);
537    $resourceListStructured = [];
538    foreach ($resourceList as $item) {
539      $resourceListStructured[$item['id']][$item['relationship_type']][] = "{$item['type']}/{$item['name']}";
540      $path = "/ws_$this->workspaceId/{$item['type']}/{$item['name']}";
541      CacheService::storeFile($path);
542    }
543    return $resourceListStructured;
544  }
545
546  public function getWorkspaceHash(): string {
547    return hash('XXH3', serialize(self::getHashOfAllFiles($this->getWorkspacePath())));
548  }
549
550  public function setWorkspaceHash(): void {
551    $this->workspaceDAO->setWorkspaceHash($this->getWorkspaceHash());
552  }
553
554  private function loadFilesIntoCache(WorkspaceCache $workspaceCache, FileType $type): void {
555    foreach (FileType::getDependenciesOfType($type) as $dependingType) {
556      $files = $this->workspaceDAO->getAllFilesWhere(['type' => $dependingType]);
557      foreach ($files[$dependingType] as $file) {
558        $workspaceCache->addFile($dependingType, $file);
559      }
560    }
561  }
562}