Passed
Pull Request — master (#99)
by Glynn
04:13
created

PHP_Engine::clean_filename()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Basic PHP engine for using the Renderable interface.
7
 *
8
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
9
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
10
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
11
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
12
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
13
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
14
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
15
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
16
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
17
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
18
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
19
 *
20
 * @author Glynn Quelch <[email protected]>
21
 * @license http://www.opensource.org/licenses/mit-license.html  MIT License
22
 * @package PinkCrab\Perique\View
23
 */
24
25
namespace PinkCrab\Perique\Services\View;
26
27
use Exception;
28
use PinkCrab\Perique\Interfaces\Renderable;
29
use PinkCrab\Perique\Services\View\View_Model;
30
use PinkCrab\Perique\Services\View\Component\Component;
31
use PinkCrab\Perique\Services\View\Component\Component_Compiler;
32
33
class PHP_Engine implements Renderable {
34
35
	/**
36
	 * The path to base of templates.
37
	 *
38
	 * @var string
39
	 */
40
	protected $base_view_path;
41
42
	/**
43
	 * Access to the component compiler.
44
	 *
45
	 * @var Component_Compiler
46
	 */
47
	protected $component_compiler;
48
49
	/**
50
	 * Creates an instance of the PHP_Engine
51
	 *
52
	 * @param string $base_view_path
53
	 */
54
	public function __construct( string $base_view_path ) {
55
		$this->base_view_path = $this->verify_view_path( $base_view_path );
56
	}
57
58
	/**
59
	 * Sets the component compiler.
60
	 *
61
	 * @param Component_Compiler $compiler
62
	 * @return void
63
	 */
64
	public function set_component_compiler( Component_Compiler $compiler ): void {
65
		$this->component_compiler = $compiler;
66
	}
67
68
	/**
69
	 * Renders a template with data.
70
	 *
71
	 * @param string $view
72
	 * @param iterable<string, mixed> $data
73
	 * @param bool $print
74
	 * @return string|void
75
	 */
76
	public function render( string $view, iterable $data, bool $print = true ) {
77
		$view = $this->resolve_file_path( $view );
78
		if ( $print ) {
79
			print( $this->render_buffer( $view, $data ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
80
		} else {
81
			return $this->render_buffer( $view, $data );
82
		}
83
	}
84
85
	/**
86
	 * Renders a component.
87
	 *
88
	 * @param Component $component
89
	 * @return string|void
90
	 */
91
	public function component( Component $component, bool $print = true ) {
92
93
		// Throw exception of no compiler passed.
94
		if ( ! is_a( $this->component_compiler, Component_Compiler::class ) ) {
95
			throw new Exception( 'No component compiler passed to PHP_Engine' );
96
		}
97
98
		// Compile the component.
99
		$compiled = $this->component_compiler->compile( $component );
100
		$template = $this->maybe_resolve_dot_notation( $compiled->template() );
101
		$view     = sprintf( '%s%s.php', \DIRECTORY_SEPARATOR, ltrim( $template ) );
102
		if ( $print ) {
103
			print( $this->render_buffer( $view, $compiled->data() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
104
		} else {
105
			return $this->render_buffer( $view, $compiled->data() );
106
		}
107
	}
108
109
110
	/**
111
	 * Renders a view Model
112
	 *
113
	 * @param View_Model $view_model
114
	 * @return string|void
115
	 */
116
	public function view_model( View_Model $view_model, bool $print = true ) {
117
		return $this->render( $view_model->template(), $view_model->data(), $print );
118
	}
119
120
	/**
121
	 * Include a partial sub template
122
	 *
123
	 * @param string $view
124
	 * @param iterable<string, mixed> $data
125
	 * @param bool $print
126
	 * @return string|void
127
	 */
128
	public function partial( string $view, iterable $data = array(), bool $print = true ) {
129
		if ( $print ) {
130
			$this->render( $view, $data, $print );
131
		} else {
132
			return $this->render( $view, $data, $print );
133
		}
134
	}
135
136
	/**
137
	 * Builds the view.
138
	 *
139
	 * @param string $view
140
	 * @param iterable<string, mixed> $__data
141
	 * @return string
142
	 * @throws Exception
143
	 */
144
	protected function render_buffer( string $view, iterable $__data ): string {
145
146
		if ( ! file_exists( $view ) ) {
147
			throw new Exception( "{$view} doesn't exist" );
148
		}
149
150
		$output = '';
151
		ob_start();
152
153
		// Set all the data values a parameters.
154
		foreach ( $__data as $__key => $__value ) {
155
			if ( is_string( $__key ) ) {
156
				${\wp_strip_all_tags( $__key )} = $__value;
157
			}
158
159
			// Unset the key and value.
160
			unset( $__key, $__value );
161
		}
162
163
		// Unset the data.
164
		unset( $__data );
165
166
		include $view;
167
		$output = ob_get_contents();
168
		ob_end_clean();
169
		return $output ?: '';
170
	}
171
172
	/**
173
	 * Resolves the filepath from a filename.
174
	 *
175
	 * @param string $filename
176
	 * @return string
177
	 */
178
	protected function resolve_file_path( string $filename ): string {
179
		$filename = $this->maybe_resolve_dot_notation( $filename );
180
		return sprintf(
181
			'%s%s.php',
182
			$this->base_view_path,
183
			ltrim( $filename )
184
		);
185
	}
186
187
	/**
188
	 * Replaces dots with directory separator based on OS from $filename
189
	 *
190
	 * @param string $filename
191
	 * @return string
192
	 */
193
	protected function maybe_resolve_dot_notation( string $filename ): string {
194
		if ( $this->str_ends_with( '.php', $filename ) ) {
195
			$filename = substr( $filename, 0, -4 );
196
		}
197
198
		$parts    = explode( '.', $filename );
199
		$filename = implode( DIRECTORY_SEPARATOR, $parts );
200
201
		return $filename;
202
	}
203
204
	/**
205
	 * Polyfill with str_replace for str_ends_with
206
	 *
207
	 * @param string $needle
208
	 * @param string $haystack
209
	 * @return bool
210
	 */
211
	protected function str_ends_with( string $needle, string $haystack ): bool {
212
		$needle_len = strlen( $needle );
213
		return ( $needle_len === 0 || 0 === substr_compare( $haystack, $needle, - $needle_len ) );
214
	}
215
216
217
	/**
218
	 * Verifies the view path exists and it has the trailing slash.
219
	 *
220
	 * @param string $path
221
	 * @return string
222
	 * @throws Exception
223
	 */
224
	protected function verify_view_path( string $path ): string {
225
		$path = $this->maybe_resolve_dot_notation( $path );
226
		$path = rtrim( $path, '/' ) . '/';
227
228
		if ( ! \is_dir( $path ) ) {
229
			throw new Exception( "{$path} doesn't exist and cant be used as the base view path." );
230
		}
231
232
		return $path;
233
	}
234
}
235