Completed
Push — feature/EVO-4597-rabbitmq-hand... ( e70ebe...b48550 )
by
unknown
103:19 queued 97:29
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 402
    public function __construct(
98
        CacheProvider $cache,
99
        $cacheDir,
100
        $loaderId
101
    ) {
102 402
        $this->cache = $cache;
103 402
        $this->cacheDirTranslations = $cacheDir . '/translations';
104
105
        // cache keys
106 402
        $this->cacheKey = 'i18n.addedTranslations';
107 402
        $this->cacheKeyFinalResource = 'i18n.finalResources';
108
109 402
        $this->loaderId = $loaderId;
110 402
        $this->resourceDir = __DIR__.'/../Resources/translations/';
111
112
        // do we have existing resources?
113 402
        if ($this->cache->contains($this->cacheKey)) {
114 392
            $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 196
        }
116 402
    }
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 398
    public function getResources($resources)
152
    {
153 398
        $finalResources = null;
154
155
        // do we have a full resource map in the cache already? (full = translator + additions)
156 398
        if ($this->cache->contains($this->cacheKeyFinalResource)) {
157 388
            $finalResources = $this->cache->fetch($this->cacheKeyFinalResource);
158 194
        }
159
160
        // do we have cached additions?
161 398
        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 12
            $finalResources = $this->mergeResourcesWithAdditions($resources);
164
165
            // cache it
166 12
            $this->cache->save($this->cacheKeyFinalResource, $finalResources);
167 6
        }
168
169
        // so, did we got anything?
170 398
        if (is_array($finalResources)) {
171 388
            $resources = $finalResources;
172 194
        }
173
174 398
        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 140
    public function invalidate($locale, $domain)
194
    {
195 140
        $filename = sprintf('%s.%s.%s', $domain, $locale, $this->loaderId);
196
197 140
        if (!isset($this->addedResources[$locale]) || !in_array($filename, $this->addedResources[$locale])) {
198 118
            $this->addedResources[$locale][] = $filename;
199 118
            $this->isDirty = true;
200 59
        }
201
202 140
        $this->invalidations[$locale] = '';
203 140
    }
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 12
    private function mergeResourcesWithAdditions($resources)
217
    {
218 12
        foreach ($this->addedResources as $locale => $files) {
219 12
            foreach ($files as $file) {
220 12
                $isExistent = false;
221 12
                if (isset($resources[$locale])) {
222 12
                    $hits = preg_grep('/\/'.str_replace('.', '\\.', $file).'$/', $resources[$locale]);
223 12
                    if (count($hits) > 0) {
224 4
                        $isExistent = true;
225 2
                    }
226 6
                }
227
228 12
                if (!$isExistent) {
229 12
                    $resourceFile = $this->resourceDir.$file;
230
231
                    // make sure the file exists
232 12
                    $fs = new Filesystem();
233 12
                    $fs->touch($resourceFile);
234
235 12
                    $resources[$locale][] = $resourceFile;
236 6
                }
237 6
            }
238 6
        }
239 12
        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 12
    private function persistAdditions()
249
    {
250 12
        $this->cache->save($this->cacheKey, $this->addedResources);
251
252
        // remove full map from cache
253 12
        $this->cache->delete($this->cacheKeyFinalResource);
254 12
    }
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 398
    private function processInvalidations()
263
    {
264 398
        if (empty($this->invalidations)) {
265 380
            return;
266
        }
267
268 48
        $fs = new Filesystem();
269 48
        $localesToClean = array_keys($this->invalidations);
270 48
        $deleteRegex = '/^catalogue\.(['.implode('|', $localesToClean).'])/';
271
272
        try {
273 48
            $finder = new Finder();
274
            $finder
275 48
                ->files()
276 48
                ->in($this->cacheDirTranslations)
277 48
                ->name($deleteRegex);
278
279 48
            foreach ($finder as $file) {
280 46
                $fs->remove($file->getRealPath());
281 24
            }
282 24
        } catch (\InvalidArgumentException $e) {
283
            // happens when cache is non-existent
284
        }
285 48
    }
286
287
    /**
288
     * processes all pending operations
289
     *
290
     * @return void
291
     */
292 398
    public function processPending()
293
    {
294 398
        $this->processInvalidations();
295
296 398
        if ($this->isDirty === true) {
297 12
            $this->persistAdditions();
298 6
        }
299 398
    }
300
}
301