Completed
Pull Request — develop (#337)
by Bastian
06:24
created

I18nCacheUtils::setResourceDir()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1.037

Importance

Changes 3
Bugs 0 Features 2
Metric Value
dl 0
loc 4
ccs 2
cts 3
cp 0.6667
rs 10
c 3
b 0
f 2
cc 1
eloc 2
nc 1
nop 1
crap 1.037
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 188
    public function __construct(
98
        CacheProvider $cache,
99
        $cacheDir,
100
        $loaderId
101
    ) {
102 188
        $this->cache = $cache;
103 188
        $this->cacheDirTranslations = $cacheDir . '/translations';
104
105
        // cache keys
106 188
        $this->cacheKey = 'i18n.addedTranslations';
107 188
        $this->cacheKeyFinalResource = 'i18n.finalResources';
108
109 188
        $this->loaderId = $loaderId;
110 188
        $this->resourceDir = __DIR__.'/../Resources/translations/';
111
112
        // do we have existing resources?
113 188
        if ($this->cache->contains($this->cacheKey)) {
114 185
            $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 185
        }
116 188
    }
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 186
    public function getResources($resources)
152
    {
153 186
        $finalResources = null;
154
155
        // do we have a full resource map in the cache already? (full = translator + additions)
156 186
        if ($this->cache->contains($this->cacheKeyFinalResource)) {
157 183
            $finalResources = $this->cache->fetch($this->cacheKeyFinalResource);
158 183
        }
159
160
        // do we have cached additions?
161 186
        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 186
        if (is_array($finalResources)) {
171 183
            $resources = $finalResources;
172 183
        }
173
174 186
        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 61
    public function invalidate($locale, $domain)
194
    {
195 61
        $filename = sprintf('%s.%s.%s', $domain, $locale, $this->loaderId);
196
197 61
        if (!isset($this->addedResources[$locale]) || !in_array($filename, $this->addedResources[$locale])) {
198 52
            $this->addedResources[$locale][] = $filename;
199 52
            $this->isDirty = true;
200 52
        }
201
202 61
        $this->invalidations[$locale] = '';
203 61
    }
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 186
    private function processInvalidations()
263
    {
264 186
        if (empty($this->invalidations)) {
265 182
            return;
266
        }
267
268 19
        $fs = new Filesystem();
269 19
        $localesToClean = array_keys($this->invalidations);
270 19
        $deleteRegex = '/^catalogue\.(['.implode('|', $localesToClean).'])/';
271
272
        try {
273 19
            $finder = new Finder();
274
            $finder
275 19
                ->files()
276 19
                ->in($this->cacheDirTranslations)
277 19
                ->name($deleteRegex);
278
279 19
            foreach ($finder as $file) {
280 19
                $fs->remove($file->getRealPath());
281 19
            }
282 19
        } catch (\InvalidArgumentException $e) {
1 ignored issue
show
Unused Code introduced by
This catchblock is empty and will swallow any caught exception.

This check looks for ``catch` blocks that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

Empty catch blocks will swallow any caught exception, sometimes causing bugs in your code that are very hard to debug. Consider logging the exception to a debug log or re-throwing it in some way, shape or form.

Loading history...
283
            // happens when cache is non-existent
284
        }
285 19
    }
286
287
    /**
288
     * processes all pending operations
289
     *
290
     * @return void
291
     */
292 186
    public function processPending()
293
    {
294 186
        $this->processInvalidations();
295
296 186
        if ($this->isDirty === true) {
297 6
            $this->persistAdditions();
298 6
        }
299 186
    }
300
}
301