Completed
Push — master ( 6adff3...7d59e1 )
by Rasmus
58s
created

ViewService::render()   C

Complexity

Conditions 7
Paths 24

Size

Total Lines 40
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 7

Importance

Changes 0
Metric Value
dl 0
loc 40
ccs 23
cts 23
cp 1
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 22
nc 24
nop 2
crap 7
1
<?php
2
3
namespace mindplay\kisstpl;
4
5
use Closure;
6
use Exception;
7
use RuntimeException;
8
use Throwable;
9
10
/**
11
 * This service provides a view/template rendering service and a simple output capture facility.
12
 */
13
class ViewService implements Renderer
14
{
15
    /**
16
     * @var ViewFinder
17
     */
18
    public $finder;
19
20
    /**
21
     * @var string the default type of view
22
     *
23
     * @see render()
24
     */
25
    public $default_type = 'view';
26
27
    /**
28
     * @var string[] a stack of variable references being captured to
29
     *
30
     * @see begin()
31
     * @see end()
32
     */
33
    protected $capture_stack = array();
34
35
    /**
36
     * @var int a unique index to track use of the capture stack
37
     *
38
     * @see begin()
39
     * @see end()
40
     */
41
    private $capture_index = 0;
42
43
    /**
44
     * @var string[][] map where view-model class name => map where view type => view path
45
     */
46
    private $path_cache = array();
47
48
    /**
49
     * @var Closure[] map where view path => view Closure
50
     */
51
    private $closure_cache = array();
52
53
    /**
54
     * @param ViewFinder $finder
55
     */
56 1
    public function __construct(ViewFinder $finder)
57
    {
58 1
        $this->finder = $finder;
59 1
    }
60
61
    /**
62
     * Locate and render a PHP template for the given view, directly to output - as
63
     * opposed to {@see capture()} which will use output buffering to capture the
64
     * content and return it as a string.
65
     *
66
     * The view will be made available to the template as <code>$view</code> and the
67
     * calling context (<code>$this</code>) will be this ViewService.
68
     *
69
     * @param object      $view the view-model to render
70
     * @param string|null $type the type of view to render (optional)
71
     *
72
     * @return void
73
     *
74
     * @throws RuntimeException
75
     *
76
     * @see capture()
77
     */
78 1
    public function render($view, $type = null)
79
    {
80 1
        $__type = $type === null
81 1
            ? $this->default_type
82 1
            : $type;
83
84 1
        unset($type);
85
86 1
        $__class = get_class($view);
87
88 1
        if (! isset($this->path_cache[$__class][$__type])) {
89 1
            $this->path_cache[$__class][$__type] = $this->finder->findTemplate($view, $__type);
90
        }
91
92 1
        $__path = $this->path_cache[$__class][$__type];
93
94 1
        if ($__path === null) {
95 1
            $this->onMissingView($view, $__type);
96
97 1
            return;
98
        }
99
100 1
        $__depth = count($this->capture_stack);
101
102 1
        $ob_level = ob_get_level();
103
104 1
        $this->renderFile($__path, $view);
105
106 1
        if (ob_get_level() !== $ob_level) {
107 1
            $error = count($this->capture_stack) !== $__depth
108 1
                ? "begin() without matching end()"
109 1
                : "output buffer-level mismatch: was " . ob_get_level() . ", expected {$ob_level}";
110
111 1
            while (ob_get_level() > $ob_level) {
112 1
                ob_end_clean(); // clean up any hanging output buffers prior to throwing
113
            }
114
115 1
            throw new RuntimeException("{$error} in file: {$__path}");
116
        }
117 1
    }
118
119
    /**
120
     * Render and capture the output from a PHP template for the given view - as
121
     * opposed to {@see render()} which will render directly to output.
122
     *
123
     * Use capture only when necessary, such as when capturing content from a
124
     * rendered template to populate the body of an e-mail.
125
     *
126
     * @param object      $view the view-model to render
127
     * @param string|null $type the type of view to render (optional)
128
     *
129
     * @return string rendered content
130
     *
131
     * @see render()
132
     */
133 1
    public function capture($view, $type = null)
134
    {
135 1
        ob_start();
136
137
        try {
138 1
            $this->render($view, $type);
139 1
        } catch (Exception $exception) {
140
            // re-throwing below (PHP 5.3+)
141
        } catch (Throwable $exception) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
142
            // re-throwing below (PHP 7.0+)
143
        }
144
145 1
        $output = ob_get_clean();
146
147 1
        if (isset($exception)) {
148 1
            throw $exception;
149
        }
150
151 1
        return $output;
152
    }
