Passed
Pull Request — master (#88)
by Glynn
02:19
created

PHP_Engine::maybe_resolve_dot_notation()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
c 0
b 0
f 0
dl 0
loc 9
rs 10
cc 2
nc 2
nop 1
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
		$view     = sprintf( '%s%s.php', \DIRECTORY_SEPARATOR, $this->clean_filename( $compiled->template() ) );
101
		if ( $print ) {
102
			print( $this->render_buffer( $view, $compiled->data() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
103
		} else {
104
			return $this->render_buffer( $view, $compiled->data() );
105
		}
106
	}
107
108
109
	/**
110
	 * Renders a view Model
111
	 *
112
	 * @param View_Model $view_model
113
	 * @return string|void
114
	 */
115
	public function view_model( View_Model $view_model, bool $print = true ) {
116
		return $this->render( $view_model->template(), $view_model->data(), $print );
117
	}
118
119
	/**
120
	 * Include a partial sub template
121
	 *
122
	 * @param string $view
123
	 * @param iterable<string, mixed> $data
124
	 * @param bool $print
125
	 * @return string|void
126
	 */
127
	public function partial( string $view, iterable $data = array(), bool $print = true ) {
128
		if ( $print ) {
129
			$this->render( $view, $data, $print );
130
		} else {
131
			return $this->render( $view, $data, $print );
132
		}
133
	}
134
135
	/**
136
	 * Builds the view.
137
	 *
138
	 * @param string $view
139
	 * @param iterable<string, mixed> $__data
140
	 * @return string
141
	 * @throws Exception
142
	 */
143
	protected function render_buffer( string $view, iterable $__data ): string {
144
145
		if ( ! file_exists( $view ) ) {
146
			throw new Exception( "{$view} doesn't exist" );
147
		}
148
149
		$output = '';
150
		ob_start();
151
152
		// Set all the data values a parameters.
153
		foreach ( $__data as $__key => $__value ) {
154
			if ( is_string( $__key ) ) {
155
				${\wp_strip_all_tags( $__key )} = $__value;
156
			}
157
158
			// Unset the key and value.
159
			unset( $__key, $__value, $__data );
160
		}
161
162
		include $view;
163
		$output = ob_get_contents();
164
		ob_end_clean();
165
		return $output ?: '';
166
	}
167
168
	/**
169
	 * Trims any leading slash and removes .php
170
	 *
171
	 * @param string $file
172
	 * @return string
173
	 */
174
	protected function clean_filename( string $file ): string {
175
		$file = ltrim( $file, '/' );
176
		return substr( $file, -4 ) === '.php'
177
			? substr( $file, 0, -4 )
178
			: $file;
179
180
	}
181
182
	/**
183
	 * Resolves the filepath from a filename.
184
	 *
185
	 * @param string $filename
186
	 * @return string
187
	 */
188
	protected function resolve_file_path( string $filename ): string {
189
		$filename = $this->maybe_resolve_dot_notation( $filename );
190
		return sprintf(
191
			'%s%s.php',
192
			$this->base_view_path,
193
			$this->clean_filename( $filename )
194
		);
195
	}
196
197
	/**
198
	 * Replaces dots with directory separator based on OS from $filename
199
	 *
200
	 * @param string $filename
201
	 * @return string
202
	 */
203
	protected function maybe_resolve_dot_notation( string $filename ): string {
204
		if ( $this->str_ends_with( '.php', $filename ) ) {
205
			$filename = substr( $filename, 0, -4 );
206
		}
207
208
		$parts    = explode( '.', $filename );
209
		$filename = implode( DIRECTORY_SEPARATOR, $parts );
210
211
		return $filename;
212
	}
213
214
	/**
215
	 * Polyfill with str_replace for str_ends_with
216
	 *
217
	 * @param string $needle
218
	 * @param string $haystack
219
	 * @return bool
220
	 */
221
	protected function str_ends_with( string $needle, string $haystack ): bool {
222
		$needle_len = strlen( $needle );
223
		return ( $needle_len === 0 || 0 === substr_compare( $haystack, $needle, - $needle_len ) );
224
	}
225
226
227
	/**
228
	 * Verifies the view path exists and it has the trailing slash.
229
	 *
230
	 * @param string $path
231
	 * @return string
232
	 * @throws Exception
233
	 */
234
	protected function verify_view_path( string $path ): string {
235
236
		$path = rtrim( $path, '/' ) . '/';
237
238
		if ( ! \is_dir( $path ) ) {
239
			throw new Exception( "{$path} doesn't exist and cant be used as the base view path." );
240
		}
241
242
		return $path;
243
	}
244
}
245