FileStorage.php 9.84 KB
Newer Older
1
2
3
4
<?php

namespace Drupal\Core\Config;

5
use Drupal\Component\FileCache\FileCacheFactory;
6
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
7
use Drupal\Core\File\FileSystemInterface;
8
use Drupal\Core\Serialization\Yaml;
9

10
/**
11
 * Defines the file storage.
12
 */
13
class FileStorage implements StorageInterface {
14

15
16
17
18
19
20
21
  /**
   * The storage collection.
   *
   * @var string
   */
  protected $collection;

22
  /**
23
   * The filesystem path for configuration objects.
24
   *
25
   * @var string
26
   */
27
  protected $directory = '';
28

29
30
31
32
33
34
35
  /**
   * The file cache object.
   *
   * @var \Drupal\Component\FileCache\FileCacheInterface
   */
  protected $fileCache;

36
  /**
37
   * Constructs a new FileStorage.
38
39
40
   *
   * @param string $directory
   *   A directory path to use for reading and writing of configuration files.
41
42
43
   * @param string $collection
   *   (optional) The collection to store configuration in. Defaults to the
   *   default collection.
44
   */
45
  public function __construct($directory, $collection = StorageInterface::DEFAULT_COLLECTION) {
46
    $this->directory = $directory;
47
    $this->collection = $collection;
48
    // Use a NULL File Cache backend by default. This will ensure only the
49
    // internal static caching of FileCache is used and thus avoids blowing up
50
51
    // the APCu cache.
    $this->fileCache = FileCacheFactory::get('config', ['cache_backend_class' => NULL]);
52
53
54
  }

  /**
55
   * Returns the path to the configuration file.
56
   *
57
58
   * @return string
   *   The path to the configuration file.
59
   */
60
  public function getFilePath($name) {
61
    return $this->getCollectionDirectory() . '/' . $name . '.' . static::getFileExtension();
62
63
64
65
66
67
68
69
70
71
  }

