Completed
Pull Request — 1.2 (#12)
by David
04:51
created

FileBasedRenderer::getTemplateFileName()   B

Complexity

Conditions 9
Paths 13

Size

Total Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 41
rs 7.7084
c 0
b 0
f 0
cc 9
nc 13
nop 2
1
<?php
2
/*
3
 * Copyright (c) 2013 David Negrier
4
 *
5
 * See the file LICENSE.txt for copying permission.
6
 */
7
8
namespace Mouf\Html\Renderer;
9
10
use Psr\Container\ContainerInterface;
11
use Mouf\MoufException;
12
use Mouf\Html\Renderer\Twig\MoufTwigExtension;
13
use Mouf\MoufManager;
14
use Psr\SimpleCache\CacheInterface;
15
16
/**
17
 * This class is a renderer that renders objects using a directory containing template files.
18
 * Each file should be a Twig file or PHP file named after the PHP class full name (respecting the PSR-0 notation).
19
 *
20
 * For instance, the class Mouf\Menu\Item would be rendered by the file Mouf\Menu\Item.twig or Mouf\Menu\Item.php
21
 *
22
 * If a context is passed, it will be appended after a double underscore to the file name.
23
 * If the file does not exist, we default to the base class.
24
 *
25
 * For instance, the class Mouf\Menu\Item with context "primary" would be rendered by the file Mouf\Menu\Item__primary.twig
26
 *
27
 * If the template for the class is not found, a test through the parents of the class is performed.
28
 *
29
 * If you are using PHP template files, the properties of the object are accessible using local vars.
30
 * For instance, if your object has a $a property, the property can be accessed using the $a variable.
31
 * Any property (even private properties can be accessed).
32
 * The object is accessible using the $object variable. Private methods or properties of the $object cannot
33
 * be accessed.
34
 *
35
 *
36
 * @author David Négrier <[email protected]>
37
 */
38
class FileBasedRenderer implements ChainableRendererInterface
39
{
40
41
    private $directory;
42
43
    private $cacheService;
44
45
    private $twig;
46
47
	private $tmpFileName;
48
	
49
	private $debugMode;
50
	
51
	private $debugStr;
52
	
53
	/**
54
	 * 
55
	 * @param string $directory The directory of the templates, relative to the project root. Does not start and does not finish with a /
56
	 * @param CacheInterface $cacheService This service is used to speed up the mapping between the object and the template.
57
	 */
58
    public function __construct(string $directory, CacheInterface $cacheService, ContainerInterface $container, \Twig_Environment $twig = null)
59
    {
60
        $this->directory = $directory;
61
        $this->cacheService = $cacheService;
62
63
        $loader = new \Twig_Loader_Filesystem($this->directory);
64
        if (function_exists('posix_geteuid')) {
65
            $posixGetuid = posix_geteuid();
66
        } else {
67
            $posixGetuid = '';
68
        }
69
        $cacheFilesystem = new \Twig_Cache_Filesystem(rtrim(sys_get_temp_dir(),'/\\').'/mouftwigtemplatemain_'.$posixGetuid.'_'.$this->directory);
70
        if ($twig === null) {
71
72
            $this->twig = new \Twig_Environment($loader, array(
73
                // The cache directory is in the temporary directory and reproduces the path to the directory (to avoid cache conflict between apps).
74
                'cache' => $cacheFilesystem,
75
                'auto_reload' => true,
76
                'debug' => true
77
            ));
78
            $this->twig->addExtension(new MoufTwigExtension($container));
79
            $this->twig->addExtension(new \Twig_Extension_Debug());
80
        } else {
81
            // We need to modify the loader of the twig environment.
82
            // Let's clone it.
83
            $this->twig = clone $twig;
84
            $this->twig->setLoader($loader);
85
            $this->twig->setCache($cacheFilesystem);
86
            $this->twig->setCompiler(new \Twig_Compiler($this->twig));
87
        }
88
	}
89
90
    /**
91
     * (non-PHPdoc)
92
     * @see \Mouf\Html\Renderer\RendererInterface::canRender()
93
     */
94
    public function canRender($object, $context = null)
95
    {
96
        $fileName = $this->getTemplateFileName($object, $context);
97
98
        if ($fileName) {
99
            return ChainableRendererInterface::CAN_RENDER_CLASS;
100
        } else {
101
            return ChainableRendererInterface::CANNOT_RENDER;
102
        }
103
    }
104
105
    /**
106
     * (non-PHPdoc)
107
     * @see \Mouf\Html\Renderer\ChainableRendererInterface::debugCanRender()
108
     */
109
    public function debugCanRender($object, $context = null)
110
    {
111
        $this->debugMode = true;
112
        $this->debugStr = "Testing renderer for directory '".$this->directory."'\n";
113
114
        $this->canRender($object, $context);
115
116
        $this->debugMode = false;
117
118
        return $this->debugStr;
119
    }
120
121
    /**
122
     * (non-PHPdoc)
123
     * @see \Mouf\Html\Renderer\RendererInterface::render()
124
     */
125
    public function render($object, string $context = null): void
126
    {
127
        $fileName = $this->getTemplateFileName($object, $context);
128
129
        if ($fileName != false) {
130
            if (method_exists($object, 'getPrivateProperties')) {
131
                $array = $object->getPrivateProperties();
132
            } else {
133
                $array = get_object_vars($object);
134
            }
135
            if ($fileName['type'] == 'twig') {
136
                if (!isset($array['this'])) {
137
                    $array['this'] = $object;
138
                }
139
                echo $this->twig->render($fileName['fileName'], $array);
140
            } else {
141
                // Let's store the filename into the object ($this) in order to avoid name conflict between
142
                // the variables.
143
                $this->tmpFileName = $fileName;
144
145
                extract($array);
146
                // Let's create a local variable
147
                /*foreach ($array as $var__tplt=>$value__tplt) {
148
                    $$var__tplt = $value__tplt;
149
                }*/
150
                include $this->directory.'/'.$this->tmpFileName['fileName'];
151
            }
152
        } else {
153
            throw new NoTemplateFoundException("Cannot render object of class ".get_class($object).". No template found.");
154
        }
155
    }
156
157
    /**
158
     * Returns the filename of the template or false if no file found.
159
     *
160
     * @param  object      $object
161
     * @param  string      $context
162
     * @return array<string,string>|null An array with 2 keys: "filename" and "type", or null if nothing found
163
     */
164
    private function getTemplateFileName($object, ?string $context = null): ?array
165
    {
166
        $fullClassName = get_class($object);
167
168
        // Optimisation: let's see if we already performed the file_exists checks.
169
        $cacheKey = md5("FileBasedRenderer_".$this->directory.'/'.$fullClassName.'/'.$context);
170
171
        $cachedValue = $this->cacheService->get($cacheKey);
172
        if ($cachedValue !== null && !$this->debugMode) {
173
            return $cachedValue;
174
        }
175
176
        $fileName = $this->findFile($fullClassName, $context);
177
        $parentClass = $fullClassName;
178
        // If no file is found, let's go through the parents of the object.
179
        while (true) {
180
            if ($fileName != false) {
181
                break;
182
            }
183
            $parentClass = get_parent_class($parentClass);
184
            if ($parentClass == false) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $parentClass of type string to the boolean false. If you are specifically checking for an empty string, consider using the more explicit === '' instead.
Loading history...
185
                break;
186
            }
187
            $fileName = $this->findFile($parentClass, $context);
188
        }
189
190
        // Still no objects? Let's browse the interfaces.
191
        if ($fileName == false) {
192
            $interfaces = class_implements($fullClassName);
193
            foreach ($interfaces as $interface) {
194
                $fileName = $this->findFile($interface, $context);
195
                if ($fileName != false) {
196
                    break;
197
                }
198
            }
199
        }
200
201
        $this->cacheService->set($cacheKey, $fileName);
202
203
        return $fileName;
204
    }
205
206
    /**
207
     * @param string $className
208
     * @param null|string $context
209
     * @return array<string,string>|null An array with 2 keys: "filename" and "type", or null if nothing found
210
     */
211
    private function findFile(string $className, ?string $context): ?array
212
    {
213
        $baseFileName = str_replace('\\', '/', $className);
214
        if ($context) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $context of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
215
            if (file_exists($this->directory.'/'.$baseFileName.'__'.$context.'.twig')) {
216
                if ($this->debugMode) {
217
                    $this->debugStr .= "  Found file: ".$this->directory.'/'.$baseFileName.'__'.$context.'.twig'."\n";
218
                }
219
220
                return array("fileName" => $baseFileName.'__'.$context.'.twig',
221
                    "type" => "twig", );
222
            }
223
            if ($this->debugMode) {
224
                $this->debugStr .= "  Tested file: ".$this->directory.'/'.$baseFileName.'__'.$context.'.twig'."\n";
225
            }
226
227
            if (file_exists($this->directory.'/'.$baseFileName.'__'.$context.'.php')) {
228
                if ($this->debugMode) {
229
                    $this->debugStr .= "  Found file: ".$this->directory.'/'.$baseFileName.'__'.$context.'.php'."\n";
230
                }
231
232
                return array("fileName" => $baseFileName.'__'.$context.'.php',
233
                    "type" => "php", );
234
            }
235
            if ($this->debugMode) {
236
                $this->debugStr .= "  Tested file: ".$this->directory.'/'.$baseFileName.'__'.$context.'.php'."\n";
237
            }
238
        }
239
        if (file_exists($this->directory.'/'.$baseFileName.'.twig')) {
240
            if ($this->debugMode) {
241
                $this->debugStr .= "  Found file: ".$this->directory.'/'.$baseFileName.'.twig'."\n";
242
            }
243
244
            return array("fileName" => $baseFileName.'.twig',
245
                    "type" => "twig", );
246
        }
247
        if ($this->debugMode) {
248
            $this->debugStr .= "  Tested file: ".$this->directory.'/'.$baseFileName.'.twig'."\n";
249
        }
250
251
        if (file_exists($this->directory.'/'.$baseFileName.'.php')) {
252
            if ($this->debugMode) {
253
                $this->debugStr .= "  Found file: ".$this->directory.'/'.$baseFileName.'.php'."\n";
254
            }
255
256
            return array("fileName" => $baseFileName.'.php',
257
                    "type" => "php", );
258
        }
259
        if ($this->debugMode) {
260
            $this->debugStr .= "  Tested file: ".$this->directory.'/'.$baseFileName.'.php'."\n";
261
        }
262
263
        return null;
264
    }
265
}
266