Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
81 / 81
100.00% covered (success)
100.00%
19 / 19
CRAP
100.00% covered (success)
100.00%
1 / 1
View
100.00% covered (success)
100.00%
81 / 81
100.00% covered (success)
100.00%
19 / 19
38
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 set
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addScript
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 scripts
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 addStyle
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 styles
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 useLayout
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 layout
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 block
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 startBlock
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 endBlock
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addFolder
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 folder
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setTheme
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 theme
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fetch
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 getRealPath
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
5
 includeFunctions
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3namespace Dynart\Micro;
4
5class View implements ViewInterface {
6
7    const CONFIG_DEFAULT_FOLDER = 'view.default_folder';
8
9    protected ConfigInterface $config;
10
11    /** The layout for the currently fetched template */
12    protected string $layout = '';
13
14    /** The theme path for overriding templates */
15    protected string $theme = '';
16
17    /** Holds the blocks queue for start/end block functions */
18    protected array $blockQueue = [];
19
20    /** Holds the content of the blocks by name */
21    protected array $blocks = [];
22
23    /** Stores the paths for the folders by namespace */
24    protected array $folders = [];
25
26    /** Holds the view variables */
27    protected array $data = [];
28
29    /** Holds the scripts in [priority => [src => attributes]] form */
30    protected array $scripts = [];
31    protected array $scriptsRegistry = [];
32    /** Holds the styles in [priority => [src => attributes]] form */
33    protected array $styles = [];
34    protected array $stylesRegistry = [];
35
36    /** The functions were included? */
37    protected bool $functionsIncluded = false;
38
39    public function __construct(ConfigInterface $config) {
40        $this->config = $config;
41    }
42
43    public function get(string $name, mixed $default = null): mixed {
44        return $this->data[$name] ?? $default;
45    }
46
47    public function set(string $name, mixed $value): void {
48        $this->data[$name] = $value;
49    }
50
51    public function addScript(string $src, array $attributes=[], int $priority = 50): void {
52        if (in_array($src, $this->scriptsRegistry)) {
53            return;
54        }
55        $this->scriptsRegistry[] = $src;
56        if (!isset($this->scripts[$priority])) {
57            $this->scripts[$priority] = [];
58        }
59        $this->scripts[$priority][$src] = $attributes;
60    }
61
62    public function scripts(): array {
63        ksort($this->scripts);
64        $flattened = [];
65        foreach ($this->scripts as $priorityGroup) {
66            foreach ($priorityGroup as $src => $attributes) {
67                $flattened[$src] = $attributes;
68            }
69        }
70        return $flattened;
71    }
72
73    public function addStyle(string $src, array $attributes=[], int $priority = 50): void {
74        if (in_array($src, $this->stylesRegistry)) {
75            return;
76        }
77        $this->stylesRegistry[] = $src;
78        if (!isset($this->styles[$priority])) {
79            $this->styles[$priority] = [];
80        }
81        $this->styles[$priority][$src] = $attributes;
82    }
83
84    public function styles(): array {
85        ksort($this->styles);
86        $flattened = [];
87        foreach ($this->styles as $priorityGroup) {
88            foreach ($priorityGroup as $src => $attributes) {
89                $flattened[$src] = $attributes;
90            }
91        }
92        return $flattened;
93    }
94
95    public function useLayout(string $path): void {
96        $this->layout = $path;
97    }
98
99    public function layout(): string {
100        return $this->layout;
101    }
102
103    public function block(string $name): string {
104        return $this->blocks[$name] ?? '';
105    }
106
107    public function startBlock(string $name): void {
108        if (!isset($this->blocks[$name])) {
109            $this->blocks[$name] = '';
110        }
111        $this->blockQueue[] = $name;
112        ob_start();
113    }
114
115    public function endBlock(): void {
116        $name = array_pop($this->blockQueue);
117        $this->blocks[$name] .= ob_get_clean();
118    }
119
120    public function addFolder(string $namespace, string $path): void {
121        $this->folders[$namespace] = $path;
122    }
123
124    public function folder(string $namespace): string {
125        return $this->folders[$namespace];
126    }
127
128    public function setTheme(string $path): void {
129        $this->theme = $path;
130    }
131
132    public function theme(): string {
133        return $this->theme;
134    }
135
136    public function fetch(string $__viewPath, array $__vars=[]): string {
137        $this->includeFunctions();
138        $__path = $this->getRealPath($__viewPath).'.phtml';
139        if (!file_exists($__path)) {
140            throw new MicroException("Can't find view: $__viewPath$__path");
141        }
142        extract($this->data);
143        extract($__vars);
144        ob_start();
145        include $__path;
146        $result = ob_get_clean();
147        $layout = $this->layout;
148        if ($layout) {
149            $this->layout = '';
150            $result = $this->fetch($layout, $__vars);
151        }
152        return $result;
153    }
154
155    /**
156     * Returns with the real path to the template file
157     *
158     * * If the path doesn't contain a namespace it will use the `view.default_folder` config value to determine the path for the folder.
159     * * If the path contains a namespace it will use the folder of the namespace.
160     * * If the view has a theme, that path will be checked first, so the theme can override every template.
161     *
162     * For example if you added a folder with namespace 'folder':
163     *
164     * <pre>
165     * $view->addFolder('folder', '~/views/example');
166     * </pre>
167     *
168     * and the `app.root_path` config value is '/var/www/example.com'
169     * the result will be '/var/www/example.com/views/example/index.phtml'
170     *
171     * @throws MicroException If the view path has a namespace but a folder wasn't added for it
172     */
173    protected function getRealPath(string $path): string {
174        $colonPos = strpos($path, ':');
175        if ($colonPos !== false) {
176            $namespace = substr($path, 0, $colonPos);
177            if (!isset($this->folders[$namespace])) {
178                throw new MicroException("Folder wasn't added with namespace: $namespace");
179            }
180            $folder = $this->folders[$namespace];
181            $name = substr($path, $colonPos + 1);
182            $themePath = $this->theme.'/'.$namespace.'/'.$name;
183        } else {
184            $folder = $this->config->get(self::CONFIG_DEFAULT_FOLDER);
185            $name = $path;
186            $themePath = $this->theme.'/'.$path;
187        }
188        if ($this->theme) {
189            $themeFullPath = $this->config->getFullPath($themePath);
190            if (file_exists($themeFullPath.'.phtml')) {
191                return $themeFullPath;
192            }
193        }
194        return $this->config->getFullPath($folder.'/'.$name);
195    }
196
197    /**
198     * Includes the functions.php of the theme, of the app and of the framework
199     */
200    protected function includeFunctions(): void {
201        if ($this->functionsIncluded) {
202            return;
203        }
204        $themeFunctionsPath = $this->config->getFullPath($this->theme.'/functions.php');
205        $appFunctionsPath = $this->config->getFullPath($this->config->get(self::CONFIG_DEFAULT_FOLDER).'/'.'functions.php');
206        $defaultFunctionsPath = dirname(__FILE__) . '/../views/functions.php';
207        if ($this->theme && file_exists($themeFunctionsPath)) {
208            require_once $themeFunctionsPath;
209        }
210        if (file_exists($appFunctionsPath)) {
211            require_once $appFunctionsPath;
212        }
213        require_once $defaultFunctionsPath;
214        $this->functionsIncluded = true;
215    }
216}