  /**
   * Returns the file extension used by the file storage for all configuration files.
   *
   * @return string
   *   The file extension.
   */
  public static function getFileExtension() {
    return 'yml';
72
73
  }

74
75
76
77
  /**
   * Check if the directory exists and create it if not.
   */
  protected function ensureStorage() {
78
    $dir = $this->getCollectionDirectory();
79
    $success = $this->getFileSystem()->prepareDirectory($dir, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
80
81
    // Only create .htaccess file in root directory.
    if ($dir == $this->directory) {
82
      $success = $success && file_save_htaccess($this->directory, TRUE, TRUE);
83
    }
84
    if (!$success) {
85
      throw new StorageException('Failed to create config directory ' . $dir);
86
87
88
89
    }
    return $this;
  }

90
  /**
91
   * {@inheritdoc}
92
   */
93
94
  public function exists($name) {
    return file_exists($this->getFilePath($name));
95
96
97
  }

  /**
98
   * Implements Drupal\Core\Config\StorageInterface::read().
99
   *
100
   * @throws \Drupal\Core\Config\UnsupportedDataTypeConfigException
101
   */
102
103
  public function read($name) {
    if (!$this->exists($name)) {
104
      return FALSE;
105
    }
106

107
    $filepath = $this->getFilePath($name);
108
109
110
111
    if ($data = $this->fileCache->get($filepath)) {
      return $data;
    }

112
    $data = file_get_contents($filepath);
113
114
115
116
    try {
      $data = $this->decode($data);
    }
    catch (InvalidDataTypeException $e) {
117
      throw new UnsupportedDataTypeConfigException('Invalid data type in config ' . $name . ', found in file' . $filepath . ' : ' . $e->getMessage());
118
    }
119
120
    $this->fileCache->set($filepath, $data);

121
    return $data;
122
123
  }

124
125
126
127
  /**
   * {@inheritdoc}
   */
  public function readMultiple(array $names) {
128
    $list = [];
129
130
131
132
133
134
135
136
    foreach ($names as $name) {
      if ($data = $this->read($name)) {
        $list[$name] = $data;
      }
    }
    return $list;
  }

137
  /**
138
   * {@inheritdoc}
139
   */
140
  public function write($name, array $data) {
141
    try {
142
      $encoded_data = $this->encode($data);
143
    }
144
    catch (InvalidDataTypeException $e) {
145
      throw new StorageException("Invalid data type in config $name: {$e->getMessage()}");
146
147
    }

148
    $target = $this->getFilePath($name);
149
    $status = @file_put_contents($target, $encoded_data);
150
    if ($status === FALSE) {
151
      // Try to make sure the directory exists and try writing again.
152
      $this->ensureStorage();
153
      $status = @file_put_contents($target, $encoded_data);
154
    }
155
156
    if ($status === FALSE) {
      throw new StorageException('Failed to write configuration file: ' . $this->getFilePath($name));
157
    }
158
    else {
159
      $this->getFileSystem()->chmod($target);
160
    }
161
162
163

    $this->fileCache->set($target, $data);

164
    return TRUE;
165
166
167
  }

  /**
168
   * {@inheritdoc}
169
   */
170
171
172
173
  public function delete($name) {
    if (!$this->exists($name)) {
      return FALSE;
    }
174
    $this->fileCache->delete($this->getFilePath($name));
175
    return $this->getFileSystem()->unlink($this->getFilePath($name));
176
  }
177

178
  /**
179
   * {@inheritdoc}
180
181
182
183
   */
  public function rename($name, $new_name) {
    $status = @rename($this->getFilePath($name), $this->getFilePath($new_name));
    if ($status === FALSE) {
184
      return FALSE;
185
    }
186
187
    $this->fileCache->delete($this->getFilePath($name));
    $this->fileCache->delete($this->getFilePath($new_name));
188
189
190
    return TRUE;
  }

191
  /**
192
   * {@inheritdoc}
193
   */
194
  public function encode($data) {
195
    return Yaml::encode($data);
196
197
198
  }

  /**
199
   * {@inheritdoc}
200
   */
201
  public function decode($raw) {
202
    $data = Yaml::decode($raw);
203
204
205
    // A simple string is valid YAML for any reason.
    if (!is_array($data)) {
      return FALSE;
206
    }
207
    return $data;
208
209
210
  }

  /**
211
   * {@inheritdoc}
212
   */
213
  public function listAll($prefix = '') {
214
    $dir = $this->getCollectionDirectory();
215
    if (!is_dir($dir)) {
216
      return [];
217
    }
218
    $extension = '.' . static::getFileExtension();
219
220
221
222
223
224

    // glob() directly calls into libc glob(), which is not aware of PHP stream
    // wrappers. Same for \GlobIterator (which additionally requires an absolute
    // realpath() on Windows).
    // @see https://github.com/mikey179/vfsStream/issues/2
    $files = scandir($dir);
225

226
    $names = [];
227
    $pattern = '/^' . preg_quote($prefix, '/') . '.*' . preg_quote($extension, '/') . '$/';
228
    foreach ($files as $file) {
229
      if ($file[0] !== '.' && preg_match($pattern, $file)) {
230
231
        $names[] = basename($file, $extension);
      }
232
233
234
    }

    return $names;
235
  }
236
237

  /**
238
   * {@inheritdoc}
239
240
241
   */
  public function deleteAll($prefix = '') {
    $files = $this->listAll($prefix);
242
    $success = !empty($files);
243
244
245
246
247
    foreach ($files as $name) {
      if (!$this->delete($name) && $success) {
        $success = FALSE;
      }
    }
248
249
250
    if ($success && $this->collection != StorageInterface::DEFAULT_COLLECTION) {
      // Remove empty directories.
      if (!(new \FilesystemIterator($this->getCollectionDirectory()))->valid()) {
251
        $this->getFileSystem()->rmdir($this->getCollectionDirectory());
252
253
      }
    }
254
255
    return $success;
  }
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277

  /**
   * {@inheritdoc}
   */
  public function createCollection($collection) {
    return new static(
      $this->directory,
      $collection
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getCollectionName() {
    return $this->collection;
  }

  /**
   * {@inheritdoc}
   */
  public function getAllCollectionNames() {
larowlan's avatar
larowlan committed
278
279
280
    if (!is_dir($this->directory)) {
      return [];
    }
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
    $collections = $this->getAllCollectionNamesHelper($this->directory);
    sort($collections);
    return $collections;
  }

  /**
   * Helper function for getAllCollectionNames().
   *
   * If the file storage has the following subdirectory structure:
   *   ./another_collection/one
   *   ./another_collection/two
   *   ./collection/sub/one
   *   ./collection/sub/two
   * this function will return:
   * @code
   *   array(
   *     'another_collection.one',
   *     'another_collection.two',
   *     'collection.sub.one',
   *     'collection.sub.two',
   *   );
   * @endcode
   *
   * @param string $directory
   *   The directory to check for sub directories. This allows this
   *   function to be used recursively to discover all the collections in the
larowlan's avatar
larowlan committed
307
308
   *   storage. It is the responsibility of the caller to ensure the directory
   *   exists.
309
310
311
312
313
   *
   * @return array
   *   A list of collection names contained within the provided directory.
   */
  protected function getAllCollectionNamesHelper($directory) {
314
    $collections = [];
315
    $pattern = '/\.' . preg_quote($this->getFileExtension(), '/') . '$/';
316
317
318
319
320
321
322
323
324
325
326
327
328
329
    foreach (new \DirectoryIterator($directory) as $fileinfo) {
      if ($fileinfo->isDir() && !$fileinfo->isDot()) {
        $collection = $fileinfo->getFilename();
        // Recursively call getAllCollectionNamesHelper() to discover if there
        // are subdirectories. Subdirectories represent a dotted collection
        // name.
        $sub_collections = $this->getAllCollectionNamesHelper($directory . '/' . $collection);
        if (!empty($sub_collections)) {
          // Build up the collection name by concatenating the subdirectory
          // names with the current directory name.
          foreach ($sub_collections as $sub_collection) {
            $collections[] = $collection . '.' . $sub_collection;
          }
        }
330
        // Check that the collection is valid by searching it for configuration
331
332
        // objects. A directory without any configuration objects is not a valid
        // collection.
333
334
        // @see \Drupal\Core\Config\FileStorage::listAll()
        foreach (scandir($directory . '/' . $collection) as $file) {
335
          if ($file[0] !== '.' && preg_match($pattern, $file)) {
336
337
338
            $collections[] = $collection;
            break;
          }
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
        }
      }
    }
    return $collections;
  }

  /**
   * Gets the directory for the collection.
   *
   * @return string
   *   The directory for the collection.
   */
  protected function getCollectionDirectory() {
    if ($this->collection == StorageInterface::DEFAULT_COLLECTION) {
      $dir = $this->directory;
    }
    else {
      $dir = $this->directory . '/' . str_replace('.', '/', $this->collection);
    }
    return $dir;
  }

361
362
363
364
365
366
367
368
369
370
  /**
   * Returns file system service.
   *
   * @return \Drupal\Core\File\FileSystemInterface
   *   The file system service.
   */
  private function getFileSystem() {
    return \Drupal::service('file_system');
  }

371
}