Passed
Push — master ( a9f547...49cca7 )
by Raffael
04:18
created

TemplateHandler   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 263
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 30
lcom 1
dl 0
loc 263
ccs 0
cts 132
cp 0
rs 10
c 0
b 0
f 0
cbo 5

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
B setOptions() 0 20 5
A getBody() 0 4 1
A getSubject() 0 4 1
A renderTemplate() 0 13 1
A loadLocale() 0 19 4
B getLocale() 0 23 5
A parseString() 0 20 4
A decorate() 0 10 2
A decorateNode() 0 12 1
A decorateUser() 0 12 1
A getArrayValue() 0 17 4
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2018 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\App\Notification;
13
14
use Balloon\Filesystem\Node\AttributeDecorator;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Balloon\App\Notification\AttributeDecorator.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
15
use Balloon\Filesystem\Node\NodeInterface;
16
use Balloon\Server;
17
use Balloon\Server\AttributeDecorator as RoleAttributeDecorator;
18
use Balloon\Server\User;
19
use InvalidArgumentException;
20
21
class TemplateHandler
22
{
23
    /**
24
     * Attribute decorator.
25
     *
26
     * @var AttributeDecorator
27
     */
28
    protected $decorator;
29
30
    /**
31
     * Role Attribute decorator.
32
     *
33
     * @var RoleAttributeDecorator
34
     */
35
    protected $role_decorator;
36
37
    /**
38
     * Asset directory.
39
     *
40
     * @var string
41
     */
42
    protected $asset_dir = __DIR__.DIRECTORY_SEPARATOR.'assets';
43
44
    /**
45
     * Fallback locale.
46
     *
47
     * @var string
48
     */
49
    protected $fallback_locale = 'en_US';
50
51
    /**
52
     * Loaded locales.
53
     *
54
     * @var array
55
     */
56
    protected $locales = [];
57
58
    /**
59
     * Server.
60
     *
61
     * @var string
62
     */
63
    protected $server;
64
65
    /**
66
     * Server context.
67
     *
68
     * @var array
69
     */
70
    protected $context = [];
71
72
    /**
73
     * Constructor.
74
     */
75
    public function __construct(Server $server, AttributeDecorator $decorator, RoleAttributeDecorator $role_decorator)
76
    {
77
        $this->decorator = $decorator;
78
        $this->role_decorator = $role_decorator;
79
        $this->context['server_url'] = $server->getServerUrl();
80
    }
81
82
    /**
83
     * Set config.
84
     *
85
     * @param iterable $config
86
     *
87
     * @return TemplateHandler
88
     */
89
    public function setOptions(?Iterable $config = null): self
90
    {
91
        if (null === $config) {
92
            return $this;
93
        }
94
95
        foreach ($config as $option => $value) {
96
            switch ($option) {
97
                case 'asset_dir':
98
                case 'fallback_locale':
99
                    $this->{$option} = (string) $value;
100
101
                break;
102
                default:
103
                    throw new InvalidArgumentException('invalid option '.$option.' given');
104
             }
105
        }
106
107
        return $this;
108
    }
109
110
    /**
111
     * Parse body.
112
     */
113
    public function getBody(string $notification, array $context = []): string
114
    {
115
        return $this->parseString($notification, 'body', $context + $this->context);
116
    }
117
118
    /**
119
     * Parse subject.
120
     */
121
    public function getSubject(string $notification, array $context = []): string
122
    {
123
        return $this->parseString($notification, 'subject', $context + $this->context);
124
    }
125
126
    /**
127
     * Render template.
128
     */
129
    public function renderTemplate(string $notification, string $template, array $context = []): string
130
    {
131
        $path = $this->asset_dir.DIRECTORY_SEPARATOR.'templates'.DIRECTORY_SEPARATOR.$template;
132
133
        $context += [
134
            'subject' => $this->parseString($notification, 'subject', $context + $this->context),
135
            'body' => $this->parseString($notification, 'body', $context + $this->context),
136
        ];
137
138
        $template = new Template($path, $this->getLocale($context), $context + $this->context);
139
140
        return $template->render();
141
    }
142
143
    /**
144
     * Load locale.
145
     */
146
    protected function loadLocale(string $locale): ?array
147
    {
148
        if (isset($this->locales[$locale])) {
149
            return $this->locales[$locale];
150
        }
151
152
        $path = $this->asset_dir.DIRECTORY_SEPARATOR.'locales'.DIRECTORY_SEPARATOR.$locale.'.json';
153
        if (is_readable($path)) {
154
            $i18n = json_decode(file_get_contents($path));
0 ignored issues
show
Unused Code introduced by
$i18n is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
155
156
            if (json_last_error() !== 0) {
157
                throw new Exception\TemplateInvalidLocale('locale '.$locale.' is invalid json');
158
            }
159
160
            return $this->locales[$locale] = json_decode(file_get_contents($path), true);
161
        }
162
163
        return null;
164
    }
165
166
    /**
167
     * Get correct locale for context.
168
     */
169
    protected function getLocale(array $context)
170
    {
171
        $locale = null;
172
173
        if (isset($context['user'])) {
174
            $locale = $context['user']->getAttributes()['locale'];
175
        } elseif (isset($context['sender'])) {
176
            $locale = $context['sender']->getAttributes()['locale'];
177
        }
178
179
        $i18n = $this->loadLocale($locale);
180
181
        if ($i18n === null) {
182
            $locale = $this->fallback_locale;
183
            $i18n = $this->loadLocale($locale);
184
        }
185
186
        if ($i18n === null) {
187
            throw new Exception\TemplateInvalidLocale('locale '.$locale.' does not exists');
188
        }
189
190
        return $i18n;
191
    }
192
193
    /**
194
     * Parse locale string.
195
     */
196
    protected function parseString(string $notification, string $type, array $context): string
197
    {
198
        $i18n = $this->getLocale($context);
199
200
        if (!isset($i18n['type'][$notification][$type])) {
201
            throw new Exception\TemplateInvalidLocale('locale does not have a type '.$type);
202
        }
203
204
        $string = $i18n['type'][$notification][$type];
205
206
        if (isset($context['node'])) {
207
            $string = $this->decorateNode($string, $context['node']);
208
        }
209
210
        if (isset($context['user'])) {
211
            $string = $this->decorateUser($string, $context['user']);
212
        }
213
214
        return $this->decorate($string, $context);
215
    }
216
217
    /**
218
     * Replace variables.
219
     */
220
    protected function decorate(string $template, array $context): string
221
    {
222
        return preg_replace_callback('/\{([^}\.]*)\}/', function ($match) use ($context) {
223
            $key = $match[1];
224
225
            if (isset($context[$key])) {
226
                return $context[$key];
227
            }
228
        }, $template);
229
    }
230
231
    /**
232
     * Replace variables.
233
     */
234
    protected function decorateNode(string $template, NodeInterface $node): string
235
    {
236
        $decorator = $this->decorator;
237
238
        return preg_replace_callback('/(\{node\.(([a-z]\.*)+)\})/', function ($match) use ($node, $decorator) {
239
            $key = explode('.', $match[2]);
240
            $key = array_shift($key);
241
            $attrs = $decorator->decorate($node, [$key]);
242
243
            return $this->getArrayValue($attrs, $match[2]);
244
        }, $template);
245
    }
246
247
    /**
248
     * Replace variables.
249
     */
250
    protected function decorateUser(string $template, User $user): string
251
    {
252
        $role_decorator = $this->role_decorator;
253
254
        return preg_replace_callback('/(\{user\.(([a-z]\.*)+)\})/', function ($match) use ($user, $role_decorator) {
255
            $key = explode('.', $match[2]);
256
            $key = array_shift($key);
257
            $attrs = $role_decorator->decorate($user, [$key]);
258
259
            return $this->getArrayValue($attrs, $match[2]);
260
        }, $template);
261
    }
262
263
    /**
264
     * Get array value via string path.
265
     */
266
    protected function getArrayValue(Iterable $array, string $path, string $separator = '.')
267
    {
268
        if (isset($array[$path])) {
269
            return $array[$path];
270
        }
271
        $keys = explode($separator, $path);
272
273
        foreach ($keys as $key) {
274
            if (!isset($array[$key])) {
275
                return '';
276
            }
277
278
            $array = $array[$key];
279
        }
280
281
        return $array;
282
    }
283
}
284