Completed
Push — master ( 9afaa8...6adff3 )
by Rasmus
01:26 queued 01:20
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 RuntimeException;
7
8
/**
9
 * This service provides a view/template rendering service and a simple output capture facility.
10
 */
11
class ViewService implements Renderer
12
{
13
    /**
14
     * @var ViewFinder
15
     */
16
    public $finder;
17
18
    /**
19
     * @var string the default type of view
20
     *
21
     * @see render()
22
     */
23
    public $default_type = 'view';
24
25
    /**
26
     * @var string[] a stack of variable references being captured to
27
     *
28
     * @see begin()
29
     * @see end()
30
     */
31
    protected $capture_stack = array();
32
33
    /**
34
     * @var int a unique index to track use of the capture stack
35
     *
36
     * @see begin()
37
     * @see end()
38
     */
39
    private $capture_index = 0;
40
41
    /**
42
     * @var string[][] map where view-model class name => map where view type => view path
43
     */
44
    private $path_cache = array();
45
46
    /**
47
     * @var Closure[] map where view path => view Closure
48
     */
49
    private $closure_cache = array();
50
51
    /**
52
     * @param ViewFinder $finder
53
     */
54 1
    public function __construct(ViewFinder $finder)
55
    {
56 1
        $this->finder = $finder;
57 1
    }
58
59
    /**
60
     * Locate and render a PHP template for the given view, directly to output - as
61
     * opposed to {@see capture()} which will use output buffering to capture the
62
     * content and return it as a string.
63
     *
64
     * The view will be made available to the template as <code>$view</code> and the
65
     * calling context (<code>$this</code>) will be this ViewService.
66
     *
67
     * @param object      $view the view-model to render
68
     * @param string|null $type the type of view to render (optional)
69
     *
70
     * @return void
71
     *
72
     * @throws RuntimeException
73
     *
74
     * @see capture()
75
     */
76 1
    public function render($view, $type = null)
77
    {
78 1
        $__type = $type === null
79 1
            ? $this->default_type
80 1
            : $type;
81
82 1
        unset($type);
83
84 1
        $__class = get_class($view);
85
86 1
        if (! isset($this->path_cache[$__class][$__type])) {
87 1
            $this->path_cache[$__class][$__type] = $this->finder->findTemplate($view, $__type);
88
        }
89
90 1
        $__path = $this->path_cache[$__class][$__type];
91
92 1
        if ($__path === null) {
93 1
            $this->onMissingView($view, $__type);
94
95 1
            return;
96
        }
97
98 1
        $__depth = count($this->capture_stack);
99
100 1
        $ob_level = ob_get_level();
101
102 1
        $this->renderFile($__path, $view);
103
104 1
        if (ob_get_level() !== $ob_level) {
105 1
            $error = count($this->capture_stack) !== $__depth
106 1
                ? "begin() without matching end()"
107 1
                : "output buffer-level mismatch: was " . ob_get_level() . ", expected {$ob_level}";
108
109 1
            while (ob_get_level() > $ob_level) {
110 1
                ob_end_clean(); // clean up any hanging output buffers prior to throwing
111
            }
112
113 1
            throw new RuntimeException("{$error} in file: {$__path}");
114
        }
115 1
    }
116
117
    /**
118
     * Render and capture the output from a PHP template for the given view - as
119
     * opposed to {@see render()} which will render directly to output.
120
     *
121
     * Use capture only when necessary, such as when capturing content from a
122
     * rendered template to populate the body of an e-mail.
123
     *
124
     * @param object      $view the view-model to render
125
     * @param string|null $type the type of view to render (optional)
126
     *
127
     * @return string rendered content
128
     *
129
     * @see render()
130
     */
131 1
    public function capture($view, $type = null)
132
    {
133 1
        ob_start();
134
135 1
        $this->render($view, $type);
136
137 1
        return ob_get_clean();
138
    }
139
140
    /**
141
     * @param string &$var target variable reference for captured content
142
     *
143
     * @return void
144
     *
145
     * @see end()
146
     */
147 1
    public function begin(&$var)
148
    {
149 1
        $index = $this->capture_index++;
150
151 1
        $var = __CLASS__ . "::\$capture_stack[{$index}]";
152
153 1
        if (in_array($var, $this->capture_stack, true)) {
154 1
            throw new RuntimeException("begin() with same reference as prior begin()");
155
        }
156
157 1
        $this->capture_stack[] = &$var;
158
159
        // begin buffering content to capture:
160 1
        ob_start();
161 1
    }
162
163
    /**
164
     * @param string &$var target variable reference for captured content
165
     *
166
     * @return void
167
     *
168
     * @throws RuntimeException
169
     *
170
     * @see begin()
171
     */
172 1
    public function end(&$var)
173
    {
174 1
        if (count($this->capture_stack) === 0) {
175 1
            throw new RuntimeException("end() without begin()");
176
        }
177
178 1
        $index = count($this->capture_stack) - 1;
179
180 1
        if ($this->capture_stack[$index] !== $var) {
181 1
            throw new RuntimeException("end() with mismatched begin()");
182
        }
183
184
        // capture the buffered content:
185 1
        $this->capture_stack[$index] = ob_get_clean();
186
187
        // remove target variable reference from stack:
188 1
        array_pop($this->capture_stack);
189 1
    }
190
191
    /**
192
     * Internally render a template file (or delegate to a cached closure)
193
     *
194
     * @param string $_path_ absolute path to PHP template
195
     * @param object $view the view-model to render
196
     *
197
     * @return void
198
     */
199 1
    protected function renderFile($_path_, $view)
200
    {
201 1
        if (!isset($this->closure_cache[$_path_])) {
202 1
            $_closure_ = require $_path_;
203
204 1
            if (is_callable($_closure_)) {
205 1
                $this->closure_cache[$_path_] = $_closure_;
206
            }
207
        }
208
209 1
        if (isset($this->closure_cache[$_path_])) {
210 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...
211
        }
212 1
    }
213
214
    /**
215
     * Internally render a template closure
216
     *
217
     * @param Closure $closure template closure
218
     * @param object  $view    the view-model to render
219
     *
220
     * @return void
221
     */
222 1
    protected function renderClosure($closure, $view)
223
    {
224 1
        call_user_func($closure, $view, $this);
225 1
    }
226
227
    /**
228
     * Called internally, if a view could not be resolved
229
     *
230
     * @param object      $view the view-model attempted to render
231
     * @param string|null $type the type of view attempted to render (optional)
232
     *
233
     * @return void
234
     *
235
     * @see render()
236
     */
237 1
    protected function onMissingView($view, $type)
238
    {
239 1
        $class = get_class($view);
240
241 1
        $paths = $this->finder->listSearchPaths($view, $type);
242
243 1
        $message = count($paths) > 0
244 1
            ? "searched paths:\n  * " . implode("\n  * ", $paths)
245 1
            : "no applicable path(s) found";
246
247 1
        throw new RuntimeException("no view of type \"{$type}\" found for model: {$class} - {$message}");
248
    }
249
}
250