├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── debug ├── cache │ └── .gitignore ├── components │ ├── layout.php │ ├── page.php │ └── partials │ │ └── mypartial.php ├── expressions.php └── index.php ├── examples ├── cache │ └── .gitignore ├── css │ └── shop.css ├── main.php └── templates │ ├── partials │ ├── product-list.php │ └── product.php │ └── shop.php ├── phpunit.xml.dist ├── src ├── CacheTemplate.php ├── ConvertJsExpression.php ├── Engine.php ├── Node.php └── Undefined.php └── tests └── integration ├── VuePreTest.php ├── cache └── .gitignore └── templates ├── all.html └── all.result.html /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Lorenz Vandevelde 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # VuePre 3 | VuePre is a package to prerender vue templates. This is useful for SEO and avoiding blank pages on page load. What VuePre does, is translating the Vue template to a PHP template, then replaces the javascript expressions with PHP expressions and caches it. Once cached, you can render thousands of templates per second depending on your hardware. 4 | 5 | ## PROS 6 | ``` 7 | - Very fast 8 | - No dependencies 9 | ``` 10 | 11 | ## CONS 12 | ``` 13 | - Some javascript expressions are not supported (yet). 14 | ``` 15 | 16 | ## Installation 17 | ``` 18 | composer require ctxkiwi/vue-pre 19 | ``` 20 | 21 | ## Basic usage 22 | 23 | ```php 24 | $vue = new \VuePre\Engine(); 25 | $vue->setCacheDirectory(__DIR__ . '/cache'); 26 | 27 | // Method 1 28 | $data = ["name" => "world"]; 29 | $html = $vue->renderHtml('
Hello {{ name }}!
', $data); 30 | 31 | // Method 2 - Using component directory (required if you use sub-components) 32 | $vue->setComponentDirectory(__DIR__ . '/components'); 33 | $html = $vue->renderComponent('my-page', $data); 34 | ``` 35 | 36 | ## Component directory 37 | 38 | ```php 39 | // If you set your directory like this 40 | $vue->setComponentDirectory(__DIR__ . '/components'); 41 | // It's going to look for any .php file and register the filename as a component 42 | // So, if you have components/pages/homepage.php 43 | // It will use this file for the component 44 | ``` 45 | 46 | ## Component example 47 | 48 | ```php 49 | function (&$data) { 52 | $data['counter'] = 0; 53 | }, 54 | ]; 55 | ?> 56 | 57 | 64 | 65 | 79 | ``` 80 | 81 | ## Real world example 82 | 83 | ```php 84 | class View{ 85 | public static function render($view, $data = []){ 86 | // Normal PHP template engine 87 | ... 88 | return $html; 89 | } 90 | public static function renderComponent($name, $data = []){ 91 | $vue = new \VuePre\Engine(); 92 | $vue->setCacheDirectory(Path::get('tmp'). '/cache'); 93 | $vue->setComponentDirectory(Path::get('views') . '/components'); 94 | 95 | $html = $vue->renderComponent($name, $data); 96 | $templates = $vue->getTemplateScripts(); 97 | $js = $vue->getJsScripts(); 98 | $vueInstance = $vue->getVueInstanceScript('#app', $name, $data); 99 | 100 | $html = '
'.$html.'
'.$templates.$js.$vueInstance; 101 | 102 | return static::render('layouts/main.html', ['CONTENT' => $html]; 103 | } 104 | } 105 | 106 | class ViewController{ 107 | public function homepage(){ 108 | $data = [ 109 | // Dont put private data in here, because it's shared with javascript 110 | 'layoutData' => [ 111 | 'authUser' => \AuthUser::getUser()->getPublicData(), 112 | ], 113 | 'featureProducts' => Product::where('featured', true)->limit(10)->get(); 114 | ]; 115 | // Render component 116 | echo View::renderComponent('homepage', $data); 117 | } 118 | } 119 | ``` 120 | 121 | ```html 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | {!! $CONTENT !!} 130 | 131 | 132 | ``` 133 | 134 | ```php 135 | function (&$data) { 140 | $data = $data['layout-data']; 141 | }, 142 | ]; 143 | ?> 144 | 145 | 154 | 155 | 164 | ``` 165 | 166 | ```php 167 | 170 | 171 | 181 | 182 | 191 | ``` 192 | 193 | ## Generating \ 194 | 195 | You can generate scripts for your component templates and your component.js files. 196 | 197 | ```php 198 | // Based on your last render 199 | $vue->getScripts(); 200 | $vue->getTemplateScripts(); // only template scripts 201 | $vue->getJsScripts(); // only js scripts 202 | 203 | // By component name 204 | $vue->getTemplateScript('my-page'); 205 | $vue->getJsScript('my-page'); 206 | 207 | // Usefull 208 | $vue->getRenderedComponentNames(); 209 | ``` 210 | 211 | ## API 212 | 213 | ```php 214 | ->setCacheDirectory(String $path) 215 | ->setComponentDirectory(String $path) 216 | ->setGlobals(Array $globalVariables) // e.g. ['loggedIn' => true, 'user' => ['id'=>123, 'username'=>'TerryDavis']] 217 | ->renderHtml(String $html, Array $data) 218 | ->renderComponent(String $componentName, Array $data) 219 | 220 | // Optional settings 221 | ->ignoreAttributes(Array $attributeNames) 222 | ->unignoreAttributes(Array $attributeNames) 223 | ->getIgnoredAttributes() : Array $attributeNames 224 | 225 | // Generating scripts 226 | ->getScripts($idPrefix = 'vue-template-'); 227 | ->getTemplateScripts($idPrefix = 'vue-template-'); 228 | ->getTemplateScript(String $componentName, $default = null, $idPrefix = 'vue-template-'); 229 | ->getJsScripts(); 230 | ->getJsScript(String $componentName, $default = null); 231 | 232 | // Others 233 | ->getComponentAlias(String $componentName, $default = null) 234 | ->getRenderedComponentNames(); 235 | ``` 236 | 237 | 238 | ## JS expressions | Supported 239 | 240 | ``` 241 | # Prototype functions 242 | .indexOf() 243 | .length 244 | 245 | # JS Functions 246 | typeof() 247 | 248 | # Values: variables, strings, numbers, booleans, null, objects, arrays, functions 249 | 250 | # Comparisons 251 | myVar === 'Hello' 252 | something ? 'yes' : false 253 | 254 | # Nested expressions 255 | (((5 + 5) > 2) ? true : false) ? (myBool ? 'Yes' : 'Yez') : 'No' 256 | 257 | # Objects 258 | product.active ? product.name : product.category.name 259 | 260 | # Methods using $vuePre->setMethods(['myFunc'=> function(){ ... }]) 261 | product.active ? myFunc(product.name) : null 262 | ``` 263 | 264 | ## Todos 265 | 266 | Note: Feel free to make an issue for these, so i can make them a prority. The only reason these are not implemented yet is because of low priority. 267 | 268 | - Custom error handlers 269 | - Options: 270 | - `ignoreVariableNotFound` 271 | - `ignoreVariableNames` `ignoreMethodNames` 272 | - `ignoreSubComponents` `ignoreSubComponentNames` 273 | 274 | ## Contributors 275 | 276 | The DOM iterator code was partially copied from [wmde/php-vuejs-templating](https://github.com/wmde/php-vuejs-templating) 277 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ctxkiwi/vue-pre", 3 | "description": "Pre-render Vue.js templates in PHP", 4 | "type": "library", 5 | "authors": [ 6 | { 7 | "name": "Lorenz Vandevelde", 8 | "email": "vdvlorenz@gmail.com" 9 | } 10 | ], 11 | "autoload": { 12 | "psr-4": { 13 | "VuePre\\": "src" 14 | } 15 | }, 16 | "require": { 17 | "php": ">=5.6.0" 18 | }, 19 | "require-dev": { 20 | "phpunit/phpunit": "^5.4.0|^6.0", 21 | "symfony/var-dumper": "^4.1" 22 | }, 23 | "scripts": { 24 | "test": "vendor/bin/phpunit" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /debug/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /debug/components/layout.php: -------------------------------------------------------------------------------- 1 | function (&$data) { 4 | $data = $data['layout-data']; 5 | }, 6 | ]; 7 | ?> 8 | 9 | 19 | 20 | 29 | -------------------------------------------------------------------------------- /debug/components/page.php: -------------------------------------------------------------------------------- 1 | 2 | 90 | 91 | 114 | -------------------------------------------------------------------------------- /debug/components/partials/mypartial.php: -------------------------------------------------------------------------------- 1 | function (&$data) { 5 | $data['showThis'] = true; 6 | }, 7 | ]; 8 | 9 | ?> 10 | 11 | 29 | 30 | 48 | -------------------------------------------------------------------------------- /debug/expressions.php: -------------------------------------------------------------------------------- 1 | 6, 12 | 'str' => 'Test', 13 | 'product' => [ 14 | 'name' => 'Foobar', 15 | 'active' => true, 16 | 'price' => [ 17 | 'value' => 5, 18 | ], 19 | ], 20 | 'myFunc' => function () { 21 | return 'mayo'; 22 | }, 23 | 'price' => function ($product) { 24 | return $product['price']['value']; 25 | }, 26 | ]; 27 | 28 | $expressions = [ 29 | 'foo', 30 | 'foo > 6 === true', 31 | 'foo < 8', 32 | 'foo < -6', 33 | 'foo > -6', 34 | 'foo == 6', 35 | "foo == '6'", 36 | 'foo === 6', 37 | 'str + str', 38 | 'str + \'2\'', 39 | 'foo + foo', 40 | 'foo + 2', 41 | 'foo + 2 + 3 + foo', 42 | 'str + str + \'___\' + str', 43 | 'product', 44 | 'product.active', 45 | 'product[\'active\']', 46 | 'product[(product.active ? \'name\' : \'active\')]', 47 | '!product', 48 | '!product.active', 49 | '{ active: product.active }', 50 | '{ active: product.active ? true : false }', 51 | 'product.price.value > 5', 52 | 'product.price.value === 5', 53 | 'product.price.value < 7', 54 | 'product.price.value < (product.active ? 7 : 3)', 55 | '[1, 2] === [1, 2]', 56 | '[1, 2] !== [1, 2]', 57 | '[1, true] === [1, product.active]', 58 | '[1, true].indexOf(1)', 59 | '[1, true].indexOf(2)', 60 | '[1, true].indexOf(true)', 61 | "'abc'.indexOf('b')", 62 | "product.name.indexOf('a')", 63 | '[1, true].length', 64 | "'abc'.length", 65 | 'product.name.length', 66 | 'myFunc()', 67 | 'price(product)', 68 | ]; 69 | 70 | $runPhpExpr = function ($ex, $averyrandomvarname) { 71 | foreach ($averyrandomvarname as $k => $v) { 72 | ${$k} = $v; 73 | } 74 | 75 | try { 76 | // ini_set('display_errors', 0); 77 | eval('$res = (' . $ex . ');'); 78 | // ini_set('display_errors', 1); 79 | if (is_bool($res)) { 80 | $res = $res ? 'true' : 'false'; 81 | } 82 | return $res; 83 | } catch (Exception $e) { 84 | return '#ERROR'; 85 | } 86 | }; 87 | 88 | $style = 'background-color:#eee; padding:3px 8px; border:1px solid #ccc; display: inline-block;'; 89 | foreach ($expressions as $ex) { 90 | $phpex = ConvertJsExpression::convert($ex); 91 | echo '
'; 92 | echo '' . $ex . ''; 93 | echo ' => '; 94 | echo '' . $phpex . ''; 95 | 96 | $result = $runPhpExpr($phpex, $data); 97 | echo ' => '; 98 | echo ''; 99 | print_r($result); 100 | echo '
'; 101 | echo '
'; 102 | } 103 | -------------------------------------------------------------------------------- /debug/index.php: -------------------------------------------------------------------------------- 1 | [ 9 | 'title' => 'Yawza', 10 | ], 11 | 'title' => '