153
154
    /**
155
     * @param string &$var target variable reference for captured content
156
     *
157
     * @return void
158
     *
159
     * @see end()
160
     */
161 1
    public function begin(&$var)
162
    {
163 1
        $index = $this->capture_index++;
164
165 1
        $var = __CLASS__ . "::\$capture_stack[{$index}]";
166
167 1
        if (in_array($var, $this->capture_stack, true)) {
168 1
            throw new RuntimeException("begin() with same reference as prior begin()");
169
        }
170
171 1
        $this->capture_stack[] = &$var;
172
173
        // begin buffering content to capture:
174 1
        ob_start();
175 1
    }
176
177
    /**
178
     * @param string &$var target variable reference for captured content
179
     *
180
     * @return void
181
     *
182
     * @throws RuntimeException
183
     *
184
     * @see begin()
185
     */
186 1
    public function end(&$var)
187
    {
188 1
        if (count($this->capture_stack) === 0) {
189 1
            throw new RuntimeException("end() without begin()");
190
        }
191
192 1
        $index = count($this->capture_stack) - 1;
193
194 1
        if ($this->capture_stack[$index] !== $var) {
195 1
            throw new RuntimeException("end() with mismatched begin()");
196
        }
197
198
        // capture the buffered content:
199 1
        $this->capture_stack[$index] = ob_get_clean();
200
201
        // remove target variable reference from stack:
202 1
        array_pop($this->capture_stack);
203 1
    }
204
205
    /**
206
     * Internally render a template file (or delegate to a cached closure)
207
     *
208
     * @param string $_path_ absolute path to PHP template
209
     * @param object $view the view-model to render
210
     *
211
     * @return void
212
     */
213 1
    protected function renderFile($_path_, $view)
214
    {
215 1
        if (!isset($this->closure_cache[$_path_])) {
216 1
            $_closure_ = require $_path_;
217
218 1
            if (is_callable($_closure_)) {
219 1
                $this->closure_cache[$_path_] = $_closure_;
220
            }
221
        }
222
223 1
        if (isset($this->closure_cache[$_path_])) {
224 1
            $this->renderClosure($this->closure_cache[$_path_], $view);
0 ignored issues
show
Documentation introduced by
$this->closure_cache[$_path_] is of type callable, but the function expects a object<Closure>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
225
        }
226 1
    }
227
228
    /**
229
     * Internally render a template closure
230
     *
231
     * @param Closure $closure template closure
232
     * @param object  $view    the view-model to render
233
     *
234
     * @return void
235
     */
236 1
    protected function renderClosure($closure, $view)
237
    {
238 1
        call_user_func($closure, $view, $this);
239 1
    }
240
241
    /**
242
     * Called internally, if a view could not be resolved
243
     *
244
     * @param object      $view the view-model attempted to render
245
     * @param string|null $type the type of view attempted to render (optional)
246
     *
247
     * @return void
248
     *
249
     * @see render()
250
     */
251 1
    protected function onMissingView($view, $type)
252
    {
253 1
        $class = get_class($view);
254
255 1
        $paths = $this->finder->listSearchPaths($view, $type);
256
257 1
        $message = count($paths) > 0
258 1
            ? "searched paths:\n  * " . implode("\n  * ", $paths)
259 1
            : "no applicable path(s) found";
260
261 1
        throw new RuntimeException("no view of type \"{$type}\" found for model: {$class} - {$message}");
262
    }
263
}
264