Completed
Pull Request — develop (#366)
by Lucas
34:34 queued 04:38
created

I18nCacheUtils::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 2
Metric Value
dl 0
loc 20
ccs 11
cts 11
cp 1
rs 9.4285
cc 2
eloc 12
nc 2
nop 3
crap 2
1
<?php
2
/**
3
 * service for i18n stuff relating to symfony translation loader and its caching.
4
 *
5
 * it basically receives the 'resource_files' array from a Translator (that is built statically in the Container)
6
 * and adds runtime elements to it. behind the scenes, it invalidates translator caches and creates the resource files.
7
 * all with the idea that new 'translation domains' get added at runtime without the need of a container rebuild.
8
 * only then it will be actually loaded by the translation loaders and end up in the final catalogue.
9
 * caches are in place to optimize the performance impact.
10
 */
11
12
namespace Graviton\I18nBundle\Service;
13
14
use Doctrine\Common\Cache\CacheProvider;
15
use Symfony\Component\Filesystem\Filesystem;
16
use Symfony\Component\Finder\Finder;
17
18
/**
19
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
20
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
21
 * @link     http://swisscom.ch
22
 */
23
class I18nCacheUtils
24
{
25
26
    /**
27
     * doctrine cache
28
     *
29
     * @var CacheProvider
30
     */
31
    private $cache;
32
33
    /**
34
     * full path to translations cache
35
     *
36
     * @var string
37
     */
38
    private $cacheDirTranslations;
39
40
    /**
41
     * the cache key we use for our addition array
42
     *
43
     * @var string
44
     */
45
    private $cacheKey;
46
47
    /**
48
     * the cache key we use for the full resource map
49
     *
50
     * @var string
51
     */
52
    private $cacheKeyFinalResource;
53
54
    /**
55
     * the loader id suffix (like 'odm')
56
     *
57
     * @var string
58
     */
59
    private $loaderId;
60
61
    /**
62
     * full path to the bundle resource dir
63
     *
64
     * @var string
65
     */
66
    private $resourceDir;
67
68
    /**
69
     * this map contains all added resources (added to all translator knows already)
70
     *
71
     * @var array
72
     */
73
    private $addedResources = array();
74
75
    /**
76
     * if the postpersistListener invalidates something, it will be put here
77
     *
78
     * @var array
79
     */
80
    private $invalidations = array();
81
82
    /**
83
     * a boolean flag telling us if a new map persist is necessary
84
     * (that is, when some new addition has been added that needs a resource map regeneration)
85
     *
86
     * @var boolean
87
     */
88
    private $isDirty = false;
89
90
    /**
91
     * Constructor
92
     *
93
     * @param CacheProvider $cache    cache
94
     * @param string        $cacheDir full path to cache dir
95
     * @param string        $loaderId loader id suffix
96
     */
97 198
    public function __construct(
98
        CacheProvider $cache,
99
        $cacheDir,
100
        $loaderId
101
    ) {
102 198
        $this->cache = $cache;
103 198
        $this->cacheDirTranslations = $cacheDir . '/translations';
104
105
        // cache keys
106 198
        $this->cacheKey = 'i18n.addedTranslations';
107 198
        $this->cacheKeyFinalResource = 'i18n.finalResources';
108
109 198
        $this->loaderId = $loaderId;
110 198
        $this->resourceDir = __DIR__.'/../Resources/translations/';
111
112
        // do we have existing resources?
113 198
        if ($this->cache->contains($this->cacheKey)) {
114 195
            $this->addedResources = $this->cache->fetch($this->cacheKey);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->cache->fetch($this->cacheKey) of type * is incompatible with the declared type array of property $addedResources.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
115 195
        }
116 198
    }
117
118
    /**
119
     * Gets the cache instance
120
     *
121
     * @return CacheProvider cache
122
     */
123
    public function getCache()
124
    {
125
        return $this->cache;
126
    }
127
128
    /**
129
     * sets the resource dir
130
     *
131
     * @param string $dir resource dir
132
     *
133
     * @return void
134
     */
135 6
    public function setResourceDir($dir)
136
    {
137 6
        $this->resourceDir = $dir;
138
    }
139
140
    /**
141
     * this shall be called by a Translator.
142
     * it adds our additions to the already existent ones in the Translator and returns it.
143
     * as this is called quite often, we cache the final result (the full map including the translator resources)
144
     * and only regenerate that when a *new* domain has been added. basic invalidations by the PostPersistListener
145
     * will *not* result in a rebuild here - only if a new domain has been added.
146
     *
147
     * @param array $resources the resources array of the translator
148
     *
149
     * @return array the finalized map containing translator resources and our additions
150
     */
151 196
    public function getResources($resources)
152
    {
153 196
        $finalResources = null;
154
155
        // do we have a full resource map in the cache already? (full = translator + additions)
156 196
        if ($this->cache->contains($this->cacheKeyFinalResource)) {
157 193
            $finalResources = $this->cache->fetch($this->cacheKeyFinalResource);
158 193
        }
159
160
        // do we have cached additions?
161 196
        if (!is_array($finalResources) && $this->cache->contains($this->cacheKey)) {
162
            // merge the two together, always keep an eye to not duplicate (paths are different!)
163 6
            $finalResources = $this->mergeResourcesWithAdditions($resources);
164
165
            // cache it
166 6
            $this->cache->save($this->cacheKeyFinalResource, $finalResources);
167 6
        }
168
169
        // so, did we got anything?
170 196
        if (is_array($finalResources)) {
171 193
            $resources = $finalResources;
172 193
        }
173
174 196
        return $resources;
175
    }
176
177
    /**
178
     * will be executed on the event dispatched by PostPersistTranslatableListener.
179
     * if someone invalidates a locale & domain pair, this will lead to:
180
     * - removal of the symfony translation cache files
181
     * (if this pair has never been seen)
182
     * - creation of the resource files ("trigger files")
183
     * - a regeneration of the full resource map for the translator
184
     *
185
     * please note that calling invalidate() will do the above mentioned in a lazy way
186
     * when the kernel.terminate event fires.
187
     *
188
     * @param string $locale locale (de,en,fr)
189
     * @param string $domain domain
190
     *
191
     * @return void
192
     */
193 68
    public function invalidate($locale, $domain)
194
    {
195 68
        $filename = sprintf('%s.%s.%s', $domain, $locale, $this->loaderId);
196
197 68
        if (!isset($this->addedResources[$locale]) || !in_array($filename, $this->addedResources[$locale])) {
198 57
            $this->addedResources[$locale][] = $filename;
199 57
            $this->isDirty = true;
200 57
        }
201
202 68
        $this->invalidations[$locale] = '';
203 68
    }
204
205
    /**
206
     * Merges the cached additions with the one the Translator already has.
207
     * I need to use preg_grep() here as I'm unable to compose an absolute path that is
208
     * identical to the one the Translator would have already as I have to deal with relative
209
     * paths here (and rightfully so). This shouldn't hurt too much as the end result is cached
210
     * and only redone if something changes.
211
     *
212
     * @param array $resources resources
213
     *
214
     * @return array finalized full map
215
     */
216 6
    private function mergeResourcesWithAdditions($resources)
217
    {
218 6
        foreach ($this->addedResources as $locale => $files) {
219 6
            foreach ($files as $file) {
220 6
                $isExistent = false;
221 6
                if (isset($resources[$locale])) {
222 6
                    $hits = preg_grep('/\/'.str_replace('.', '\\.', $file).'$/', $resources[$locale]);
223 6
                    if (count($hits) > 0) {
224 2
                        $isExistent = true;
225 2
                    }
226 6
                }
227
228 6
                if (!$isExistent) {
229 6
                    $resourceFile = $this->resourceDir.$file;
230
231
                    // make sure the file exists
232 6
                    $fs = new Filesystem();
233 6
                    $fs->touch($resourceFile);
234
235 6
                    $resources[$locale][] = $resourceFile;
236 6
                }
237 6
            }
238 6
        }
239 6
        return $resources;
240
    }
241
242
    /**
243
     * saves our addition array to our cache and removes the full map from the cache
244
     * leading to a regeneration of the map.
245
     *
246
     * @return void
247
     */
248 6
    private function persistAdditions()
249
    {
250 6
        $this->cache->save($this->cacheKey, $this->addedResources);
251
252
        // remove full map from cache
253 6
        $this->cache->delete($this->cacheKeyFinalResource);
254 6
    }
255
256
    /**
257
     * processes all queued cache invalidations for the symfony translation cache.
258
     * this is now only 1 Finder search for a single request.
259
     *
260
     * @return void
261
     */
262 196
    private function processInvalidations()
263
    {
264 196
        if (empty($this->invalidations)) {
265 187
            return;
266
        }
267
268 24
        $fs = new Filesystem();
269 24
        $localesToClean = array_keys($this->invalidations);
270 24
        $deleteRegex = '/^catalogue\.(['.implode('|', $localesToClean).'])/';
271
272
        try {
273 24
            $finder = new Finder();
274
            $finder
275 24
                ->files()
276 24
                ->in($this->cacheDirTranslations)
277 24
                ->name($deleteRegex);
278
279 24
            foreach ($finder as $file) {
280 22
                $fs->remove($file->getRealPath());
281 24
            }
282 24
        } catch (\InvalidArgumentException $e) {
283
            // happens when cache is non-existent
284
        }
285 24
    }
286
287
    /**
288
     * processes all pending operations
289
     *
290
     * @return void
291
     */
292 196
    public function processPending()
293
    {
294 196
        $this->processInvalidations();
295
296 196
        if ($this->isDirty === true) {
297 6
            $this->persistAdditions();
298 6
        }
299 196
    }
300
}
301