Hi

', 12 | 'toggle' => true, 13 | 'aclass' => 'laclass', 14 | 'messages' => explode(' ', 'Hello there my old chum'), 15 | 'myVar' => 'Hello', 16 | 'myObject' => (object) [ 17 | 'myProp' => 'World', 18 | ], 19 | 'dynCompo' => 'mypartial', 20 | 'myclass' => 'red', 21 | 'stylee' => 'color:green', 22 | 'nulltest' => (object) ['value' => null], 23 | 'multiParamFunc' => function ($nr1, $nr2) {return $nr1 + $nr2;}, 24 | 'func' => function ($text) {return $text;}, 25 | ]; 26 | 27 | $vue = new \VuePre\Engine(); 28 | $vue->disableCache = true; 29 | $vue->setCacheDirectory(__DIR__ . '/cache'); 30 | $vue->setComponentDirectory(__DIR__ . '/components'); 31 | $vue->setGlobals(['myGlobal' => 'HelloGlobe']); 32 | 33 | // $benchSeconds = 2; 34 | // $end = time() + $benchSeconds; 35 | // $compileTimes = 0; 36 | // while (time() < $end) { 37 | // $html = $vue->renderComponent('page', $data); 38 | // $compileTimes++; 39 | // } 40 | // echo 'Compiled ' . ($compileTimes / $benchSeconds) . ' times per second'; 41 | // exit; 42 | 43 | $html = $vue->renderComponent('page', $data); 44 | $templates = $vue->getTemplateScripts(); 45 | $js = $vue->getJsScripts(); 46 | $vueInstance = $vue->getVueInstanceScript('#app', 'page', $data); 47 | 48 | // $html = $vue->renderHtml('
{{ title }}
', $data); 49 | ?> 50 | 51 | 60 | 61 | 62 | 63 |
64 | 65 |
66 | 67 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /examples/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /examples/css/shop.css: -------------------------------------------------------------------------------- 1 | 2 | *{ 3 | margin: 0; 4 | padding: 0; 5 | box-sizing:border-box; 6 | } 7 | 8 | body{ 9 | font-family: Roboto; 10 | } 11 | 12 | header{ 13 | text-align: center; 14 | background-color: #eee; 15 | font-size:30px; 16 | padding:25px; 17 | font-weight: bold; 18 | margin-bottom:50px; 19 | } 20 | 21 | .container{ 22 | margin:0 auto; 23 | max-width: 1000px; 24 | } 25 | 26 | h2{ 27 | display:block; 28 | border-bottom:1px solid #444; 29 | padding-bottom: 5px; 30 | margin-bottom:15px; 31 | } 32 | 33 | .list{ 34 | display:flex; 35 | flex-wrap: wrap; 36 | } 37 | 38 | .product{ 39 | flex-grow:1; 40 | float: left; 41 | width: 20%; 42 | margin: 10px; 43 | background-color:#eee; 44 | border:1px solid #ddd; 45 | padding: 15px; 46 | height:300px; 47 | } 48 | -------------------------------------------------------------------------------- /examples/main.php: -------------------------------------------------------------------------------- 1 | [ 9 | 'name' => 'My Shop', 10 | 'products' => [ 11 | ['name' => 'Bread'], 12 | ['name' => 'Horse'], 13 | ['name' => 'Stingray'], 14 | ['name' => 'Ball of paper'], 15 | ['name' => 'Ceiling'], 16 | ['name' => 'Wheels'], 17 | ], 18 | ], 19 | ]; 20 | 21 | $vue = new \VuePre\Engine(); 22 | $vue->setCacheDirectory(__DIR__ . '/cache'); 23 | $vue->setComponentDirectory(__DIR__ . '/templates'); 24 | 25 | $html = $vue->renderComponent('shop', $data); 26 | ?> 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 |
37 | 38 | -------------------------------------------------------------------------------- /examples/templates/partials/product-list.php: -------------------------------------------------------------------------------- 1 | 2 | 13 | -------------------------------------------------------------------------------- /examples/templates/partials/product.php: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /examples/templates/shop.php: -------------------------------------------------------------------------------- 1 | 2 | 18 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests/ 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/CacheTemplate.php: -------------------------------------------------------------------------------- 1 | engine = $engine; 15 | } 16 | 17 | public function render($data, $options = []): String { 18 | $html = ''; 19 | 20 | $firstEl = true; 21 | foreach ($this->nodes as $k => $node) { 22 | if ($firstEl && $node->nodeType === 1) { 23 | // First element 24 | $firstEl = false; 25 | // Merge class/style if in options 26 | if (isset($options['class'])) { 27 | $node->class = "'" . ($options['class']) . " ' . " . (isset($node->class) ? $node->class : "''"); 28 | } 29 | if (isset($options['style'])) { 30 | $node->style = "'" . ($options['style']) . " ' . " . (isset($node->style) ? $node->style : "''"); 31 | } 32 | } 33 | $html .= $this->renderNode($node, $data); 34 | } 35 | 36 | return $html; 37 | } 38 | 39 | public function addDomNode(DOMNode $node) { 40 | $cacheNode = new Node($this); 41 | $cacheNode->settings->isRootEl = true; 42 | $cacheNode->parseDomNode($node); 43 | $this->nodes[] = $cacheNode->export(); 44 | } 45 | 46 | public function export() { 47 | $result = (object) [ 48 | 'nodes' => $this->nodes, 49 | ]; 50 | 51 | return $result; 52 | } 53 | 54 | public function import($exportData) { 55 | 56 | if (isset($exportData->nodes)) { 57 | $this->nodes = $exportData->nodes; 58 | } 59 | 60 | return $this; 61 | } 62 | 63 | public function renderNode($node, $data): String { 64 | 65 | $this->errorLineNr = $node->line; 66 | 67 | $html = isset($node->content) ? $node->content : ''; 68 | 69 | // VFOR 70 | if (isset($node->vfor)) { 71 | $html = ''; 72 | $items = $this->eval($node->vfor, $data); 73 | $nodeCopy = json_decode(json_encode($node)); // Deep clone 74 | $nodeCopy->vfor = null; 75 | foreach ($items as $k => $v) { 76 | if (isset($node->vforIndexName)) {$data[$node->vforIndexName] = $k;} 77 | if (isset($node->vforAsName)) {$data[$node->vforAsName] = $v;} 78 | $html .= $this->renderNode($nodeCopy, $data); 79 | } 80 | return $html; 81 | } 82 | 83 | // VIF 84 | if (isset($node->vif)) { 85 | $node->vifResult = $this->eval($node->vif, $data); 86 | if (!$node->vifResult) {return '';} 87 | } 88 | if (isset($node->velseif) && (!isset($node->vifResult) || !$node->vifResult)) { 89 | $node->vifResult = $this->eval($node->velseif, $data); 90 | if (!$node->vifResult) {return '';} 91 | } 92 | if (isset($node->velse) && (!isset($node->vifResult) || $node->vifResult)) { 93 | $node->vifResult = null; 94 | return ''; 95 | } 96 | 97 | // VSLOT 98 | if (isset($node->vslot)) { 99 | $slotHtml = $this->engine->getSlotHtml($node->vslot); 100 | return $slotHtml; 101 | } 102 | 103 | // CLASS 104 | if (isset($node->class)) { 105 | $html = str_replace('_VUEPRE_CLASS_', $this->eval($node->class, $data), $html); 106 | } 107 | 108 | // STYLE 109 | if (isset($node->style)) { 110 | $html = str_replace('_VUEPRE_STYLE_', $this->eval($node->style, $data), $html); 111 | } 112 | 113 | // VHTML 114 | if (isset($node->vhtml)) { 115 | $html = str_replace('_VUEPRE_HTML_PLACEHOLDER_', $this->eval($node->vhtml, $data), $html); 116 | } 117 | 118 | // Components 119 | if (isset($node->isComponent)) { 120 | $options = []; 121 | // Render slots 122 | $slotHtml = []; 123 | if (isset($node->slotNodes)) { 124 | foreach ($node->slotNodes as $slotName => $nodes) { 125 | if (!isset($slotHtml[$slotName])) { 126 | $slotHtml[$slotName] = ''; 127 | } 128 | 129 | foreach ($nodes as $slotNode) { 130 | $slotHtml[$slotName] .= $this->renderNode($slotNode, $data); 131 | } 132 | } 133 | } 134 | $options['slots'] = $slotHtml; 135 | // Render component 136 | $newData = []; 137 | if (isset($node->bindedValues)) { 138 | foreach ($node->bindedValues as $k => $expr) { 139 | $newData[$k] = $this->eval($expr, $data); 140 | } 141 | } 142 | 143 | if (isset($node->class)) { 144 | $options['class'] = $this->eval($node->class, $data); 145 | } 146 | if (isset($node->style)) { 147 | $options['style'] = $this->eval($node->style, $data); 148 | } 149 | 150 | $componentName = $this->eval($node->isComponent, $data); 151 | return $this->engine->renderComponent($componentName, $newData, $options); 152 | } 153 | 154 | if (isset($node->bindedValues)) { 155 | foreach ($node->bindedValues as $k => $expr) { 156 | $replace = ''; 157 | if (!in_array($k, $this->engine->getIgnoredAttributes(), true)) { 158 | $replace = $this->eval($expr, $data); 159 | } 160 | $html = str_replace('_VUEPRE_ATR_' . $k . '_ATREND_', $replace, $html); 161 | } 162 | } 163 | 164 | // {{ }} 165 | if (isset($node->mustacheValues)) { 166 | foreach ($node->mustacheValues as $k => $v) { 167 | $html = str_replace($k, $this->eval($v, $data), $html); 168 | } 169 | } 170 | 171 | // SUBNODES 172 | if ($node->nodeType === 1) { 173 | $subHtml = ''; 174 | $vifResult = null; 175 | if (isset($node->childNodes)) { 176 | foreach ($node->childNodes as $cnode) { 177 | $cnode->vifResult = $vifResult; 178 | $subHtml .= $this->renderNode($cnode, $data); 179 | $vifResult = $cnode->vifResult ?? null; 180 | } 181 | } 182 | //